redis: improve dialog style, open connection, enumerate databases
This commit is contained in:
parent
0363bc65f4
commit
3a4bdbde94
184
db_redis.go
184
db_redis.go
@ -1,5 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/redis/go-redis/v9"
|
||||
"context"
|
||||
"errors"
|
||||
"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) {
|
||||
|
||||
/*
|
||||
// Load properties
|
||||
|
||||
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
|
||||
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName)
|
||||
f.propertiesBox.SetText(content)
|
||||
|
||||
// Load data
|
||||
|
||||
f.contentBox.SetEnabled(false)
|
||||
f.contentBox.Clear()
|
||||
|
||||
// Bolt always uses Key + Value as the columns
|
||||
|
||||
f.contentBox.Columns().Clear()
|
||||
colKey := f.contentBox.Columns().Add()
|
||||
colKey.SetCaption("Key")
|
||||
colKey.SetWidth(MY_WIDTH)
|
||||
colKey.SetAlignment(types.TaLeftJustify)
|
||||
colVal := f.contentBox.Columns().Add()
|
||||
colVal.SetCaption("Value")
|
||||
|
||||
err := ld.db.View(func(tx *bbolt.Tx) error {
|
||||
b := boltTargetBucket(tx, ndata.bucketPath)
|
||||
if b == nil {
|
||||
// no such bucket
|
||||
return nil
|
||||
}
|
||||
|
||||
// Valid
|
||||
f.contentBox.Clear()
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
dataEntry := f.contentBox.Items().Add()
|
||||
dataEntry.SetCaption(formatUtf8(k))
|
||||
dataEntry.SubItems().Add(formatUtf8(v))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Valid
|
||||
f.contentBox.SetEnabled(true)*/
|
||||
}
|
||||
|
||||
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||
// In the bolt implementation, the nav is a recursive tree of child buckets
|
||||
return nil, errors.New("TODO")
|
||||
//return boltChildBucketNames(ld.db, 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
|
||||
}
|
||||
|
||||
// Put ret into the data field somehow
|
||||
fmt.Printf("result\n%#v\n", ret)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
ld.Keepalive(navData)
|
||||
}
|
||||
|
21
main.go
21
main.go
@ -9,12 +9,6 @@ import (
|
||||
"github.com/ying32/govcl/vcl/types"
|
||||
)
|
||||
|
||||
const (
|
||||
MY_SPACING = 6
|
||||
MY_HEIGHT = 90
|
||||
MY_WIDTH = 180
|
||||
)
|
||||
|
||||
type TMainForm struct {
|
||||
*vcl.TForm
|
||||
|
||||
@ -241,10 +235,21 @@ func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) {
|
||||
}
|
||||
|
||||
func (f *TMainForm) OnMnuFileRedisConnectClick(sender vcl.IObject) {
|
||||
|
||||
var child *TRedisConnectionDialog
|
||||
vcl.Application.CreateForm(&child)
|
||||
child.ShowModal()
|
||||
child.Free()
|
||||
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) {
|
||||
|
@ -1,10 +1,15 @@
|
||||
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
|
||||
|
||||
@ -12,72 +17,76 @@ type TRedisConnectionDialog struct {
|
||||
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.NewPanel(f)
|
||||
row1.SetParent(f)
|
||||
row1.SetBorderStyle(types.BsNone)
|
||||
row1.SetAlign(types.AlTop)
|
||||
row1.SetTop(1)
|
||||
row1.SetAutoSize(true)
|
||||
row1 := vcl_row(f, 1)
|
||||
|
||||
lblAddress := vcl.NewStaticText(row1)
|
||||
lblAddress := vcl.NewLabel(row1)
|
||||
lblAddress.SetParent(row1)
|
||||
lblAddress.SetCaption("Address:")
|
||||
lblAddress.SetAlign(types.AlLeft)
|
||||
lblAddress.SetLayout(types.TlCenter)
|
||||
lblAddress.SetLeft(1)
|
||||
//lblAddress.SetAutoSize(true)
|
||||
lblAddress.BorderSpacing().SetAround(MY_SPACING)
|
||||
|
||||
f.Address = vcl.NewEdit(row1)
|
||||
f.Address.SetParent(row1)
|
||||
f.Address.SetAlign(types.AlLeft)
|
||||
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.NewStaticText(row1)
|
||||
lblPort := vcl.NewLabel(row1)
|
||||
lblPort.SetParent(row1)
|
||||
lblPort.SetCaption(":")
|
||||
lblPort.SetAlign(types.AlLeft)
|
||||
lblPort.SetLeft(3)
|
||||
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.AlLeft)
|
||||
f.Port.SetLeft(4)
|
||||
f.Port.SetAlign(types.AlRight)
|
||||
f.Port.SetLeft(1000)
|
||||
f.Port.BorderSpacing().SetAround(MY_SPACING)
|
||||
|
||||
// row 2
|
||||
|
||||
row2 := vcl.NewPanel(f)
|
||||
row2.SetParent(f)
|
||||
row2.SetBorderStyle(types.BsNone)
|
||||
row2.SetAlign(types.AlTop)
|
||||
row2.SetTop(2)
|
||||
row2.SetAutoSize(true)
|
||||
row2 := vcl_row(f, 2)
|
||||
|
||||
lblPassword := vcl.NewStaticText(row2)
|
||||
lblPassword := vcl.NewLabel(row2)
|
||||
lblPassword.SetParent(row2)
|
||||
lblPassword.SetCaption("Password:")
|
||||
lblPassword.SetAlign(types.AlLeft)
|
||||
lblPassword.SetLayout(types.TlCenter)
|
||||
lblPassword.SetLeft(1)
|
||||
// lblPassword.SetAutoSize(true)
|
||||
lblPassword.BorderSpacing().SetAround(MY_SPACING)
|
||||
|
||||
f.Password = vcl.NewEdit(row2)
|
||||
f.Password.SetParent(row2)
|
||||
f.Password.SetAlign(types.AlLeft)
|
||||
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
|
||||
|
||||
@ -87,5 +96,35 @@ func (f *TRedisConnectionDialog) OnFormCreate(sender vcl.IObject) {
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user