Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb34221620 | |||
| cc3ba4d9f0 | |||
| f79d17afed | |||
| d7c2282335 | |||
| 43002a9fde | |||
| bc33d26cfd | |||
| 38d9e6238f | |||
| a653ef8ca4 | |||
| cc336366c9 | |||
| 8f105183eb | |||
| b45faa2e73 | |||
| 7e5d17100d | |||
| a817e5fa21 | |||
| 3d185033f3 | |||
| 924957d00d | |||
| d674078071 | |||
| 5992d19906 | |||
| 8cac46e9f2 | |||
| db5f6816c5 | |||
| 3a4bdbde94 | |||
| 0363bc65f4 | |||
| a47898e099 | |||
| 1487b18a3a | |||
| 04ef53720f | |||
| bc82aacd57 | |||
| 6dcc1afd6b |
36
README.md
36
README.md
@@ -10,14 +10,16 @@ This is an experimental application and you should generally prefer to use [qbol
|
|||||||
- Connect to multiple databases
|
- Connect to multiple databases
|
||||||
- Browse table/bucket content
|
- Browse table/bucket content
|
||||||
- Run custom SQL queries
|
- Run custom SQL queries
|
||||||
|
- Select text to run partial query
|
||||||
- Safe handling for non-UTF8 key and data fields
|
- Safe handling for non-UTF8 key and data fields
|
||||||
- Supported databases:
|
- Supported databases:
|
||||||
|
- Badger v4
|
||||||
- Bolt
|
- Bolt
|
||||||
- Full compatibility via the upstream [etcd-io/bbolt](https://github.com/etcd-io/bbolt) library
|
- Full compatibility via the upstream [etcd-io/bbolt](https://github.com/etcd-io/bbolt) library
|
||||||
- Recursive bucket support
|
- Recursive bucket support
|
||||||
- SQLite
|
- SQLite
|
||||||
- Uses CGo if available or modernc.org if not
|
- Uses CGo if available or modernc.org if not
|
||||||
- Badger v4
|
- Redis
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ The code in this project is licensed under the ISC license (see `LICENSE` file f
|
|||||||
|
|
||||||
This project redistributes images from the famfamfam/silk icon set under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
|
This project redistributes images from the famfamfam/silk icon set under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
|
||||||
|
|
||||||
|
This project includes trademarked logo images for each supported database type.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. `CGO_ENABLED=1 go build`
|
1. `CGO_ENABLED=1 go build`
|
||||||
@@ -34,20 +38,32 @@ This project redistributes images from the famfamfam/silk icon set under the [CC
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
2024-06-23 v0.4.0
|
||||||
|
|
||||||
|
- Redis: Add as supported database
|
||||||
|
- Badger: Allow creating in-memory databases
|
||||||
|
- App: Allow selecting partial query text to execute
|
||||||
|
- App: Allow closing database connections from context menu
|
||||||
|
- App: Allow scrolling large content on Properties pane
|
||||||
|
- App: Preload recursive navigation
|
||||||
|
- App: Automatically switch to selected database when new connection is created
|
||||||
|
- App: Add help website link
|
||||||
|
- App: Add database logo images
|
||||||
|
|
||||||
2024-06-25 v0.3.0
|
2024-06-25 v0.3.0
|
||||||
|
|
||||||
- Add support for running custom queries
|
- Badger: Add BadgerDB v4 as supported database
|
||||||
- Add BadgerDB v4 as supported database
|
- SQLite: Add support for CGo-free SQLite driver under cross-compilation
|
||||||
- Add support for CGo-free SQLite driver under cross-compilation
|
- Bolt: Update Bolt to v1.4.0-alpha.1
|
||||||
- Add status bar showing currently selected DB
|
- App: Add support for running custom queries
|
||||||
- Update Bolt to v1.4.0-alpha.1
|
- App: Add status bar showing currently selected DB
|
||||||
- Fix missing icons in nav when selecting items
|
- App: Fix missing icons in nav when selecting items
|
||||||
- Fix extra quotemarks when browsing string content of database
|
- App: Fix extra quotemarks when browsing string content of database
|
||||||
|
|
||||||
2024-06-08 v0.2.0
|
2024-06-08 v0.2.0
|
||||||
|
|
||||||
- Add SQLite support (now requires CGo)
|
- SQLite: Add SQLite support (now requires CGo)
|
||||||
- Add images for menu and navigation items
|
- App: Add images for menu and navigation items
|
||||||
|
|
||||||
2024-06-03 v0.1.0
|
2024-06-03 v0.1.0
|
||||||
|
|
||||||
|
|||||||
5
TODO
5
TODO
@@ -7,7 +7,6 @@
|
|||||||
- More DB types
|
- More DB types
|
||||||
- MySQL
|
- MySQL
|
||||||
- Postgres
|
- Postgres
|
||||||
- Redis
|
|
||||||
- MSSQL (recursive navigation for instances)
|
- MSSQL (recursive navigation for instances)
|
||||||
- SSH tunnels
|
- SSH tunnels
|
||||||
- Special actions
|
- Special actions
|
||||||
@@ -18,6 +17,8 @@
|
|||||||
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
|
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
|
||||||
- https://github.com/litements/litexplore
|
- https://github.com/litements/litexplore
|
||||||
- Badger encryption key dialog
|
- Badger encryption key dialog
|
||||||
- Badger in-memory
|
|
||||||
- Makefile to cross-compile release binaries in docker
|
- Makefile to cross-compile release binaries in docker
|
||||||
|
- Build own liblcl binaries in docker
|
||||||
- Win32 icon resource
|
- Win32 icon resource
|
||||||
|
- Faster virtual rendering
|
||||||
|
- https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go
|
||||||
BIN
assets/vendor_dgraph.png
Normal file
BIN
assets/vendor_dgraph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
BIN
assets/vendor_github.png
Normal file
BIN
assets/vendor_github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 780 B |
BIN
assets/vendor_mysql.png
Normal file
BIN
assets/vendor_mysql.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 824 B |
BIN
assets/vendor_redis.png
Normal file
BIN
assets/vendor_redis.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/vendor_sqlite.png
Normal file
BIN
assets/vendor_sqlite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 919 B |
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
type badgerLoadedDatabase struct {
|
type badgerLoadedDatabase struct {
|
||||||
displayName string
|
displayName string
|
||||||
path string
|
|
||||||
db *badger.DB
|
db *badger.DB
|
||||||
nav *vcl.TTreeNode
|
nav *vcl.TTreeNode
|
||||||
|
|
||||||
@@ -108,24 +107,41 @@ func (ld *badgerLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListVie
|
|||||||
vcl.ShowMessage("Badger doesn't support querying")
|
vcl.ShowMessage("Badger doesn't support querying")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ld *badgerLoadedDatabase) Close() {
|
||||||
|
_ = ld.db.Close()
|
||||||
|
ld.arena = nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion
|
var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
|
func (f *TMainForm) badgerAddDatabaseFromMemory() {
|
||||||
|
f.badgerAddDatabaseFrom(badger.DefaultOptions("").WithInMemory(true))
|
||||||
|
}
|
||||||
|
|
||||||
func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) {
|
func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) {
|
||||||
|
f.badgerAddDatabaseFrom(badger.DefaultOptions(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) badgerAddDatabaseFrom(opts badger.Options) {
|
||||||
// TODO load in background thread to stop blocking the UI
|
// TODO load in background thread to stop blocking the UI
|
||||||
db, err := badger.Open(badger.DefaultOptions(path))
|
db, err := badger.Open(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
|
vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ld := &badgerLoadedDatabase{
|
ld := &badgerLoadedDatabase{
|
||||||
path: path,
|
|
||||||
displayName: filepath.Base(path),
|
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Dir == "" {
|
||||||
|
ld.displayName = ":memory:" // SQLite-style naming
|
||||||
|
} else {
|
||||||
|
ld.displayName = filepath.Base(opts.Dir)
|
||||||
|
}
|
||||||
|
|
||||||
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
|
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
|
||||||
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
|
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
|
||||||
ld.nav.SetImageIndex(imgDatabase)
|
ld.nav.SetImageIndex(imgDatabase)
|
||||||
@@ -138,6 +154,7 @@ func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) {
|
|||||||
ld.nav.SetData(unsafe.Pointer(navData))
|
ld.nav.SetData(unsafe.Pointer(navData))
|
||||||
|
|
||||||
f.dbs = append(f.dbs, ld)
|
f.dbs = append(f.dbs, ld)
|
||||||
|
f.Buckets.SetSelected(ld.nav) // Select new element
|
||||||
|
|
||||||
ld.Keepalive(navData)
|
ld.Keepalive(navData)
|
||||||
}
|
}
|
||||||
@@ -98,6 +98,11 @@ func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView)
|
|||||||
vcl.ShowMessage("Bolt doesn't support querying")
|
vcl.ShowMessage("Bolt doesn't support querying")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ld *boltLoadedDatabase) Close() {
|
||||||
|
_ = ld.db.Close()
|
||||||
|
ld.arena = nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
|
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -128,6 +133,7 @@ func (f *TMainForm) boltAddDatabaseFromFile(path string) {
|
|||||||
ld.nav.SetData(unsafe.Pointer(navData))
|
ld.nav.SetData(unsafe.Pointer(navData))
|
||||||
|
|
||||||
f.dbs = append(f.dbs, ld)
|
f.dbs = append(f.dbs, ld)
|
||||||
|
f.Buckets.SetSelected(ld.nav) // Select new element
|
||||||
|
|
||||||
ld.Keepalive(navData)
|
ld.Keepalive(navData)
|
||||||
}
|
}
|
||||||
37
db_none.go
Normal file
37
db_none.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ying32/govcl/vcl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type noLoadedDatabase struct{}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) DisplayName() string {
|
||||||
|
return "yvbolt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) DriverName() string {
|
||||||
|
return "No database selected"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) RootElement() *vcl.TTreeNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
f.propertiesBox.SetText("Open a database to get started...")
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) Close() {}
|
||||||
282
db_redis.go
Normal file
282
db_redis.go
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type redisLoadedDatabase struct {
|
||||||
|
displayName string
|
||||||
|
db *redis.Client
|
||||||
|
nav *vcl.TTreeNode
|
||||||
|
|
||||||
|
currentDb int
|
||||||
|
maxDb int
|
||||||
|
serverVersion string // populated at connection-time from INFO command
|
||||||
|
|
||||||
|
arena []*navData // keepalive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) DisplayName() string {
|
||||||
|
return ld.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) DriverName() string {
|
||||||
|
return "Redis " + ld.serverVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) RootElement() *vcl.TTreeNode {
|
||||||
|
return ld.nav
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
|
ld.arena = append(ld.arena, ndata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if len(ndata.bucketPath) == 0 {
|
||||||
|
// Top-level: Show info() on main Properties tab
|
||||||
|
infostr, err := ld.db.Info(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Retreiving database info: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.propertiesBox.SetText(infostr)
|
||||||
|
|
||||||
|
// Disable data tab
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
|
} else if len(ndata.bucketPath) == 1 {
|
||||||
|
// One selected database
|
||||||
|
// Figure out its content
|
||||||
|
err := ld.db.Do(ctx, "SELECT", ndata.bucketPath[0]).Err()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Switching to database %q: %v", ndata.bucketPath[0], err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allKeys, err := ld.db.Keys(ctx, "*").Result()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Listing keys in database %q: %v", ndata.bucketPath[0], err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
|
||||||
|
|
||||||
|
// Redis always uses Key + Value as the columns
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
|
f.contentBox.Columns().Clear()
|
||||||
|
colKey := f.contentBox.Columns().Add()
|
||||||
|
colKey.SetCaption("Key")
|
||||||
|
colKey.SetWidth(MY_WIDTH)
|
||||||
|
colKey.SetAlignment(types.TaLeftJustify)
|
||||||
|
colType := f.contentBox.Columns().Add()
|
||||||
|
colType.SetCaption("Type")
|
||||||
|
colVal := f.contentBox.Columns().Add()
|
||||||
|
colVal.SetCaption("Value")
|
||||||
|
|
||||||
|
for _, key := range allKeys {
|
||||||
|
typeName, err := ld.db.Type(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataEntry := f.contentBox.Items().Add()
|
||||||
|
dataEntry.SetCaption(key) // formatUtf8
|
||||||
|
dataEntry.SubItems().Add(typeName)
|
||||||
|
|
||||||
|
switch typeName {
|
||||||
|
case "string":
|
||||||
|
val, err := ld.db.Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataEntry.SubItems().Add(val)
|
||||||
|
|
||||||
|
case "hash":
|
||||||
|
val, err := ld.db.HGetAll(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// It's a map[string]string
|
||||||
|
dataEntry.SubItems().Add(formatAny(val))
|
||||||
|
|
||||||
|
case "lists":
|
||||||
|
fallthrough
|
||||||
|
case "sets":
|
||||||
|
fallthrough
|
||||||
|
case "sorted sets":
|
||||||
|
fallthrough
|
||||||
|
case "stream":
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
default:
|
||||||
|
dataEntry.SubItems().Add("<<<other object type>>>")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid
|
||||||
|
f.contentBox.SetEnabled(true)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Unexpected nav position %q", ndata.bucketPath))
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
// ctx := context.Background()
|
||||||
|
|
||||||
|
if len(ndata.bucketPath) == 0 {
|
||||||
|
// Top-level: list of all child databases (usually 16x)
|
||||||
|
ret := make([]string, 0, ld.maxDb)
|
||||||
|
for i := 0; i < ld.maxDb; i++ {
|
||||||
|
ret = append(ret, fmt.Sprintf("%d", i))
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
|
||||||
|
} else if len(ndata.bucketPath) == 1 {
|
||||||
|
// One selected database
|
||||||
|
// No child keys underneath it
|
||||||
|
return []string{}, nil
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Need to parse the query into separate string+args fields for the protocol
|
||||||
|
// TODO This needs to better handle quotes, escaping, ...
|
||||||
|
fields := strings.Fields(query)
|
||||||
|
|
||||||
|
fields_boxed := box_interface(fields)
|
||||||
|
|
||||||
|
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("The redis query returned an error: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultArea.SetEnabled(false)
|
||||||
|
resultArea.Clear()
|
||||||
|
|
||||||
|
resultArea.Columns().Clear()
|
||||||
|
colVal := resultArea.Columns().Add()
|
||||||
|
colVal.SetCaption("Result")
|
||||||
|
|
||||||
|
// The result is probably a single value or a string slice
|
||||||
|
switch ret := ret.(type) {
|
||||||
|
case []string:
|
||||||
|
// Multiple values
|
||||||
|
for _, single := range ret {
|
||||||
|
cell := resultArea.Items().Add()
|
||||||
|
cell.SetCaption(single) // formatUtf8
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Single value
|
||||||
|
dataEntry := resultArea.Items().Add()
|
||||||
|
dataEntry.SetCaption(formatAny(ret)) // formatUtf8
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resultArea.SetEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *redisLoadedDatabase) Close() {
|
||||||
|
_ = ld.db.Close()
|
||||||
|
ld.arena = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ loadedDatabase = &redisLoadedDatabase{} // interface assertion
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
func (f *TMainForm) redisConnect(opts *redis.Options) {
|
||||||
|
// TODO load in background thread to stop blocking the UI
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
ld := &redisLoadedDatabase{
|
||||||
|
displayName: opts.Addr,
|
||||||
|
currentDb: 0,
|
||||||
|
maxDb: 1,
|
||||||
|
serverVersion: "<unknown>",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch in a hook to remember current DB after keepalive reconnection
|
||||||
|
opts.DB = 0 // Default
|
||||||
|
opts.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
|
||||||
|
return cn.Select(ctx, ld.currentDb).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient doesn't necessarily connect, so it can't throw an err
|
||||||
|
ld.db = redis.NewClient(opts)
|
||||||
|
|
||||||
|
// Make an INFO request (mandatory)
|
||||||
|
info, err := ld.db.InfoMap(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During INFO: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverInfo, ok := info["Server"]; ok {
|
||||||
|
if v, ok := serverInfo["redis_version"]; ok {
|
||||||
|
ld.serverVersion = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List available databases (usually 1..16) with "0" as default
|
||||||
|
// If this fails, probably the target redis does not support multiple databases
|
||||||
|
// (e.g. Redis Cluster). Assume max=0.
|
||||||
|
if maxDatabases, err := ld.db.ConfigGet(ctx, "databases").Result(); err == nil {
|
||||||
|
// Got a result. Must parse it
|
||||||
|
m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During CONFIG GET databases: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ld.maxDb = int(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done with setup
|
||||||
|
|
||||||
|
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
|
||||||
|
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
|
||||||
|
ld.nav.SetImageIndex(imgDatabase)
|
||||||
|
ld.nav.SetSelectedIndex(imgDatabase)
|
||||||
|
navData := &navData{
|
||||||
|
ld: ld,
|
||||||
|
childrenLoaded: false, // will be loaded dynamically
|
||||||
|
bucketPath: []string{}, // empty = root
|
||||||
|
}
|
||||||
|
ld.nav.SetData(unsafe.Pointer(navData))
|
||||||
|
|
||||||
|
f.dbs = append(f.dbs, ld)
|
||||||
|
f.Buckets.SetSelected(ld.nav) // Select new element
|
||||||
|
|
||||||
|
ld.Keepalive(navData)
|
||||||
|
}
|
||||||
@@ -230,6 +230,11 @@ func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
|||||||
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
|
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ld *sqliteLoadedDatabase) Close() {
|
||||||
|
_ = ld.db.Close()
|
||||||
|
ld.arena = nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
|
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -261,6 +266,7 @@ func (f *TMainForm) sqliteAddDatabaseFromFile(path string) {
|
|||||||
ld.nav.SetData(unsafe.Pointer(navData))
|
ld.nav.SetData(unsafe.Pointer(navData))
|
||||||
|
|
||||||
f.dbs = append(f.dbs, ld)
|
f.dbs = append(f.dbs, ld)
|
||||||
|
f.Buckets.SetSelected(ld.nav) // Select new element
|
||||||
|
|
||||||
ld.Keepalive(navData)
|
ld.Keepalive(navData)
|
||||||
}
|
}
|
||||||
3
go.mod
3
go.mod
@@ -13,6 +13,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/glog v1.0.0 // indirect
|
github.com/golang/glog v1.0.0 // indirect
|
||||||
@@ -24,7 +25,9 @@ require (
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/klauspost/compress v1.12.3 // indirect
|
github.com/klauspost/compress v1.12.3 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.5.3 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
go.opencensus.io v0.22.5 // indirect
|
go.opencensus.io v0.22.5 // indirect
|
||||||
golang.org/x/mod v0.3.0 // indirect
|
golang.org/x/mod v0.3.0 // indirect
|
||||||
|
|||||||
7
go.sum
7
go.sum
@@ -13,6 +13,8 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
|
|||||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
@@ -49,10 +51,14 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
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/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
|
||||||
|
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
@@ -102,6 +108,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
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=
|
||||||
|
|||||||
10
images.go
10
images.go
@@ -21,6 +21,11 @@ const (
|
|||||||
imgTableAdd
|
imgTableAdd
|
||||||
imgTableDelete
|
imgTableDelete
|
||||||
imgTableSave
|
imgTableSave
|
||||||
|
imgVendorDgraph
|
||||||
|
imgVendorGithub
|
||||||
|
imgVendorMySQL
|
||||||
|
imgVendorRedis
|
||||||
|
imgVendorSqlite
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadImages(owner vcl.IComponent) *vcl.TImageList {
|
func loadImages(owner vcl.IComponent) *vcl.TImageList {
|
||||||
@@ -51,6 +56,11 @@ func loadImages(owner vcl.IComponent) *vcl.TImageList {
|
|||||||
ilist.Add(mustLoad("assets/table_add.png"), nil)
|
ilist.Add(mustLoad("assets/table_add.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/table_delete.png"), nil)
|
ilist.Add(mustLoad("assets/table_delete.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/table_save.png"), nil)
|
ilist.Add(mustLoad("assets/table_save.png"), nil)
|
||||||
|
ilist.Add(mustLoad("assets/vendor_dgraph.png"), nil)
|
||||||
|
ilist.Add(mustLoad("assets/vendor_github.png"), nil)
|
||||||
|
ilist.Add(mustLoad("assets/vendor_mysql.png"), nil)
|
||||||
|
ilist.Add(mustLoad("assets/vendor_redis.png"), nil)
|
||||||
|
ilist.Add(mustLoad("assets/vendor_sqlite.png"), nil)
|
||||||
return ilist
|
return ilist
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type loadedDatabase interface {
|
|||||||
ExecQuery(query string, resultArea *vcl.TListView)
|
ExecQuery(query string, resultArea *vcl.TListView)
|
||||||
NavChildren(ndata *navData) ([]string, error)
|
NavChildren(ndata *navData) ([]string, error)
|
||||||
Keepalive(ndata *navData)
|
Keepalive(ndata *navData)
|
||||||
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
|
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
|
||||||
|
|||||||
263
main.go
263
main.go
@@ -5,15 +5,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
_ "github.com/ying32/govcl/pkgs/winappres" // Extra _syso files for Windows
|
"github.com/pkg/browser"
|
||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
"github.com/ying32/govcl/vcl/types"
|
"github.com/ying32/govcl/vcl/types"
|
||||||
)
|
"github.com/ying32/govcl/vcl/types/colors"
|
||||||
|
|
||||||
const (
|
|
||||||
MY_SPACING = 6
|
|
||||||
MY_HEIGHT = 90
|
|
||||||
MY_WIDTH = 180
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TMainForm struct {
|
type TMainForm struct {
|
||||||
@@ -29,6 +24,7 @@ type TMainForm struct {
|
|||||||
queryInput *vcl.TMemo
|
queryInput *vcl.TMemo
|
||||||
queryResult *vcl.TListView
|
queryResult *vcl.TListView
|
||||||
|
|
||||||
|
none *noLoadedDatabase
|
||||||
dbs []loadedDatabase
|
dbs []loadedDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,35 +40,76 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
|||||||
f.ImageList = loadImages(f)
|
f.ImageList = loadImages(f)
|
||||||
|
|
||||||
f.SetCaption("yvbolt")
|
f.SetCaption("yvbolt")
|
||||||
|
f.ScreenCenter()
|
||||||
f.ImageList.GetIcon(imgDatabaseLightning, f.Icon())
|
f.ImageList.GetIcon(imgDatabaseLightning, f.Icon())
|
||||||
|
|
||||||
mnuFile := vcl.NewMenuItem(f)
|
mnuFile := vcl.NewMenuItem(f)
|
||||||
mnuFile.SetCaption("File")
|
mnuFile.SetCaption("File")
|
||||||
|
|
||||||
mnuFileOpen := vcl.NewMenuItem(mnuFile)
|
mnuFileBadger := vcl.NewMenuItem(mnuFile)
|
||||||
mnuFileOpen.SetCaption("Open Bolt database...")
|
mnuFileBadger.SetCaption("Badger")
|
||||||
mnuFileOpen.SetImageIndex(imgDatabaseAdd)
|
mnuFileBadger.SetImageIndex(imgVendorDgraph)
|
||||||
mnuFileOpen.SetShortCutFromString("Ctrl+O")
|
mnuFile.Add(mnuFileBadger)
|
||||||
mnuFileOpen.SetOnClick(f.OnMnuFileOpenClick)
|
|
||||||
mnuFile.Add(mnuFileOpen)
|
|
||||||
|
|
||||||
mnuFileSqliteOpen := vcl.NewMenuItem(mnuFile)
|
mnuFileBadgerOpen := vcl.NewMenuItem(mnuFileBadger)
|
||||||
mnuFileSqliteOpen.SetCaption("Open SQLite database...")
|
mnuFileBadgerOpen.SetCaption("Open database...")
|
||||||
mnuFileSqliteOpen.SetImageIndex(imgDatabaseAdd)
|
|
||||||
mnuFileSqliteOpen.SetOnClick(f.OnMnuFileSqliteOpenClick)
|
|
||||||
mnuFile.Add(mnuFileSqliteOpen)
|
|
||||||
|
|
||||||
mnuFileBadgerOpen := vcl.NewMenuItem(mnuFile)
|
|
||||||
mnuFileBadgerOpen.SetCaption("Open Badger v4 database...")
|
|
||||||
mnuFileBadgerOpen.SetImageIndex(imgDatabaseAdd)
|
mnuFileBadgerOpen.SetImageIndex(imgDatabaseAdd)
|
||||||
mnuFileBadgerOpen.SetOnClick(f.OnMnuFileBadgerOpenClick)
|
mnuFileBadgerOpen.SetOnClick(f.OnMnuFileBadgerOpenClick)
|
||||||
mnuFile.Add(mnuFileBadgerOpen)
|
mnuFileBadger.Add(mnuFileBadgerOpen)
|
||||||
|
|
||||||
mnuFileSqliteMemory := vcl.NewMenuItem(mnuFile)
|
mnuFileBadgerMemory := vcl.NewMenuItem(mnuFileBadger)
|
||||||
mnuFileSqliteMemory.SetCaption("New SQLite in-memory database")
|
mnuFileBadgerMemory.SetCaption("New in-memory database")
|
||||||
|
mnuFileBadgerMemory.SetImageIndex(imgDatabaseAdd)
|
||||||
|
mnuFileBadgerMemory.SetOnClick(f.OnMnuFileBadgerMemoryClick)
|
||||||
|
mnuFileBadger.Add(mnuFileBadgerMemory)
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
mnuFileBolt := vcl.NewMenuItem(mnuFile)
|
||||||
|
mnuFileBolt.SetCaption("Bolt")
|
||||||
|
mnuFileBolt.SetImageIndex(imgVendorGithub)
|
||||||
|
mnuFile.Add(mnuFileBolt)
|
||||||
|
|
||||||
|
mnuFileBoltOpen := vcl.NewMenuItem(mnuFileBolt)
|
||||||
|
mnuFileBoltOpen.SetCaption("Open database...")
|
||||||
|
mnuFileBoltOpen.SetImageIndex(imgDatabaseAdd)
|
||||||
|
mnuFileBoltOpen.SetShortCutFromString("Ctrl+O")
|
||||||
|
mnuFileBoltOpen.SetOnClick(f.OnMnuFileOpenClick)
|
||||||
|
mnuFileBolt.Add(mnuFileBoltOpen)
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
mnuFileRedis := vcl.NewMenuItem(mnuFile)
|
||||||
|
mnuFileRedis.SetCaption("Redis")
|
||||||
|
mnuFileRedis.SetImageIndex(imgVendorRedis)
|
||||||
|
mnuFile.Add(mnuFileRedis)
|
||||||
|
|
||||||
|
mnuFileRedisConnect := vcl.NewMenuItem(mnuFileRedis)
|
||||||
|
mnuFileRedisConnect.SetCaption("Connect...")
|
||||||
|
mnuFileRedisConnect.SetImageIndex(imgDatabaseAdd)
|
||||||
|
mnuFileRedisConnect.SetOnClick(f.OnMnuFileRedisConnectClick)
|
||||||
|
mnuFileRedis.Add(mnuFileRedisConnect)
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
mnuFileSqlite := vcl.NewMenuItem(mnuFile)
|
||||||
|
mnuFileSqlite.SetCaption("SQLite")
|
||||||
|
mnuFileSqlite.SetImageIndex(imgVendorSqlite)
|
||||||
|
mnuFile.Add(mnuFileSqlite)
|
||||||
|
|
||||||
|
mnuFileSqliteOpen := vcl.NewMenuItem(mnuFileSqlite)
|
||||||
|
mnuFileSqliteOpen.SetCaption("Open database...")
|
||||||
|
mnuFileSqliteOpen.SetImageIndex(imgDatabaseAdd)
|
||||||
|
mnuFileSqliteOpen.SetOnClick(f.OnMnuFileSqliteOpenClick)
|
||||||
|
mnuFileSqlite.Add(mnuFileSqliteOpen)
|
||||||
|
|
||||||
|
mnuFileSqliteMemory := vcl.NewMenuItem(mnuFileSqlite)
|
||||||
|
mnuFileSqliteMemory.SetCaption("New in-memory database")
|
||||||
mnuFileSqliteMemory.SetImageIndex(imgDatabaseAdd)
|
mnuFileSqliteMemory.SetImageIndex(imgDatabaseAdd)
|
||||||
mnuFileSqliteMemory.SetOnClick(f.OnMnuFileSqliteMemoryClick)
|
mnuFileSqliteMemory.SetOnClick(f.OnMnuFileSqliteMemoryClick)
|
||||||
mnuFile.Add(mnuFileSqliteMemory)
|
mnuFileSqlite.Add(mnuFileSqliteMemory)
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
mnuSep := vcl.NewMenuItem(mnuFile)
|
mnuSep := vcl.NewMenuItem(mnuFile)
|
||||||
mnuSep.SetCaption("-") // Creates separator
|
mnuSep.SetCaption("-") // Creates separator
|
||||||
@@ -93,10 +130,20 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
|||||||
mnuQueryExecute.SetImageIndex(imgLightning)
|
mnuQueryExecute.SetImageIndex(imgLightning)
|
||||||
mnuQuery.Add(mnuQueryExecute)
|
mnuQuery.Add(mnuQueryExecute)
|
||||||
|
|
||||||
|
mnuHelp := vcl.NewMenuItem(f)
|
||||||
|
mnuHelp.SetCaption("Help")
|
||||||
|
|
||||||
|
mnuHelpHomepage := vcl.NewMenuItem(mnuHelp)
|
||||||
|
mnuHelpHomepage.SetCaption("About yvbolt")
|
||||||
|
mnuHelpHomepage.SetShortCutFromString("F1")
|
||||||
|
mnuHelpHomepage.SetOnClick(f.OnMnuHelpHomepage)
|
||||||
|
mnuHelp.Add(mnuHelpHomepage)
|
||||||
|
|
||||||
f.Menu = vcl.NewMainMenu(f)
|
f.Menu = vcl.NewMainMenu(f)
|
||||||
f.Menu.SetImages(f.ImageList)
|
f.Menu.SetImages(f.ImageList)
|
||||||
f.Menu.Items().Add(mnuFile)
|
f.Menu.Items().Add(mnuFile)
|
||||||
f.Menu.Items().Add(mnuQuery)
|
f.Menu.Items().Add(mnuQuery)
|
||||||
|
f.Menu.Items().Add(mnuHelp)
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
@@ -114,6 +161,7 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
|||||||
f.Buckets.SetReadOnly(true) // prevent click to rename on nodes
|
f.Buckets.SetReadOnly(true) // prevent click to rename on nodes
|
||||||
f.Buckets.SetOnExpanding(f.OnNavExpanding)
|
f.Buckets.SetOnExpanding(f.OnNavExpanding)
|
||||||
f.Buckets.SetOnChange(f.OnNavChange)
|
f.Buckets.SetOnChange(f.OnNavChange)
|
||||||
|
f.Buckets.SetOnContextPopup(f.OnNavContextPopup)
|
||||||
|
|
||||||
hsplit := vcl.NewSplitter(f)
|
hsplit := vcl.NewSplitter(f)
|
||||||
hsplit.SetParent(f)
|
hsplit.SetParent(f)
|
||||||
@@ -135,9 +183,11 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
|||||||
f.propertiesBox.BorderSpacing().SetAround(MY_SPACING)
|
f.propertiesBox.BorderSpacing().SetAround(MY_SPACING)
|
||||||
f.propertiesBox.SetAlign(types.AlClient) // fill remaining space
|
f.propertiesBox.SetAlign(types.AlClient) // fill remaining space
|
||||||
f.propertiesBox.SetReadOnly(true)
|
f.propertiesBox.SetReadOnly(true)
|
||||||
f.propertiesBox.SetEnabled(false)
|
f.propertiesBox.SetEnabled(true) // Need to leave it enabled so scrolling works
|
||||||
|
f.propertiesBox.SetColor(colors.ClForm) // 0x00f0f0f0
|
||||||
|
|
||||||
f.propertiesBox.SetBorderStyle(types.BsNone)
|
f.propertiesBox.SetBorderStyle(types.BsNone)
|
||||||
f.propertiesBox.SetText("Open a database to get started...")
|
f.propertiesBox.SetScrollBars(types.SsAutoVertical)
|
||||||
|
|
||||||
dataTab := vcl.NewTabSheet(f.Tabs)
|
dataTab := vcl.NewTabSheet(f.Tabs)
|
||||||
dataTab.SetParent(f.Tabs)
|
dataTab.SetParent(f.Tabs)
|
||||||
@@ -199,6 +249,9 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
|||||||
f.queryResult.BorderSpacing().SetLeft(MY_SPACING)
|
f.queryResult.BorderSpacing().SetLeft(MY_SPACING)
|
||||||
f.queryResult.BorderSpacing().SetRight(MY_SPACING)
|
f.queryResult.BorderSpacing().SetRight(MY_SPACING)
|
||||||
f.queryResult.BorderSpacing().SetBottom(MY_SPACING)
|
f.queryResult.BorderSpacing().SetBottom(MY_SPACING)
|
||||||
|
|
||||||
|
f.none = &noLoadedDatabase{}
|
||||||
|
f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnMnuFileOpenClick(sender vcl.IObject) {
|
func (f *TMainForm) OnMnuFileOpenClick(sender vcl.IObject) {
|
||||||
@@ -230,14 +283,101 @@ func (f *TMainForm) OnMnuFileBadgerOpenClick(sender vcl.IObject) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) OnMnuFileBadgerMemoryClick(sender vcl.IObject) {
|
||||||
|
f.badgerAddDatabaseFromMemory()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) {
|
func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) {
|
||||||
f.sqliteAddDatabaseFromFile(`:memory:`)
|
f.sqliteAddDatabaseFromFile(`:memory:`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) OnMnuFileRedisConnectClick(sender vcl.IObject) {
|
||||||
|
|
||||||
|
var child *TRedisConnectionDialog
|
||||||
|
vcl.Application.CreateForm(&child)
|
||||||
|
defer child.Free()
|
||||||
|
|
||||||
|
var ret TRedisConnectionDialogResult
|
||||||
|
child.CustomReturn = &ret
|
||||||
|
|
||||||
|
mr := child.ShowModal() // Fake blocking
|
||||||
|
if mr != types.MrOk || ret == nil {
|
||||||
|
return // Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
f.redisConnect(ret)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnMnuFileExitClick(sender vcl.IObject) {
|
func (f *TMainForm) OnMnuFileExitClick(sender vcl.IObject) {
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) OnMnuHelpHomepage(sender vcl.IObject) {
|
||||||
|
err := browser.OpenURL("https://code.ivysaur.me/yvbolt")
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage("Opening browser: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) OnNavContextPopup(sender vcl.IObject, mousePos types.TPoint, handled *bool) {
|
||||||
|
*handled = true
|
||||||
|
|
||||||
|
curItem := f.Buckets.Selected()
|
||||||
|
if curItem == nil {
|
||||||
|
// Nothing is selected at all
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mnu := vcl.NewPopupMenu(f.Buckets)
|
||||||
|
|
||||||
|
mnuRefresh := vcl.NewMenuItem(mnu)
|
||||||
|
mnuRefresh.SetCaption("Refresh")
|
||||||
|
mnuRefresh.SetOnClick(f.OnNavContextRefresh)
|
||||||
|
mnu.Items().Add(mnuRefresh)
|
||||||
|
|
||||||
|
// Check what custom actions the ndata->db itself wants to add
|
||||||
|
// ...
|
||||||
|
|
||||||
|
if curItem.Parent() == nil {
|
||||||
|
// Top-level item (database connection). Allow closing by right-click.
|
||||||
|
mnuSep := vcl.NewMenuItem(mnu)
|
||||||
|
mnuSep.SetCaption("-")
|
||||||
|
mnu.Items().Add(mnuSep)
|
||||||
|
|
||||||
|
mnuClose := vcl.NewMenuItem(mnu)
|
||||||
|
mnuClose.SetCaption("Close")
|
||||||
|
mnuClose.SetOnClick(f.OnNavContextClose)
|
||||||
|
mnu.Items().Add(mnuClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show popup
|
||||||
|
mnu.Popup2()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) OnNavContextRefresh(sender vcl.IObject) {
|
||||||
|
vcl.ShowMessage("TODO")
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) OnNavContextClose(sender vcl.IObject) {
|
||||||
|
curItem := f.Buckets.Selected()
|
||||||
|
if curItem == nil {
|
||||||
|
return // Nothing selected (shouldn't happen)
|
||||||
|
}
|
||||||
|
if curItem.Parent() != nil {
|
||||||
|
return // Selection is not top-level DB connection (shouldn't happen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ndata := (*navData)(curItem.Data())
|
||||||
|
|
||||||
|
ndata.ld.Close()
|
||||||
|
curItem.Delete()
|
||||||
|
|
||||||
|
// n.b. This triggers OnNavChange, which will then re-render from noLoadedDatabase{}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
||||||
// If query tab is not selected, switch to it, but do not exec
|
// If query tab is not selected, switch to it, but do not exec
|
||||||
if f.Tabs.ActivePageIndex() != 2 {
|
if f.Tabs.ActivePageIndex() != 2 {
|
||||||
@@ -245,6 +385,11 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryString := f.queryInput.Text()
|
||||||
|
if f.queryInput.SelLength() > 0 {
|
||||||
|
queryString = f.queryInput.SelText() // Just the selected text
|
||||||
|
}
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
node := f.Buckets.Selected()
|
node := f.Buckets.Selected()
|
||||||
if node == nil {
|
if node == nil {
|
||||||
@@ -253,22 +398,23 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
ndata := (*navData)(node.Data())
|
||||||
ndata.ld.ExecQuery(f.queryInput.Text(), f.queryResult)
|
ndata.ld.ExecQuery(queryString, f.queryResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
|
func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
|
||||||
|
|
||||||
if node.Data() == nil {
|
var ld loadedDatabase = f.none
|
||||||
vcl.ShowMessage("unexpected nil data")
|
var ndata *navData = nil
|
||||||
return
|
|
||||||
|
if node != nil && node.Data() != nil {
|
||||||
|
ndata = (*navData)(node.Data())
|
||||||
|
ld = ndata.ld
|
||||||
}
|
}
|
||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
|
||||||
|
|
||||||
ndata.ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
|
|
||||||
|
|
||||||
// We're in charge of common status bar text updates
|
// We're in charge of common status bar text updates
|
||||||
f.StatusBar.SetSimpleText(ndata.ld.DisplayName() + " | " + ndata.ld.DriverName())
|
f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) {
|
func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) {
|
||||||
@@ -281,25 +427,58 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
|
|||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
ndata := (*navData)(node.Data())
|
||||||
|
|
||||||
if ndata.childrenLoaded {
|
err := f.NavLoadChildren(node, ndata)
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(err.Error())
|
||||||
|
*allowExpansion = false // Permanently block
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*allowExpansion = node.HasChildren()
|
||||||
|
|
||||||
|
// While we're here - preload one single level deep (not any deeper)
|
||||||
|
|
||||||
|
if node.HasChildren() {
|
||||||
|
|
||||||
|
// This node has children that we haven't processed. Process them now
|
||||||
|
cc := node.GetFirstChild()
|
||||||
|
|
||||||
|
for {
|
||||||
|
ndataChild := (*navData)(cc.Data())
|
||||||
|
if ndataChild.childrenLoaded {
|
||||||
|
break // We always do them together, so if one's done, no need to keep looking
|
||||||
|
}
|
||||||
|
|
||||||
|
f.NavLoadChildren(cc, ndataChild)
|
||||||
|
|
||||||
|
cc = cc.GetNextSibling()
|
||||||
|
if cc == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TMainForm) NavLoadChildren(node *vcl.TTreeNode, ndata *navData) error {
|
||||||
|
|
||||||
|
if ndata.childrenLoaded {
|
||||||
|
return nil // Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
// Find the child buckets from this point under the element
|
// Find the child buckets from this point under the element
|
||||||
nextBucketNames, err := ndata.ld.NavChildren(ndata)
|
nextBucketNames, err := ndata.ld.NavChildren(ndata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error()))
|
return fmt.Errorf("Failed to find child buckets under %q: %w", strings.Join(ndata.bucketPath, `/`), err)
|
||||||
*allowExpansion = false
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ndata.childrenLoaded = true // don't repeat this work
|
ndata.childrenLoaded = true // don't repeat this work
|
||||||
|
|
||||||
if len(nextBucketNames) == 0 {
|
if len(nextBucketNames) == 0 {
|
||||||
node.SetHasChildren(false)
|
node.SetHasChildren(false)
|
||||||
*allowExpansion = false
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
node.SetHasChildren(true)
|
||||||
|
|
||||||
// Populate LCL child nodes
|
// Populate LCL child nodes
|
||||||
for _, bucketName := range nextBucketNames {
|
for _, bucketName := range nextBucketNames {
|
||||||
@@ -319,7 +498,7 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
|
|||||||
|
|
||||||
ndata.ld.Keepalive(navData)
|
ndata.ld.Keepalive(navData)
|
||||||
}
|
}
|
||||||
|
|
||||||
*allowExpansion = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
5
platform_windows.go
Normal file
5
platform_windows.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/ying32/govcl/pkgs/winappres" // Extra _syso files for Windows
|
||||||
|
)
|
||||||
130
redisConnectionDialog.go
Normal file
130
redisConnectionDialog.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TRedisConnectionDialogResult = *redis.Options
|
||||||
|
|
||||||
|
type TRedisConnectionDialog struct {
|
||||||
|
*vcl.TForm
|
||||||
|
|
||||||
|
Address *vcl.TEdit
|
||||||
|
Port *vcl.TSpinEdit
|
||||||
|
Password *vcl.TEdit
|
||||||
|
IsResp3Protocol *vcl.TCheckBox
|
||||||
|
ConnectBtn *vcl.TButton
|
||||||
|
|
||||||
|
// CustomReturn must be set by the caller to a *redis.Options variable.
|
||||||
|
CustomReturn *TRedisConnectionDialogResult // nil = no CustomReturn/cancelled dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TRedisConnectionDialog) OnFormCreate(sender vcl.IObject) {
|
||||||
|
|
||||||
|
f.SetCaption("Connect to Redis...")
|
||||||
|
f.ScreenCenter()
|
||||||
|
f.SetWidth(320)
|
||||||
|
f.SetHeight(160)
|
||||||
|
|
||||||
|
// row 1
|
||||||
|
|
||||||
|
row1 := vcl_row(f, 1)
|
||||||
|
|
||||||
|
lblAddress := vcl.NewLabel(row1)
|
||||||
|
lblAddress.SetParent(row1)
|
||||||
|
lblAddress.SetCaption("Address:")
|
||||||
|
lblAddress.SetAlign(types.AlLeft)
|
||||||
|
lblAddress.SetLayout(types.TlCenter)
|
||||||
|
lblAddress.SetLeft(1)
|
||||||
|
lblAddress.BorderSpacing().SetAround(MY_SPACING)
|
||||||
|
|
||||||
|
f.Address = vcl.NewEdit(row1)
|
||||||
|
f.Address.SetParent(row1)
|
||||||
|
f.Address.SetAlign(types.AlClient) // grow AlLeft)
|
||||||
|
f.Address.SetWidth(MY_WIDTH)
|
||||||
|
f.Address.SetLeft(2)
|
||||||
|
f.Address.SetText("127.0.0.1")
|
||||||
|
f.Address.BorderSpacing().SetAround(MY_SPACING)
|
||||||
|
|
||||||
|
lblPort := vcl.NewLabel(row1)
|
||||||
|
lblPort.SetParent(row1)
|
||||||
|
lblPort.SetCaption(":")
|
||||||
|
lblPort.SetAlign(types.AlRight)
|
||||||
|
lblPort.SetLayout(types.TlCenter)
|
||||||
|
lblPort.SetLeft(0)
|
||||||
|
lblPort.SetAutoSize(true)
|
||||||
|
lblPort.BorderSpacing().SetAround(MY_SPACING)
|
||||||
|
|
||||||
|
f.Port = vcl.NewSpinEdit(row1)
|
||||||
|
f.Port.SetParent(row1)
|
||||||
|
f.Port.SetMinValue(1)
|
||||||
|
f.Port.SetMaxValue(65535)
|
||||||
|
f.Port.SetValue(6379) // Redis default port
|
||||||
|
f.Port.SetAlign(types.AlRight)
|
||||||
|
f.Port.SetLeft(1000)
|
||||||
|
f.Port.BorderSpacing().SetAround(MY_SPACING)
|
||||||
|
|
||||||
|
// row 2
|
||||||
|
|
||||||
|
row2 := vcl_row(f, 2)
|
||||||
|
|
||||||
|
lblPassword := vcl.NewLabel(row2)
|
||||||
|
lblPassword.SetParent(row2)
|
||||||
|
lblPassword.SetCaption("Password:")
|
||||||
|
lblPassword.SetAlign(types.AlLeft)
|
||||||
|
lblPassword.SetLayout(types.TlCenter)
|
||||||
|
lblPassword.SetLeft(1)
|
||||||
|
lblPassword.BorderSpacing().SetAround(MY_SPACING)
|
||||||
|
|
||||||
|
f.Password = vcl.NewEdit(row2)
|
||||||
|
f.Password.SetParent(row2)
|
||||||
|
f.Password.SetAlign(types.AlClient) // grow
|
||||||
|
f.Password.SetLeft(2)
|
||||||
|
f.Password.SetWidth(MY_WIDTH)
|
||||||
|
f.Password.SetPasswordChar(uint16('*'))
|
||||||
|
f.Password.BorderSpacing().SetAround(MY_SPACING)
|
||||||
|
|
||||||
|
// row 3
|
||||||
|
|
||||||
|
f.IsResp3Protocol = vcl.NewCheckBox(f)
|
||||||
|
f.IsResp3Protocol.SetParent(f)
|
||||||
|
f.IsResp3Protocol.SetCaption("Use RESP v3 protocol")
|
||||||
|
f.IsResp3Protocol.SetChecked(true)
|
||||||
|
f.IsResp3Protocol.SetAlign(types.AlTop)
|
||||||
|
f.IsResp3Protocol.SetTop(3)
|
||||||
|
f.IsResp3Protocol.BorderSpacing().SetAround(MY_SPACING)
|
||||||
|
|
||||||
|
// buttons
|
||||||
|
|
||||||
|
row4 := vcl_row(f, 4)
|
||||||
|
row4.SetAlign(types.AlBottom)
|
||||||
|
|
||||||
|
f.ConnectBtn = vcl.NewButton(row4)
|
||||||
|
f.ConnectBtn.SetParent(row4)
|
||||||
|
f.ConnectBtn.SetAlign(types.AlRight)
|
||||||
|
f.ConnectBtn.SetCaption("Connect")
|
||||||
|
f.ConnectBtn.SetOnClick(f.OnConnectBtnClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TRedisConnectionDialog) OnConnectBtnClick(sender vcl.IObject) {
|
||||||
|
|
||||||
|
ret := &redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", f.Address.Text(), f.Port.Value()),
|
||||||
|
Password: f.Password.Text(),
|
||||||
|
DB: 0, // Start off with default DB, but, we will browse them ourselves
|
||||||
|
Protocol: 2, // RESP2
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsResp3Protocol.Checked() {
|
||||||
|
ret.Protocol = 3 // RESP3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success.
|
||||||
|
// Fill in target pointer and close dialog with "MrOk" sentinel
|
||||||
|
*f.CustomReturn = ret
|
||||||
|
f.SetModalResult(types.MrOk)
|
||||||
|
}
|
||||||
11
util_types.go
Normal file
11
util_types.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// box_interface creates a slice where all elements of the input slice are boxed
|
||||||
|
// in an interface{} type.
|
||||||
|
func box_interface[T any](input []T) []interface{} {
|
||||||
|
ret := make([]interface{}, 0, len(input))
|
||||||
|
for _, v := range input {
|
||||||
|
ret = append(ret, v)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
27
util_vcl.go
Normal file
27
util_vcl.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MY_SPACING = 6
|
||||||
|
MY_HEIGHT = 90
|
||||||
|
MY_WIDTH = 180
|
||||||
|
)
|
||||||
|
|
||||||
|
// vcl_row makes a TPanel row inside the target component.
|
||||||
|
func vcl_row(owner vcl.IWinControl, top int32) *vcl.TPanel {
|
||||||
|
|
||||||
|
r := vcl.NewPanel(owner)
|
||||||
|
r.SetParent(owner)
|
||||||
|
r.SetBorderStyle(types.BsNone)
|
||||||
|
r.SetBevelInner(types.BvNone)
|
||||||
|
r.SetBevelOuter(types.BvNone)
|
||||||
|
r.SetAlign(types.AlTop)
|
||||||
|
r.SetTop(top)
|
||||||
|
r.SetAutoSize(true)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user