stashign before reboot
This commit is contained in:
parent
61fd5af955
commit
313ea55f57
28
Makefile
Normal file
28
Makefile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Environment Variables
|
||||||
|
CGO =1
|
||||||
|
SRC =cmd
|
||||||
|
BUILD =build
|
||||||
|
PREFIX=$(GOPATH)/bin/
|
||||||
|
|
||||||
|
version?="0.0.0"
|
||||||
|
commit =`if [ -d ./.git ]; then git rev-list -1 HEAD | head -c 8; else echo release-build; fi`
|
||||||
|
date =`date "+%Y-%m-%d"`
|
||||||
|
package =main
|
||||||
|
ldflags ="-X $(package).commit=$(commit) -X $(package).version=$(version) -X $(package).date=$(date)"
|
||||||
|
|
||||||
|
default: darwin
|
||||||
|
|
||||||
|
main: darwin
|
||||||
|
|
||||||
|
linux: clean
|
||||||
|
env CGO_ENABLED=$(CGO) GOOS=$@ go build -ldflags $(ldflags) -o $(BUILD)/shiny-pancake $(SRC)/main.go
|
||||||
|
|
||||||
|
darwin: clean
|
||||||
|
env CGO_ENABLED=$(CGO) GOOS=$@ go build -ldflags $(ldflags) -o $(BUILD)/shiny-pancake $(SRC)/main.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BUILD)/*
|
||||||
|
touch $(BUILD)/.keep
|
||||||
|
|
||||||
|
install:
|
||||||
|
mv $(BUILD)/shiny-pancake $(PREFIX)
|
||||||
11
cmd/add.go
11
cmd/add.go
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"shiny-pancake/database"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
"shiny-pancake/logger"
|
"shiny-pancake/logger"
|
||||||
@ -89,7 +90,7 @@ func configWriter(f *tview.Form){
|
|||||||
var fp string
|
var fp string
|
||||||
var hostFile string
|
var hostFile string
|
||||||
var cfg []byte
|
var cfg []byte
|
||||||
var c HostDetails
|
var c database.HostDetails
|
||||||
var hd string
|
var hd string
|
||||||
hd = viper.GetString("hostsDirectory")
|
hd = viper.GetString("hostsDirectory")
|
||||||
|
|
||||||
@ -119,7 +120,7 @@ func configWriter(f *tview.Form){
|
|||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(fp)
|
file, err := os.Create(fp)
|
||||||
c = HostDetails{ip[3], hostFile, ip[1], ip[2]}
|
c = database.HostDetails{ip[3], hostFile, ip[1], ip[2]}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("unable to create file: " + fp)
|
log.Fatal("unable to create file: " + fp)
|
||||||
}
|
}
|
||||||
@ -141,9 +142,3 @@ func configWriter(f *tview.Form){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type HostDetails struct {
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
DatabaseName string `json:"databaseName"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
}
|
|
||||||
235
cmd/load.go
235
cmd/load.go
@ -16,12 +16,46 @@ limitations under the License.
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"shiny-pancake/database"
|
||||||
|
"shiny-pancake/internal/gui"
|
||||||
|
"shiny-pancake/logger"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadCmd represents the load command
|
type host struct {
|
||||||
|
tic int
|
||||||
|
t string
|
||||||
|
choices []string
|
||||||
|
choice int
|
||||||
|
chosen bool
|
||||||
|
Quitting bool
|
||||||
|
Frames int
|
||||||
|
File chan string
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
term = termenv.ColorProfile()
|
||||||
|
subtle = makeFgStyle("241")
|
||||||
|
dot = colorFg(" • ", "236")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// billyCmd represents the billy command
|
||||||
var loadCmd = &cobra.Command{
|
var loadCmd = &cobra.Command{
|
||||||
Use: "load",
|
Use: "load",
|
||||||
Short: "A brief description of your command",
|
Short: "A brief description of your command",
|
||||||
@ -32,10 +66,44 @@ Cobra is a CLI library for Go that empowers applications.
|
|||||||
This application is a tool to generate the needed files
|
This application is a tool to generate the needed files
|
||||||
to quickly create a Cobra application.`,
|
to quickly create a Cobra application.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("load called")
|
var title string
|
||||||
|
var pgHosts []string
|
||||||
|
var fn string
|
||||||
|
|
||||||
|
result := make(chan string, 1)
|
||||||
|
|
||||||
|
pgHosts = readHosts()
|
||||||
|
title = "choose db to load, \"l\" or enter will load your choice"
|
||||||
|
var hostChoice host
|
||||||
|
hostChoice = host{60, title, pgHosts, 0, false, false, 0, result }
|
||||||
|
if err := tea.NewProgram(hostChoice).Start(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print out the final choice.
|
||||||
|
if fn = <-result; len(fn) !=0 {
|
||||||
|
var hostDir string
|
||||||
|
hostDir = viper.GetString("hostsDirectory")
|
||||||
|
file, _ := ioutil.ReadFile(hostDir + "/"+ fn)
|
||||||
|
data := database.HostDetails{}
|
||||||
|
err := json.Unmarshal(file, &data)
|
||||||
|
if err != nil {
|
||||||
|
var l logger.Log
|
||||||
|
l = logger.Log{"error", logrus.Fields{"loadFailed": fn }, "config not loaded"}
|
||||||
|
logger.Lgr(&l)
|
||||||
|
fmt.Println("invalid config file. ")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
var l logger.Log
|
||||||
|
l = logger.Log{"info", logrus.Fields{"loaded": fn }, "config loading to ui"}
|
||||||
|
logger.Lgr(&l)
|
||||||
|
gui.Gui(data)
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(loadCmd)
|
rootCmd.AddCommand(loadCmd)
|
||||||
|
|
||||||
@ -43,9 +111,166 @@ func init() {
|
|||||||
|
|
||||||
// Cobra supports Persistent Flags which will work for this command
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
// and all subcommands, e.g.:
|
// and all subcommands, e.g.:
|
||||||
// loadCmd.PersistentFlags().String("foo", "", "A help for foo")
|
// billyCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
// Cobra supports local flags which will only run when this command
|
// Cobra supports local flags which will only run when this command
|
||||||
// is called directly, e.g.:
|
// is called directly, e.g.:
|
||||||
// loadCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
// billyCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func readHosts() []string {
|
||||||
|
var hosts []string
|
||||||
|
var hostsDir string
|
||||||
|
hostsDir = viper.GetString("hostsDirectory")
|
||||||
|
file, err := os.Open(hostsDir)
|
||||||
|
if err != nil {
|
||||||
|
var l logger.Log
|
||||||
|
l = logger.Log{"error", logrus.Fields{"filepath": hostsDir}, "unable to access directory"}
|
||||||
|
logger.Lgr(&l)
|
||||||
|
os.Exit(0)
|
||||||
|
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hosts, err = file.Readdirnames(0) // 0 to read all files and folders
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func (m host) Init() tea.Cmd {
|
||||||
|
return tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
func choicesUpdate(msg tea.Msg, m host) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "q", "esc":
|
||||||
|
m.Quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
case "j", "down":
|
||||||
|
m.choice += 1
|
||||||
|
if m.choice > len(m.choices) {
|
||||||
|
m.choice = len(m.choices)
|
||||||
|
}
|
||||||
|
case "k", "up":
|
||||||
|
m.choice -= 1
|
||||||
|
if m.choice < 0 {
|
||||||
|
m.choice = 0
|
||||||
|
}
|
||||||
|
case "l", "enter":
|
||||||
|
m.File <- m.choices[m.choice]
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
case tickMsg:
|
||||||
|
if m.tic == 0 {
|
||||||
|
m.Quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
m.tic -= 1
|
||||||
|
return m, tick()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update loop for the second view after a choice has been made
|
||||||
|
func (m host) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||||
|
k := msg.String()
|
||||||
|
if k == "q" || k == "esc" || k == "ctrl+c" {
|
||||||
|
m.Quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.chosen {
|
||||||
|
return choicesUpdate(msg, m)
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Views return a string based on data in the host. That string which will be
|
||||||
|
// rendered to the terminal.
|
||||||
|
func (m host) View() string {
|
||||||
|
var s string
|
||||||
|
if m.Quitting {
|
||||||
|
return "leaving program..."
|
||||||
|
|
||||||
|
}
|
||||||
|
if !m.chosen {
|
||||||
|
s = choicesView(m)
|
||||||
|
} else {
|
||||||
|
m.Quitting = true
|
||||||
|
return "leaving program..."
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Creates the menu for choosing a file from your configs.
|
||||||
|
// Exits after 60 seconds.
|
||||||
|
// Gets Title from outside in the load method if this... should ever be needed elsewhere.
|
||||||
|
func choicesView(m host) string {
|
||||||
|
var menu string
|
||||||
|
c := m.choice
|
||||||
|
|
||||||
|
tpl := m.t + "\n\n"
|
||||||
|
tpl += "%s\n\n"
|
||||||
|
tpl += "menu exits in %s seconds\n\n"
|
||||||
|
tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit")
|
||||||
|
for i, s := range m.choices {
|
||||||
|
menu = menu + "::" + checkbox(s, c == i)
|
||||||
|
}
|
||||||
|
mn := strings.Replace(menu, "::", "\n", -1)
|
||||||
|
choices := fmt.Sprintf(
|
||||||
|
mn,
|
||||||
|
)
|
||||||
|
|
||||||
|
return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.tic), "79"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// the rest of these functions are to do with looks and aesthetics
|
||||||
|
|
||||||
|
// returns a neat checkbox
|
||||||
|
func checkbox(label string, checked bool) string {
|
||||||
|
if checked {
|
||||||
|
return colorFg("[x] "+label, "212")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[ ] %s", label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun colors
|
||||||
|
func colorFg(val, color string) string {
|
||||||
|
return termenv.String(val).Foreground(term.Color(color)).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a function that will colorize the foreground of a given string.
|
||||||
|
func makeFgStyle(color string) func(string) string {
|
||||||
|
return termenv.Style{}.Foreground(term.Color(color)).Styled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame event
|
||||||
|
func Frame() tea.Cmd {
|
||||||
|
return tea.Tick(time.Second/60, func(time.Time) tea.Msg {
|
||||||
|
return FrameMsg{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type tickMsg struct{}
|
||||||
|
type FrameMsg struct{}
|
||||||
|
|
||||||
|
// exits if no choice is selected in a minute
|
||||||
|
func tick() tea.Cmd {
|
||||||
|
return tea.Tick(time.Second, func(time.Time) tea.Msg {
|
||||||
|
return tickMsg{}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
68
database/database.go
Normal file
68
database/database.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
//func DbConn(c HostDetails) (*sql.DB, error) {
|
||||||
|
// var h string
|
||||||
|
// var u string
|
||||||
|
// var pw string
|
||||||
|
// var dbn string
|
||||||
|
// var psqlInfo string
|
||||||
|
// h = c.Hostname
|
||||||
|
// u = c.Username
|
||||||
|
// pw = c.Secret
|
||||||
|
// dbn = c.DatabaseName
|
||||||
|
//
|
||||||
|
// logger.Logger("[LOG] connection details: host = " + h + " username = " + u)
|
||||||
|
//
|
||||||
|
// found, testUserName := data.ViperPgmConfig("test_user", "r")
|
||||||
|
// if found == true {
|
||||||
|
// logger.Logger("[LOG] found test user in .pgm.yaml")
|
||||||
|
// }
|
||||||
|
// psqlInfo = fmt.Sprintf("host=%s port=%d user=%s "+
|
||||||
|
// "password=%s dbname=%s sslmode=disable",
|
||||||
|
// h, 5432, u, pw, dbn)
|
||||||
|
// db, err := sql.Open("postgres", psqlInfo)
|
||||||
|
// if err != nil {
|
||||||
|
// var db *sql.DB
|
||||||
|
// switch u {
|
||||||
|
// case testUserName:
|
||||||
|
// logger.Logger("[ERROR] go tests expects to connect with the docker compose database." +
|
||||||
|
// "[ERROR] run \"docker-compose up\" and re-run tests")
|
||||||
|
// err := errors.New("unable to ping db" + dbn + "local db is closed, please run docker-compose up")
|
||||||
|
// return db, err
|
||||||
|
// default:
|
||||||
|
// logger.Logger("[ERROR] db error: " + err.Error())
|
||||||
|
// return db, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// logger.Logger("[LOG] pinging db " + dbn)
|
||||||
|
// ctx := context.Background()
|
||||||
|
// err = db.PingContext(ctx)
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// var db *sql.DB
|
||||||
|
// switch u {
|
||||||
|
// case testUserName:
|
||||||
|
// logger.Logger("[ERROR] db error, unable to ping: " + err.Error()+
|
||||||
|
// "\n[ERROR] ensure docker database is running and re-run tests")
|
||||||
|
// fmt.Println("local db is closed, please run docker-compose up")
|
||||||
|
// err1 := errors.New("unable to ping db" + dbn)
|
||||||
|
// return db, err1
|
||||||
|
// default:
|
||||||
|
// logger.Logger("[ERROR] db error: " + err.Error())
|
||||||
|
// return db, err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return db, nil
|
||||||
|
//}
|
||||||
|
|
||||||
|
type HostDetails struct {
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
DatabaseName string `json:"databaseName"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
0
docker-compose.yml
Normal file
0
docker-compose.yml
Normal file
3
go.mod
3
go.mod
@ -3,8 +3,11 @@ module shiny-pancake
|
|||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/charmbracelet/bubbletea v0.12.2
|
||||||
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591
|
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591
|
||||||
|
github.com/lib/pq v1.8.0
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
github.com/muesli/termenv v0.7.4
|
||||||
github.com/rivo/tview v0.0.0-20201018122409-d551c850a743
|
github.com/rivo/tview v0.0.0-20201018122409-d551c850a743
|
||||||
github.com/sirupsen/logrus v1.7.0
|
github.com/sirupsen/logrus v1.7.0
|
||||||
github.com/spf13/cobra v1.1.1
|
github.com/spf13/cobra v1.1.1
|
||||||
|
|||||||
17
go.sum
17
go.sum
@ -24,7 +24,11 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
|||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/charmbracelet/bubbletea v0.12.2 h1:y9Yo2Pv8tcm3mAJsWONGsmHhzrbNxJVxpVtemikxE9A=
|
||||||
|
github.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
|
||||||
|
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
@ -61,6 +65,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
|||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
@ -105,11 +110,14 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
|||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
@ -125,12 +133,16 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
|
|||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
|
||||||
|
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=
|
||||||
|
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
@ -186,6 +198,8 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
|
||||||
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -243,6 +257,9 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 h1:XtNJkfEjb4zR3q20BBBcYUykVOEMgZeIUOpBPfNYgxg=
|
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 h1:XtNJkfEjb4zR3q20BBBcYUykVOEMgZeIUOpBPfNYgxg=
|
||||||
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
367
internal/gui/data.go
Normal file
367
internal/gui/data.go
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"reflect"
|
||||||
|
"shiny-pancake/database"
|
||||||
|
"shiny-pancake/logger"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
batchSize = 80 // The number of rows loaded per batch.
|
||||||
|
finderPage = "*finder*" // The name of the Finder page.
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
app *tview.Application // The tview application.
|
||||||
|
pages *tview.Pages // The application pages.
|
||||||
|
finderFocus tview.Primitive // The primitive in the Finder that last had focus.
|
||||||
|
)
|
||||||
|
|
||||||
|
// Main entry point.
|
||||||
|
func data(host database.HostDetails) {
|
||||||
|
var h string
|
||||||
|
var u string
|
||||||
|
var pw string
|
||||||
|
var dbn string
|
||||||
|
var psqlInfo string
|
||||||
|
h = host.Hostname
|
||||||
|
u = host.Username
|
||||||
|
pw = host.Secret
|
||||||
|
dbn = host.DatabaseName
|
||||||
|
// Start the application.
|
||||||
|
psqlInfo = fmt.Sprintf("host=%s port=%d user=%s "+
|
||||||
|
"password=%s dbname=%s sslmode=disable",
|
||||||
|
h, 5432, u, pw, dbn)
|
||||||
|
app = tview.NewApplication()
|
||||||
|
finder(psqlInfo)
|
||||||
|
if err := app.Run(); err != nil {
|
||||||
|
fmt.Printf("Error running application: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up a "Finder" used to navigate the databases, tables, and columns.
|
||||||
|
func finder(connString string) {
|
||||||
|
// Create the basic objects.
|
||||||
|
databases := tview.NewList().ShowSecondaryText(false)
|
||||||
|
databases.SetBorder(true).SetTitle("Databases")
|
||||||
|
columns := tview.NewTable().SetBorders(true)
|
||||||
|
columns.SetBorder(true).SetTitle("Columns")
|
||||||
|
tables := tview.NewList()
|
||||||
|
tables.ShowSecondaryText(false).
|
||||||
|
SetDoneFunc(func() {
|
||||||
|
tables.Clear()
|
||||||
|
columns.Clear()
|
||||||
|
app.SetFocus(databases)
|
||||||
|
})
|
||||||
|
tables.SetBorder(true).SetTitle("Tables")
|
||||||
|
|
||||||
|
// Create the layout.
|
||||||
|
flex := tview.NewFlex().
|
||||||
|
AddItem(databases, 0, 1, true).
|
||||||
|
AddItem(tables, 0, 1, false).
|
||||||
|
AddItem(columns, 0, 3, false)
|
||||||
|
|
||||||
|
// We keep one connection pool per database.
|
||||||
|
dbMutex := sync.Mutex{}
|
||||||
|
dbs := make(map[string]*sql.DB)
|
||||||
|
getDB := func(database string) *sql.DB {
|
||||||
|
// Connect to a new database.
|
||||||
|
dbMutex.Lock()
|
||||||
|
defer dbMutex.Unlock()
|
||||||
|
if db, ok := dbs[database]; ok {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
db, err := sql.Open("postgres", connString)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dbs[database] = db
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of all databases.
|
||||||
|
generalDB, err := sql.Open("postgres", connString)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer generalDB.Close() // We really close the DB because we only use it for this one query.
|
||||||
|
rows, err := generalDB.Query("select datname from pg_database where datistemplate = false")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var dbName string
|
||||||
|
if err := rows.Scan(&dbName); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
databases.AddItem(dbName, "", 0, func() {
|
||||||
|
// A database was selected. Show all of its tables.
|
||||||
|
columns.Clear()
|
||||||
|
tables.Clear()
|
||||||
|
db := getDB(dbName)
|
||||||
|
t, err := db.Query("select table_name from information_schema.tables where table_schema not in ('information_schema','pg_catalog')")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer t.Close()
|
||||||
|
for t.Next() {
|
||||||
|
var tableName string
|
||||||
|
if err := t.Scan(&tableName); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
tables.AddItem(tableName, "", 0, nil)
|
||||||
|
}
|
||||||
|
if err := t.Err(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
app.SetFocus(tables)
|
||||||
|
|
||||||
|
// When the user navigates to a table, show its columns.
|
||||||
|
tables.SetChangedFunc(func(i int, tableName string, t string, s rune) {
|
||||||
|
// A table was selected. Show its columns.
|
||||||
|
columns.Clear()
|
||||||
|
c, err := db.Query(`
|
||||||
|
select
|
||||||
|
c.column_name,
|
||||||
|
c.is_nullable,
|
||||||
|
c.data_type,
|
||||||
|
c.character_maximum_length,
|
||||||
|
c.numeric_precision,
|
||||||
|
c.numeric_scale,
|
||||||
|
c.ordinal_position,
|
||||||
|
tc.constraint_type pkey
|
||||||
|
from
|
||||||
|
information_schema.columns c
|
||||||
|
left join
|
||||||
|
information_schema.constraint_column_usage as ccu
|
||||||
|
on
|
||||||
|
c.table_schema = ccu.table_schema
|
||||||
|
and c.table_name = ccu.table_name
|
||||||
|
and c.column_name = ccu.column_name
|
||||||
|
left join
|
||||||
|
information_schema.table_constraints as tc
|
||||||
|
on
|
||||||
|
ccu.constraint_schema = tc.constraint_schema
|
||||||
|
and ccu.constraint_name = tc.constraint_name
|
||||||
|
where
|
||||||
|
c.table_schema not in ('information_schema','pg_catalog')
|
||||||
|
and c.table_name = $1
|
||||||
|
`, tableName)
|
||||||
|
if err != nil {
|
||||||
|
var l logger.Log
|
||||||
|
l = logger.Log{"error", logrus.Fields{"database": err.Error() }, "an error occured in view."}
|
||||||
|
logger.Lgr(&l)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
columns.SetCell(0, 0, &tview.TableCell{Text: "Name", Align: tview.AlignCenter, Color: tcell.ColorYellow}).
|
||||||
|
SetCell(0, 1, &tview.TableCell{Text: "Type", Align: tview.AlignCenter, Color: tcell.ColorYellow}).
|
||||||
|
SetCell(0, 2, &tview.TableCell{Text: "Size", Align: tview.AlignCenter, Color: tcell.ColorYellow}).
|
||||||
|
SetCell(0, 3, &tview.TableCell{Text: "Null", Align: tview.AlignCenter, Color: tcell.ColorYellow}).
|
||||||
|
SetCell(0, 4, &tview.TableCell{Text: "Constraint", Align: tview.AlignCenter, Color: tcell.ColorYellow})
|
||||||
|
for c.Next() {
|
||||||
|
var (
|
||||||
|
columnName, isNullable, dataType string
|
||||||
|
constraintType sql.NullString
|
||||||
|
size, numericPrecision, numericScale sql.NullInt64
|
||||||
|
ordinalPosition int
|
||||||
|
)
|
||||||
|
if err := c.Scan(&columnName,
|
||||||
|
&isNullable,
|
||||||
|
&dataType,
|
||||||
|
&size,
|
||||||
|
&numericPrecision,
|
||||||
|
&numericScale,
|
||||||
|
&ordinalPosition,
|
||||||
|
&constraintType,
|
||||||
|
); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
sizeText := ""
|
||||||
|
if size.Valid {
|
||||||
|
sizeText = strconv.Itoa(int(size.Int64))
|
||||||
|
} else if numericPrecision.Valid {
|
||||||
|
sizeText = strconv.Itoa(int(numericPrecision.Int64))
|
||||||
|
if numericScale.Valid {
|
||||||
|
sizeText += "," + strconv.Itoa(int(numericScale.Int64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color := tcell.ColorWhite
|
||||||
|
if constraintType.Valid {
|
||||||
|
color = map[string]tcell.Color{
|
||||||
|
"CHECK": tcell.ColorGreen,
|
||||||
|
"FOREIGN KEY": tcell.ColorDarkMagenta,
|
||||||
|
"PRIMARY KEY": tcell.ColorRed,
|
||||||
|
"UNIQUE": tcell.ColorDarkCyan,
|
||||||
|
}[constraintType.String]
|
||||||
|
}
|
||||||
|
columns.SetCell(ordinalPosition, 0, &tview.TableCell{Text: columnName, Color: color}).
|
||||||
|
SetCell(ordinalPosition, 1, &tview.TableCell{Text: dataType, Color: color}).
|
||||||
|
SetCell(ordinalPosition, 2, &tview.TableCell{Text: sizeText, Align: tview.AlignRight, Color: color}).
|
||||||
|
SetCell(ordinalPosition, 3, &tview.TableCell{Text: isNullable, Align: tview.AlignRight, Color: color}).
|
||||||
|
SetCell(ordinalPosition, 4, &tview.TableCell{Text: constraintType.String, Align: tview.AlignLeft, Color: color})
|
||||||
|
}
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tables.SetCurrentItem(0) // Trigger the initial selection.
|
||||||
|
|
||||||
|
// When the user selects a table, show its content.
|
||||||
|
tables.SetSelectedFunc(func(i int, tableName string, t string, s rune) {
|
||||||
|
content(db, dbName, tableName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the pages and show the Finder.
|
||||||
|
pages = tview.NewPages().
|
||||||
|
AddPage(finderPage, flex, true, true)
|
||||||
|
app.SetRoot(pages, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows the contents of the given table.
|
||||||
|
func content(db *sql.DB, dbName, tableName string) {
|
||||||
|
finderFocus = app.GetFocus()
|
||||||
|
|
||||||
|
// If this page already exists, just show it.
|
||||||
|
if pages.HasPage(dbName + "." + tableName) {
|
||||||
|
pages.SwitchToPage(dbName + "." + tableName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var l logger.Log
|
||||||
|
l = logger.Log{"info", logrus.Fields{"database": tableName }, "selected table to view"}
|
||||||
|
logger.Lgr(&l)
|
||||||
|
var schemaName string
|
||||||
|
err := db.QueryRow(fmt.Sprintf("select table_schema from information_schema.tables where table_schema not in ('information_schema','pg_catalog') and table_name = '%s'", tableName)).Scan(&schemaName)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We display the data in a table embedded in a frame.
|
||||||
|
table := tview.NewTable().
|
||||||
|
SetFixed(1, 0).
|
||||||
|
SetBordersColor(tcell.ColorYellow)
|
||||||
|
frame := tview.NewFrame(table).
|
||||||
|
SetBorders(0, 0, 0, 0, 0, 0)
|
||||||
|
frame.SetBorder(true).
|
||||||
|
SetTitle(fmt.Sprintf(`Contents of table "%s.%s"`,schemaName, tableName))
|
||||||
|
|
||||||
|
// How many rows does this table have?
|
||||||
|
var rowCount int
|
||||||
|
err = db.QueryRow(fmt.Sprintf("select count(*) from %s.%s", schemaName, tableName)).Scan(&rowCount)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a batch of rows.
|
||||||
|
loadRows := func(offset int) {
|
||||||
|
rows, err := db.Query(fmt.Sprintf("select * from %s.%s limit $1 offset $2", schemaName, tableName), batchSize, offset)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// The first row in the table is the list of column names.
|
||||||
|
columnNames, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for index, name := range columnNames {
|
||||||
|
table.SetCell(0, index, &tview.TableCell{Text: name, Align: tview.AlignCenter, Color: tcell.ColorYellow})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the rows.
|
||||||
|
columns := make([]interface{}, len(columnNames))
|
||||||
|
columnPointers := make([]interface{}, len(columns))
|
||||||
|
for index := range columnPointers {
|
||||||
|
columnPointers[index] = &columns[index]
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
// Read the columns.
|
||||||
|
err := rows.Scan(columnPointers...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer them to the table.
|
||||||
|
row := table.GetRowCount()
|
||||||
|
for index, column := range columns {
|
||||||
|
switch value := column.(type) {
|
||||||
|
case int64:
|
||||||
|
table.SetCell(row, index, &tview.TableCell{Text: strconv.Itoa(int(value)), Align: tview.AlignRight, Color: tcell.ColorDarkCyan})
|
||||||
|
case float64:
|
||||||
|
table.SetCell(row, index, &tview.TableCell{Text: strconv.FormatFloat(value, 'f', 2, 64), Align: tview.AlignRight, Color: tcell.ColorDarkCyan})
|
||||||
|
case string:
|
||||||
|
table.SetCellSimple(row, index, value)
|
||||||
|
case time.Time:
|
||||||
|
t := value.Format("2006-01-02")
|
||||||
|
table.SetCell(row, index, &tview.TableCell{Text: t, Align: tview.AlignRight, Color: tcell.ColorDarkMagenta})
|
||||||
|
case []uint8:
|
||||||
|
str := make([]byte, len(value))
|
||||||
|
for index, num := range value {
|
||||||
|
str[index] = byte(num)
|
||||||
|
}
|
||||||
|
table.SetCell(row, index, &tview.TableCell{Text: string(str), Align: tview.AlignRight, Color: tcell.ColorGreen})
|
||||||
|
case nil:
|
||||||
|
table.SetCell(row, index, &tview.TableCell{Text: "NULL", Align: tview.AlignCenter, Color: tcell.ColorRed})
|
||||||
|
default:
|
||||||
|
// We've encountered a type that we don't know yet.
|
||||||
|
t := reflect.TypeOf(value)
|
||||||
|
str := "?nil?"
|
||||||
|
if t != nil {
|
||||||
|
str = "?" + t.String() + "?"
|
||||||
|
}
|
||||||
|
table.SetCellSimple(row, index, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show how much we've loaded.
|
||||||
|
frame.Clear()
|
||||||
|
loadMore := ""
|
||||||
|
if table.GetRowCount()-1 < rowCount {
|
||||||
|
loadMore = " - press Enter to load more"
|
||||||
|
}
|
||||||
|
loadMore = fmt.Sprintf("Loaded %d of %d rows%s. esc to return to previous menu", table.GetRowCount()-1, rowCount, loadMore)
|
||||||
|
frame.AddText(loadMore, false, tview.AlignCenter, tcell.ColorYellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the first batch of rows.
|
||||||
|
loadRows(0)
|
||||||
|
|
||||||
|
// Handle key presses.
|
||||||
|
table.SetDoneFunc(func(key tcell.Key) {
|
||||||
|
switch key {
|
||||||
|
case tcell.KeyEscape:
|
||||||
|
// Go back to Finder.
|
||||||
|
pages.SwitchToPage(finderPage)
|
||||||
|
if finderFocus != nil {
|
||||||
|
app.SetFocus(finderFocus)
|
||||||
|
}
|
||||||
|
case tcell.KeyEnter:
|
||||||
|
// Load the next batch of rows.
|
||||||
|
loadRows(table.GetRowCount() - 1)
|
||||||
|
table.ScrollToEnd()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a new page and show it.
|
||||||
|
pages.AddPage(dbName+"."+tableName, frame, true, true)
|
||||||
|
}
|
||||||
30
internal/gui/gui.go
Normal file
30
internal/gui/gui.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import "github.com/rivo/tview"
|
||||||
|
|
||||||
|
type panels struct {
|
||||||
|
currentPanel int
|
||||||
|
panel []panel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
panels panels
|
||||||
|
tabBar *tview.TextView
|
||||||
|
resources resources
|
||||||
|
stopChans map[string]chan int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newState() *state {
|
||||||
|
return &state{
|
||||||
|
stopChans: make(map[string]chan int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Gui struct {
|
||||||
|
app *tview.Application
|
||||||
|
pages *tview.Pages
|
||||||
|
state *state
|
||||||
|
db *db.Database
|
||||||
|
//statsLocations *statsLocations
|
||||||
|
}
|
||||||
1
internal/gui/state.go
Normal file
1
internal/gui/state.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package gui
|
||||||
Loading…
Reference in New Issue
Block a user