redis: improve dialog style, open connection, enumerate databases

This commit is contained in:
mappu 2024-06-23 13:07:33 +12:00
parent 0363bc65f4
commit 3a4bdbde94
5 changed files with 296 additions and 32 deletions

View File

@ -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
View File

@ -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) {

View File

@ -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
View 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
View 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
}