From e28d09150059840d27868f8bbfff97541fad6746 Mon Sep 17 00:00:00 2001
From: tiny-craft <137850705+tiny-craft@users.noreply.github.com>
Date: Sun, 5 Nov 2023 11:57:52 +0800
Subject: [PATCH] refactor: split connection_service into connection_service
and browser_service in go
refactor: split connectionStore into connectionStore and browserStore in js
---
backend/services/browser_service.go | 1348 +++++++++++++++
backend/services/connection_service.go | 1340 +--------------
backend/utils/string/common.go | 2 +-
frontend/src/AppContent.vue | 2 -
.../src/components/content/ContentLogPane.vue | 8 +-
.../components/content/ContentValueTab.vue | 4 +-
.../content_value/ContentServerStatus.vue | 8 +-
.../components/content_value/ContentSlog.vue | 10 +-
.../content_value/ContentToolbar.vue | 8 +-
.../content_value/ContentValueHash.vue | 12 +-
.../content_value/ContentValueList.vue | 12 +-
.../content_value/ContentValueSet.vue | 12 +-
.../content_value/ContentValueStream.vue | 8 +-
.../content_value/ContentValueString.vue | 12 +-
.../content_value/ContentValueWrapper.vue | 6 +-
.../content_value/ContentValueZSet.vue | 12 +-
.../components/dialogs/AddFieldsDialog.vue | 38 +-
.../components/dialogs/ConnectionDialog.vue | 4 +-
.../components/dialogs/DeleteKeyDialog.vue | 8 +-
.../src/components/dialogs/FlushDbDialog.vue | 6 +-
.../components/dialogs/KeyFilterDialog.vue | 8 +-
.../src/components/dialogs/NewKeyDialog.vue | 10 +-
.../components/dialogs/RenameKeyDialog.vue | 8 +-
.../src/components/dialogs/SetTtlDialog.vue | 6 +-
.../src/components/sidebar/BrowserPane.vue | 4 +-
.../src/components/sidebar/BrowserTree.vue | 42 +-
.../src/components/sidebar/ConnectionPane.vue | 2 -
.../src/components/sidebar/ConnectionTree.vue | 20 +-
frontend/src/components/sidebar/NavMenu.vue | 6 +-
frontend/src/stores/browser.js | 1482 ++++++++++++++++
frontend/src/stores/connections.js | 1491 +----------------
frontend/src/utils/theme.js | 1 +
main.go | 5 +-
33 files changed, 2994 insertions(+), 2951 deletions(-)
create mode 100644 backend/services/browser_service.go
create mode 100644 frontend/src/stores/browser.js
diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go
new file mode 100644
index 0000000..fbbf46b
--- /dev/null
+++ b/backend/services/browser_service.go
@@ -0,0 +1,1348 @@
+package services
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/redis/go-redis/v9"
+ "net/url"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+ "tinyrdm/backend/consts"
+ "tinyrdm/backend/types"
+ "tinyrdm/backend/utils/coll"
+ maputil "tinyrdm/backend/utils/map"
+ redis2 "tinyrdm/backend/utils/redis"
+ sliceutil "tinyrdm/backend/utils/slice"
+ strutil "tinyrdm/backend/utils/string"
+)
+
+type slowLogItem struct {
+ Timestamp int64 `json:"timestamp"`
+ Client string `json:"client"`
+ Addr string `json:"addr"`
+ Cmd string `json:"cmd"`
+ Cost int64 `json:"cost"`
+}
+
+type connectionItem struct {
+ client redis.UniversalClient
+ ctx context.Context
+ cancelFunc context.CancelFunc
+ cursor map[int]uint64 // current cursor of databases
+ stepSize int64
+}
+
+type browserService struct {
+ ctx context.Context
+ connMap map[string]connectionItem
+ cmdHistory []cmdHistoryItem
+}
+
+var browser *browserService
+var onceBrowser sync.Once
+
+func Browser() *browserService {
+ if browser == nil {
+ onceBrowser.Do(func() {
+ browser = &browserService{
+ connMap: map[string]connectionItem{},
+ }
+ })
+ }
+ return browser
+}
+
+func (b *browserService) Start(ctx context.Context) {
+ b.ctx = ctx
+}
+
+func (b *browserService) Stop() {
+ for _, item := range b.connMap {
+ if item.client != nil {
+ item.cancelFunc()
+ item.client.Close()
+ }
+ }
+ b.connMap = map[string]connectionItem{}
+}
+
+// OpenConnection open redis server connection
+func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
+ item, err := b.getRedisClient(name, 0)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ // get connection config
+ selConn := Connection().getConnection(name)
+
+ var totaldb int
+ if selConn.DBFilterType == "" || selConn.DBFilterType == "none" {
+ // get total databases
+ if config, err := client.ConfigGet(ctx, "databases").Result(); err == nil {
+ if total, err := strconv.Atoi(config["databases"]); err == nil {
+ totaldb = total
+ }
+ }
+ }
+
+ // parse all db, response content like below
+ var dbs []types.ConnectionDB
+ var clusterKeyCount int64
+ cluster, isCluster := client.(*redis.ClusterClient)
+ if isCluster {
+ var keyCount atomic.Int64
+ err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
+ if size, serr := cli.DBSize(ctx).Result(); serr != nil {
+ return serr
+ } else {
+ keyCount.Add(size)
+ }
+ return nil
+ })
+ if err != nil {
+ resp.Msg = "get db size error:" + err.Error()
+ return
+ }
+ clusterKeyCount = keyCount.Load()
+
+ // only one database in cluster mode
+ dbs = []types.ConnectionDB{
+ {
+ Name: "db0",
+ Index: 0,
+ Keys: int(clusterKeyCount),
+ },
+ }
+ } else {
+ // get database info
+ var res string
+ res, err = client.Info(ctx, "keyspace").Result()
+ if err != nil {
+ resp.Msg = "get server info fail:" + err.Error()
+ return
+ }
+ info := b.parseInfo(res)
+
+ if totaldb <= 0 {
+ // cannot retrieve the database count by "CONFIG GET databases", try to get max index from keyspace
+ keyspace := info["Keyspace"]
+ var db, maxDB int
+ for dbName := range keyspace {
+ if db, err = strconv.Atoi(strings.TrimLeft(dbName, "db")); err == nil {
+ if maxDB < db {
+ maxDB = db
+ }
+ }
+ }
+ totaldb = maxDB + 1
+ }
+
+ queryDB := func(idx int) types.ConnectionDB {
+ dbName := "db" + strconv.Itoa(idx)
+ dbInfoStr := info["Keyspace"][dbName]
+ if len(dbInfoStr) > 0 {
+ dbInfo := b.parseDBItemInfo(dbInfoStr)
+ return types.ConnectionDB{
+ Name: dbName,
+ Index: idx,
+ Keys: dbInfo["keys"],
+ Expires: dbInfo["expires"],
+ AvgTTL: dbInfo["avg_ttl"],
+ }
+ } else {
+ return types.ConnectionDB{
+ Name: dbName,
+ Index: idx,
+ }
+ }
+ }
+
+ switch selConn.DBFilterType {
+ case "show":
+ filterList := sliceutil.Unique(selConn.DBFilterList)
+ for _, idx := range filterList {
+ dbs = append(dbs, queryDB(idx))
+ }
+ case "hide":
+ hiddenList := coll.NewSet(selConn.DBFilterList...)
+ for idx := 0; idx < totaldb; idx++ {
+ if !hiddenList.Contains(idx) {
+ dbs = append(dbs, queryDB(idx))
+ }
+ }
+ default:
+ for idx := 0; idx < totaldb; idx++ {
+ dbs = append(dbs, queryDB(idx))
+ }
+ }
+ }
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "db": dbs,
+ "view": selConn.KeyView,
+ }
+ return
+}
+
+// CloseConnection close redis server connection
+func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
+ item, ok := b.connMap[name]
+ if ok {
+ delete(b.connMap, name)
+ if item.client != nil {
+ item.cancelFunc()
+ item.client.Close()
+ }
+ }
+ resp.Success = true
+ return
+}
+
+// get a redis client from local cache or create a new open
+// if db >= 0, will also switch to db index
+func (b *browserService) getRedisClient(connName string, db int) (item connectionItem, err error) {
+ var ok bool
+ var client redis.UniversalClient
+ if item, ok = b.connMap[connName]; ok {
+ client = item.client
+ } else {
+ selConn := Connection().getConnection(connName)
+ if selConn == nil {
+ err = fmt.Errorf("no match connection \"%s\"", connName)
+ return
+ }
+
+ hook := redis2.NewHook(connName, func(cmd string, cost int64) {
+ now := time.Now()
+ //last := strings.LastIndex(cmd, ":")
+ //if last != -1 {
+ // cmd = cmd[:last]
+ //}
+ b.cmdHistory = append(b.cmdHistory, cmdHistoryItem{
+ Timestamp: now.UnixMilli(),
+ Server: connName,
+ Cmd: cmd,
+ Cost: cost,
+ })
+ })
+
+ client, err = Connection().createRedisClient(selConn.ConnectionConfig)
+ if err != nil {
+ err = fmt.Errorf("create conenction error: %s", err.Error())
+ return
+ }
+ // add hook to each node in cluster mode
+ var cluster *redis.ClusterClient
+ if cluster, ok = client.(*redis.ClusterClient); ok {
+ err = cluster.ForEachShard(b.ctx, func(ctx context.Context, cli *redis.Client) error {
+ cli.AddHook(hook)
+ return nil
+ })
+ if err != nil {
+ err = fmt.Errorf("get cluster nodes error: %s", err.Error())
+ return
+ }
+ } else {
+ client.AddHook(hook)
+ }
+
+ if _, err = client.Ping(b.ctx).Result(); err != nil && err != redis.Nil {
+ err = errors.New("can not connect to redis server:" + err.Error())
+ return
+ }
+ ctx, cancelFunc := context.WithCancel(b.ctx)
+ item = connectionItem{
+ client: client,
+ ctx: ctx,
+ cancelFunc: cancelFunc,
+ cursor: map[int]uint64{},
+ stepSize: int64(selConn.LoadSize),
+ }
+ if item.stepSize <= 0 {
+ item.stepSize = consts.DEFAULT_LOAD_SIZE
+ }
+ b.connMap[connName] = item
+ }
+
+ if db >= 0 {
+ var rdb *redis.Client
+ if rdb, ok = client.(*redis.Client); ok && rdb != nil {
+ if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil {
+ return
+ }
+ }
+ }
+ return
+}
+
+// save current scan cursor
+func (b *browserService) setClientCursor(connName string, db int, cursor uint64) {
+ if _, ok := b.connMap[connName]; ok {
+ if cursor == 0 {
+ delete(b.connMap[connName].cursor, db)
+ } else {
+ b.connMap[connName].cursor[db] = cursor
+ }
+ }
+}
+
+// parse command response content which use "redis info"
+// # Keyspace\r\ndb0:keys=2,expires=1,avg_ttl=1877111749\r\ndb1:keys=33,expires=0,avg_ttl=0\r\ndb3:keys=17,expires=0,avg_ttl=0\r\ndb5:keys=3,expires=0,avg_ttl=0\r\n
+func (b *browserService) parseInfo(info string) map[string]map[string]string {
+ parsedInfo := map[string]map[string]string{}
+ lines := strings.Split(info, "\r\n")
+ if len(lines) > 0 {
+ var subInfo map[string]string
+ for _, line := range lines {
+ if strings.HasPrefix(line, "#") {
+ subInfo = map[string]string{}
+ parsedInfo[strings.TrimSpace(strings.TrimLeft(line, "#"))] = subInfo
+ } else {
+ items := strings.SplitN(line, ":", 2)
+ if len(items) < 2 {
+ continue
+ }
+ subInfo[items[0]] = items[1]
+ }
+ }
+ }
+ return parsedInfo
+}
+
+// parse db item value, content format like below
+// keys=2,expires=1,avg_ttl=1877111749
+func (b *browserService) parseDBItemInfo(info string) map[string]int {
+ ret := map[string]int{}
+ items := strings.Split(info, ",")
+ for _, item := range items {
+ kv := strings.SplitN(item, "=", 2)
+ if len(kv) > 1 {
+ ret[kv[0]], _ = strconv.Atoi(kv[1])
+ }
+ }
+ return ret
+}
+
+// ServerInfo get server info
+func (b *browserService) ServerInfo(name string) (resp types.JSResp) {
+ item, err := b.getRedisClient(name, 0)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ // get database info
+ res, err := client.Info(ctx).Result()
+ if err != nil {
+ resp.Msg = "get server info fail:" + err.Error()
+ return
+ }
+
+ resp.Success = true
+ resp.Data = b.parseInfo(res)
+ return
+}
+
+// OpenDatabase open select database, and list all keys
+// @param path contain connection name and db name
+func (b *browserService) OpenDatabase(connName string, db int, match string, keyType string) (resp types.JSResp) {
+ b.setClientCursor(connName, db, 0)
+ return b.LoadNextKeys(connName, db, match, keyType)
+}
+
+// scan keys
+// @return loaded keys
+// @return next cursor
+// @return scan error
+func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalClient, match, keyType string, cursor uint64, count int64) ([]any, uint64, error) {
+ var err error
+ filterType := len(keyType) > 0
+ scanSize := int64(Preferences().GetScanSize())
+ // define sub scan function
+ scan := func(ctx context.Context, cli redis.UniversalClient, appendFunc func(k []any)) error {
+ var loadedKey []string
+ var scanCount int64
+ for {
+ if filterType {
+ loadedKey, cursor, err = cli.ScanType(ctx, cursor, match, scanSize, keyType).Result()
+ } else {
+ loadedKey, cursor, err = cli.Scan(ctx, cursor, match, scanSize).Result()
+ }
+ if err != nil {
+ return err
+ } else {
+ ks := sliceutil.Map(loadedKey, func(i int) any {
+ return strutil.EncodeRedisKey(loadedKey[i])
+ })
+ scanCount += int64(len(ks))
+ appendFunc(ks)
+ }
+
+ if (count > 0 && scanCount > count) || cursor == 0 {
+ break
+ }
+ }
+ return nil
+ }
+
+ var keys []any
+ if cluster, ok := client.(*redis.ClusterClient); ok {
+ // cluster mode
+ var mutex sync.Mutex
+ err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
+ return scan(ctx, cli, func(k []any) {
+ mutex.Lock()
+ keys = append(keys, k...)
+ mutex.Unlock()
+ })
+ })
+ } else {
+ err = scan(ctx, client, func(k []any) {
+ keys = append(keys, k...)
+ })
+ }
+ if err != nil {
+ return nil, cursor, err
+ }
+ return keys, cursor, nil
+}
+
+// LoadNextKeys load next key from saved cursor
+func (b *browserService) LoadNextKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx, count := item.client, item.ctx, item.stepSize
+ cursor := item.cursor[db]
+ keys, cursor, err := b.scanKeys(ctx, client, match, keyType, cursor, count)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ b.setClientCursor(connName, db, cursor)
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "keys": keys,
+ "end": cursor == 0,
+ }
+ return
+}
+
+// LoadAllKeys load all keys
+func (b *browserService) LoadAllKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ cursor := item.cursor[db]
+ keys, _, err := b.scanKeys(ctx, client, match, keyType, cursor, 0)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ b.setClientCursor(connName, db, 0)
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "keys": keys,
+ }
+ return
+}
+
+// GetKeyValue get value by key
+func (b *browserService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ var keyType string
+ var dur time.Duration
+ keyType, err = client.Type(ctx, key).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ if keyType == "none" {
+ resp.Msg = "key not exists"
+ return
+ }
+
+ var ttl int64
+ if dur, err = client.TTL(ctx, key).Result(); err != nil {
+ ttl = -1
+ } else {
+ if dur < 0 {
+ ttl = -1
+ } else {
+ ttl = int64(dur.Seconds())
+ }
+ }
+
+ var value any
+ var size, length int64
+ var cursor uint64
+ switch strings.ToLower(keyType) {
+ case "string":
+ var str string
+ str, err = client.Get(ctx, key).Result()
+ value, decodeType, viewAs = strutil.ConvertTo(str, decodeType, viewAs)
+ length, _ = client.StrLen(ctx, key).Result()
+ size, _ = client.MemoryUsage(ctx, key, 0).Result()
+ case "list":
+ value, err = client.LRange(ctx, key, 0, -1).Result()
+ length, _ = client.LLen(ctx, key).Result()
+ size, _ = client.MemoryUsage(ctx, key, 0).Result()
+ case "hash":
+ //value, err = client.HGetAll(ctx, key).Result()
+ items := map[string]string{}
+ scanSize := int64(Preferences().GetScanSize())
+ for {
+ var loadedVal []string
+ loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", scanSize).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ for i := 0; i < len(loadedVal); i += 2 {
+ items[loadedVal[i]] = loadedVal[i+1]
+ }
+ if cursor == 0 {
+ break
+ }
+ }
+ value = items
+ length, _ = client.HLen(ctx, key).Result()
+ size, _ = client.MemoryUsage(ctx, key, 0).Result()
+ case "set":
+ //value, err = client.SMembers(ctx, key).Result()
+ items := []string{}
+ scanSize := int64(Preferences().GetScanSize())
+ for {
+ var loadedKey []string
+ loadedKey, cursor, err = client.SScan(ctx, key, cursor, "*", scanSize).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ items = append(items, loadedKey...)
+ if cursor == 0 {
+ break
+ }
+ }
+ value = items
+ length, _ = client.SCard(ctx, key).Result()
+ size, _ = client.MemoryUsage(ctx, key, 0).Result()
+ case "zset":
+ //value, err = client.ZRangeWithScores(ctx, key, 0, -1).Result()
+ var items []types.ZSetItem
+ scanSize := int64(Preferences().GetScanSize())
+ for {
+ var loadedVal []string
+ loadedVal, cursor, err = client.ZScan(ctx, key, cursor, "*", scanSize).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ var score float64
+ for i := 0; i < len(loadedVal); i += 2 {
+ if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
+ items = append(items, types.ZSetItem{
+ Value: loadedVal[i],
+ Score: score,
+ })
+ }
+ }
+ if cursor == 0 {
+ break
+ }
+ }
+ value = items
+ length, _ = client.ZCard(ctx, key).Result()
+ size, _ = client.MemoryUsage(ctx, key, 0).Result()
+ case "stream":
+ var msgs []redis.XMessage
+ items := []types.StreamItem{}
+ msgs, err = client.XRevRange(ctx, key, "+", "-").Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ for _, msg := range msgs {
+ items = append(items, types.StreamItem{
+ ID: msg.ID,
+ Value: msg.Values,
+ })
+ }
+ value = items
+ length, _ = client.XLen(ctx, key).Result()
+ size, _ = client.MemoryUsage(ctx, key, 0).Result()
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ resp.Success = true
+ resp.Data = map[string]any{
+ "type": keyType,
+ "ttl": ttl,
+ "value": value,
+ "size": size,
+ "length": length,
+ "viewAs": viewAs,
+ "decode": decodeType,
+ }
+ return
+}
+
+// SetKeyValue set value by key
+// @param ttl <= 0 means keep current ttl
+func (b *browserService) SetKeyValue(connName string, db int, k any, keyType string, value any, ttl int64, viewAs, decode string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ var expiration time.Duration
+ if ttl < 0 {
+ if expiration, err = client.PTTL(ctx, key).Result(); err != nil {
+ expiration = redis.KeepTTL
+ }
+ } else {
+ expiration = time.Duration(ttl) * time.Second
+ }
+ switch strings.ToLower(keyType) {
+ case "string":
+ if str, ok := value.(string); !ok {
+ resp.Msg = "invalid string value"
+ return
+ } else {
+ var saveStr string
+ if saveStr, err = strutil.SaveAs(str, viewAs, decode); err != nil {
+ resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error())
+ return
+ }
+ _, err = client.Set(ctx, key, saveStr, 0).Result()
+ // set expiration lonely, not "keepttl"
+ if err == nil && expiration > 0 {
+ client.Expire(ctx, key, expiration)
+ }
+ }
+ case "list":
+ if strs, ok := value.([]any); !ok {
+ resp.Msg = "invalid list value"
+ return
+ } else {
+ err = client.LPush(ctx, key, strs...).Err()
+ if err == nil && expiration > 0 {
+ client.Expire(ctx, key, expiration)
+ }
+ }
+ case "hash":
+ if strs, ok := value.([]any); !ok {
+ resp.Msg = "invalid hash value"
+ return
+ } else {
+ total := len(strs)
+ if total > 1 {
+ _, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
+ for i := 0; i < total; i += 2 {
+ pipe.HSet(ctx, key, strs[i], strs[i+1])
+ }
+ if expiration > 0 {
+ pipe.Expire(ctx, key, expiration)
+ }
+ return nil
+ })
+ }
+ }
+ case "set":
+ if strs, ok := value.([]any); !ok || len(strs) <= 0 {
+ resp.Msg = "invalid set value"
+ return
+ } else {
+ if len(strs) > 0 {
+ err = client.SAdd(ctx, key, strs...).Err()
+ if err == nil && expiration > 0 {
+ client.Expire(ctx, key, expiration)
+ }
+ }
+ }
+ case "zset":
+ if strs, ok := value.([]any); !ok || len(strs) <= 0 {
+ resp.Msg = "invalid zset value"
+ return
+ } else {
+ if len(strs) > 1 {
+ var members []redis.Z
+ for i := 0; i < len(strs); i += 2 {
+ score, _ := strconv.ParseFloat(strs[i+1].(string), 64)
+ members = append(members, redis.Z{
+ Score: score,
+ Member: strs[i],
+ })
+ }
+ err = client.ZAdd(ctx, key, members...).Err()
+ if err == nil && expiration > 0 {
+ client.Expire(ctx, key, expiration)
+ }
+ }
+ }
+ case "stream":
+ if strs, ok := value.([]any); !ok {
+ resp.Msg = "invalid stream value"
+ return
+ } else {
+ if len(strs) > 2 {
+ err = client.XAdd(ctx, &redis.XAddArgs{
+ Stream: key,
+ ID: strs[0].(string),
+ Values: strs[1:],
+ }).Err()
+ if err == nil && expiration > 0 {
+ client.Expire(ctx, key, expiration)
+ }
+ }
+ }
+ }
+
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ resp.Success = true
+ resp.Data = map[string]any{
+ "value": value,
+ }
+ return
+}
+
+// SetHashValue set hash field
+func (b *browserService) SetHashValue(connName string, db int, k any, field, newField, value string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ var removedField []string
+ updatedField := map[string]string{}
+ if len(field) <= 0 {
+ // old filed is empty, add new field
+ _, err = client.HSet(ctx, key, newField, value).Result()
+ updatedField[newField] = value
+ } else if len(newField) <= 0 {
+ // new field is empty, delete old field
+ _, err = client.HDel(ctx, key, field, value).Result()
+ removedField = append(removedField, field)
+ } else if field == newField {
+ // replace field
+ _, err = client.HSet(ctx, key, newField, value).Result()
+ updatedField[newField] = value
+ } else {
+ // remove old field and add new field
+ if _, err = client.HDel(ctx, key, field).Result(); err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ _, err = client.HSet(ctx, key, newField, value).Result()
+ removedField = append(removedField, field)
+ updatedField[newField] = value
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "removed": removedField,
+ "updated": updatedField,
+ }
+ return
+}
+
+// AddHashField add or update hash field
+func (b *browserService) AddHashField(connName string, db int, k any, action int, fieldItems []any) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ updated := map[string]any{}
+ switch action {
+ case 1:
+ // ignore duplicated fields
+ for i := 0; i < len(fieldItems); i += 2 {
+ _, err = client.HSetNX(ctx, key, fieldItems[i].(string), fieldItems[i+1]).Result()
+ if err == nil {
+ updated[fieldItems[i].(string)] = fieldItems[i+1]
+ }
+ }
+ default:
+ // overwrite duplicated fields
+ total := len(fieldItems)
+ if total > 1 {
+ _, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
+ for i := 0; i < total; i += 2 {
+ client.HSet(ctx, key, fieldItems[i], fieldItems[i+1])
+ }
+ return nil
+ })
+ for i := 0; i < total; i += 2 {
+ updated[fieldItems[i].(string)] = fieldItems[i+1]
+ }
+ }
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "updated": updated,
+ }
+ return
+}
+
+// AddListItem add item to list or remove from it
+func (b *browserService) AddListItem(connName string, db int, k any, action int, items []any) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ var leftPush, rightPush []any
+ switch action {
+ case 0:
+ // push to head
+ _, err = client.LPush(ctx, key, items...).Result()
+ leftPush = append(leftPush, items...)
+ default:
+ // append to tail
+ _, err = client.RPush(ctx, key, items...).Result()
+ rightPush = append(rightPush, items...)
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "left": leftPush,
+ "right": rightPush,
+ }
+ return
+}
+
+// SetListItem update or remove list item by index
+func (b *browserService) SetListItem(connName string, db int, k any, index int64, value string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ var removed []int64
+ updated := map[int64]string{}
+ if len(value) <= 0 {
+ // remove from list
+ err = client.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ err = client.LRem(ctx, key, 1, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ removed = append(removed, index)
+ } else {
+ // replace index value
+ err = client.LSet(ctx, key, index, value).Err()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ updated[index] = value
+ }
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "removed": removed,
+ "updated": updated,
+ }
+ return
+}
+
+// SetSetItem add members to set or remove from set
+func (b *browserService) SetSetItem(connName string, db int, k any, remove bool, members []any) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ if remove {
+ _, err = client.SRem(ctx, key, members...).Result()
+ } else {
+ _, err = client.SAdd(ctx, key, members...).Result()
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ return
+}
+
+// UpdateSetItem replace member of set
+func (b *browserService) UpdateSetItem(connName string, db int, k any, value, newValue string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ _, _ = client.SRem(ctx, key, value).Result()
+ _, err = client.SAdd(ctx, key, newValue).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ return
+}
+
+// UpdateZSetValue update value of sorted set member
+func (b *browserService) UpdateZSetValue(connName string, db int, k any, value, newValue string, score float64) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ updated := map[string]any{}
+ var removed []string
+ if len(newValue) <= 0 {
+ // blank new value, delete value
+ _, err = client.ZRem(ctx, key, value).Result()
+ if err == nil {
+ removed = append(removed, value)
+ }
+ } else if newValue == value {
+ // update score only
+ _, err = client.ZAdd(ctx, key, redis.Z{
+ Score: score,
+ Member: value,
+ }).Result()
+ } else {
+ // remove old value and add new one
+ _, err = client.ZRem(ctx, key, value).Result()
+ if err == nil {
+ removed = append(removed, value)
+ }
+
+ _, err = client.ZAdd(ctx, key, redis.Z{
+ Score: score,
+ Member: newValue,
+ }).Result()
+ if err == nil {
+ updated[newValue] = score
+ }
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "updated": updated,
+ "removed": removed,
+ }
+ return
+}
+
+// AddZSetValue add item to sorted set
+func (b *browserService) AddZSetValue(connName string, db int, k any, action int, valueScore map[string]float64) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ members := maputil.ToSlice(valueScore, func(k string) redis.Z {
+ return redis.Z{
+ Score: valueScore[k],
+ Member: k,
+ }
+ })
+
+ switch action {
+ case 1:
+ // ignore duplicated fields
+ _, err = client.ZAddNX(ctx, key, members...).Result()
+ default:
+ // overwrite duplicated fields
+ _, err = client.ZAdd(ctx, key, members...).Result()
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ return
+}
+
+// AddStreamValue add stream field
+func (b *browserService) AddStreamValue(connName string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ _, err = client.XAdd(ctx, &redis.XAddArgs{
+ Stream: key,
+ ID: ID,
+ Values: fieldItems,
+ }).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ return
+}
+
+// RemoveStreamValues remove stream values by id
+func (b *browserService) RemoveStreamValues(connName string, db int, k any, IDs []string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ _, err = client.XDel(ctx, key, IDs...).Result()
+ resp.Success = true
+ return
+}
+
+// SetKeyTTL set ttl of key
+func (b *browserService) SetKeyTTL(connName string, db int, k any, ttl int64) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ var expiration time.Duration
+ if ttl < 0 {
+ if err = client.Persist(ctx, key).Err(); err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ } else {
+ expiration = time.Duration(ttl) * time.Second
+ if err = client.Expire(ctx, key, expiration).Err(); err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ }
+
+ resp.Success = true
+ return
+}
+
+// DeleteKey remove redis key
+func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ var deletedKeys []string
+ if strings.HasSuffix(key, "*") {
+ // delete by prefix
+ var mutex sync.Mutex
+ del := func(ctx context.Context, cli redis.UniversalClient) error {
+ handleDel := func(ks []string) error {
+ pipe := cli.Pipeline()
+ for _, k2 := range ks {
+ if async {
+ cli.Unlink(ctx, k2)
+ } else {
+ cli.Del(ctx, k2)
+ }
+ }
+ pipe.Exec(ctx)
+
+ mutex.Lock()
+ deletedKeys = append(deletedKeys, ks...)
+ mutex.Unlock()
+
+ return nil
+ }
+
+ scanSize := int64(Preferences().GetScanSize())
+ iter := cli.Scan(ctx, 0, key, scanSize).Iterator()
+ resultKeys := make([]string, 0, 100)
+ for iter.Next(ctx) {
+ resultKeys = append(resultKeys, iter.Val())
+ if len(resultKeys) >= 3 {
+ handleDel(resultKeys)
+ resultKeys = resultKeys[:0:cap(resultKeys)]
+ }
+ }
+
+ if len(resultKeys) > 0 {
+ handleDel(resultKeys)
+ }
+ return nil
+ }
+
+ if cluster, ok := client.(*redis.ClusterClient); ok {
+ // cluster mode
+ err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
+ return del(ctx, cli)
+ })
+ } else {
+ err = del(ctx, client)
+ }
+
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ } else {
+ // delete key only
+ if async {
+ if _, err = client.Unlink(ctx, key).Result(); err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ } else {
+ if _, err = client.Del(ctx, key).Result(); err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ }
+ deletedKeys = append(deletedKeys, key)
+ }
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "deleted": deletedKeys,
+ }
+ return
+}
+
+// FlushDB flush database
+func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ flush := func(ctx context.Context, cli redis.UniversalClient) {
+ cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
+ pipe.Select(ctx, db)
+ if async {
+ pipe.FlushDBAsync(ctx)
+ } else {
+ pipe.FlushDB(ctx)
+ }
+ return nil
+ })
+ }
+
+ client, ctx := item.client, item.ctx
+ if cluster, ok := client.(*redis.ClusterClient); ok {
+ // cluster mode
+ err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
+ flush(ctx, cli)
+ return nil
+ })
+ } else {
+ flush(ctx, client)
+ }
+
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ resp.Success = true
+ return
+}
+
+// RenameKey rename key
+func (b *browserService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ if _, ok := client.(*redis.ClusterClient); ok {
+ resp.Msg = "RENAME not support in cluster mode yet"
+ return
+ }
+
+ if _, err = client.RenameNX(ctx, key, newKey).Result(); err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ return
+}
+
+// GetCmdHistory get redis command history
+func (b *browserService) GetCmdHistory(pageNo, pageSize int) (resp types.JSResp) {
+ resp.Success = true
+ if pageSize <= 0 || pageNo <= 0 {
+ // return all history
+ resp.Data = map[string]any{
+ "list": b.cmdHistory,
+ "pageNo": 1,
+ "pageSize": -1,
+ }
+ } else {
+ total := len(b.cmdHistory)
+ startIndex := total / pageSize * (pageNo - 1)
+ endIndex := min(startIndex+pageSize, total)
+ resp.Data = map[string]any{
+ "list": b.cmdHistory[startIndex:endIndex],
+ "pageNo": pageNo,
+ "pageSize": pageSize,
+ }
+ }
+ return
+}
+
+// CleanCmdHistory clean redis command history
+func (b *browserService) CleanCmdHistory() (resp types.JSResp) {
+ b.cmdHistory = []cmdHistoryItem{}
+ resp.Success = true
+ return
+}
+
+// GetSlowLogs get slow log list
+func (b *browserService) GetSlowLogs(connName string, db int, num int64) (resp types.JSResp) {
+ item, err := b.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ var logs []redis.SlowLog
+ if cluster, ok := client.(*redis.ClusterClient); ok {
+ // cluster mode
+ var mu sync.Mutex
+ err = cluster.ForEachShard(ctx, func(ctx context.Context, cli *redis.Client) error {
+ if subLogs, _ := client.SlowLogGet(ctx, num).Result(); len(subLogs) > 0 {
+ mu.Lock()
+ logs = append(logs, subLogs...)
+ mu.Unlock()
+ }
+ return nil
+ })
+ } else {
+ logs, err = client.SlowLogGet(ctx, num).Result()
+ }
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ sort.Slice(logs, func(i, j int) bool {
+ return logs[i].Time.UnixMilli() > logs[j].Time.UnixMilli()
+ })
+ if len(logs) > int(num) {
+ logs = logs[:num]
+ }
+
+ list := sliceutil.Map(logs, func(i int) slowLogItem {
+ var name string
+ var e error
+ if name, e = url.QueryUnescape(logs[i].ClientName); e != nil {
+ name = logs[i].ClientName
+ }
+ return slowLogItem{
+ Timestamp: logs[i].Time.UnixMilli(),
+ Client: name,
+ Addr: logs[i].ClientAddr,
+ Cmd: sliceutil.JoinString(logs[i].Args, " "),
+ Cost: logs[i].Duration.Milliseconds(),
+ }
+ })
+
+ resp.Success = true
+ resp.Data = map[string]any{
+ "list": list,
+ }
+ return
+}
diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go
index a35bf04..1a0c475 100644
--- a/backend/services/connection_service.go
+++ b/backend/services/connection_service.go
@@ -11,20 +11,11 @@ import (
"net"
"net/url"
"os"
- "sort"
- "strconv"
"strings"
"sync"
- "sync/atomic"
"time"
- "tinyrdm/backend/consts"
. "tinyrdm/backend/storage"
"tinyrdm/backend/types"
- "tinyrdm/backend/utils/coll"
- maputil "tinyrdm/backend/utils/map"
- redis2 "tinyrdm/backend/utils/redis"
- sliceutil "tinyrdm/backend/utils/slice"
- strutil "tinyrdm/backend/utils/string"
)
type cmdHistoryItem struct {
@@ -34,31 +25,9 @@ type cmdHistoryItem struct {
Cost int64 `json:"cost"`
}
-type slowLogItem struct {
- Timestamp int64 `json:"timestamp"`
- Client string `json:"client"`
- Addr string `json:"addr"`
- Cmd string `json:"cmd"`
- Cost int64 `json:"cost"`
-}
-
type connectionService struct {
- ctx context.Context
- conns *ConnectionsStorage
- connMap map[string]connectionItem
- cmdHistory []cmdHistoryItem
-}
-
-type connectionItem struct {
- client redis.UniversalClient
- ctx context.Context
- cancelFunc context.CancelFunc
- cursor map[int]uint64 // current cursor of databases
- stepSize int64
-}
-
-type keyItem struct {
- Type string `json:"t"`
+ ctx context.Context
+ conns *ConnectionsStorage
}
var connection *connectionService
@@ -68,8 +37,7 @@ func Connection() *connectionService {
if connection == nil {
onceConnection.Do(func() {
connection = &connectionService{
- conns: NewConnections(),
- connMap: map[string]connectionItem{},
+ conns: NewConnections(),
}
})
}
@@ -80,16 +48,6 @@ func (c *connectionService) Start(ctx context.Context) {
c.ctx = ctx
}
-func (c *connectionService) Stop() {
- for _, item := range c.connMap {
- if item.client != nil {
- item.cancelFunc()
- item.client.Close()
- }
- }
- c.connMap = map[string]connectionItem{}
-}
-
func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.Options, error) {
var sshClient *ssh.Client
if config.SSH.Enable {
@@ -403,1295 +361,3 @@ func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp typ
resp.Success = true
return
}
-
-// OpenConnection open redis server connection
-func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
- item, err := c.getRedisClient(name, 0)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- // get connection config
- selConn := c.conns.GetConnection(name)
-
- var totaldb int
- if selConn.DBFilterType == "" || selConn.DBFilterType == "none" {
- // get total databases
- if config, err := client.ConfigGet(ctx, "databases").Result(); err == nil {
- if total, err := strconv.Atoi(config["databases"]); err == nil {
- totaldb = total
- }
- }
- }
-
- // parse all db, response content like below
- var dbs []types.ConnectionDB
- var clusterKeyCount int64
- cluster, isCluster := client.(*redis.ClusterClient)
- if isCluster {
- var keyCount atomic.Int64
- err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
- if size, serr := cli.DBSize(ctx).Result(); serr != nil {
- return serr
- } else {
- keyCount.Add(size)
- }
- return nil
- })
- if err != nil {
- resp.Msg = "get db size error:" + err.Error()
- return
- }
- clusterKeyCount = keyCount.Load()
-
- // only one database in cluster mode
- dbs = []types.ConnectionDB{
- {
- Name: "db0",
- Index: 0,
- Keys: int(clusterKeyCount),
- },
- }
- } else {
- // get database info
- var res string
- res, err = client.Info(ctx, "keyspace").Result()
- if err != nil {
- resp.Msg = "get server info fail:" + err.Error()
- return
- }
- info := c.parseInfo(res)
-
- if totaldb <= 0 {
- // cannot retrieve the database count by "CONFIG GET databases", try to get max index from keyspace
- keyspace := info["Keyspace"]
- var db, maxDB int
- for dbName := range keyspace {
- if db, err = strconv.Atoi(strings.TrimLeft(dbName, "db")); err == nil {
- if maxDB < db {
- maxDB = db
- }
- }
- }
- totaldb = maxDB + 1
- }
-
- queryDB := func(idx int) types.ConnectionDB {
- dbName := "db" + strconv.Itoa(idx)
- dbInfoStr := info["Keyspace"][dbName]
- if len(dbInfoStr) > 0 {
- dbInfo := c.parseDBItemInfo(dbInfoStr)
- return types.ConnectionDB{
- Name: dbName,
- Index: idx,
- Keys: dbInfo["keys"],
- Expires: dbInfo["expires"],
- AvgTTL: dbInfo["avg_ttl"],
- }
- } else {
- return types.ConnectionDB{
- Name: dbName,
- Index: idx,
- }
- }
- }
-
- switch selConn.DBFilterType {
- case "show":
- filterList := sliceutil.Unique(selConn.DBFilterList)
- for _, idx := range filterList {
- dbs = append(dbs, queryDB(idx))
- }
- case "hide":
- hiddenList := coll.NewSet(selConn.DBFilterList...)
- for idx := 0; idx < totaldb; idx++ {
- if !hiddenList.Contains(idx) {
- dbs = append(dbs, queryDB(idx))
- }
- }
- default:
- for idx := 0; idx < totaldb; idx++ {
- dbs = append(dbs, queryDB(idx))
- }
- }
- }
-
- resp.Success = true
- resp.Data = map[string]any{
- "db": dbs,
- "view": selConn.KeyView,
- }
- return
-}
-
-// CloseConnection close redis server connection
-func (c *connectionService) CloseConnection(name string) (resp types.JSResp) {
- item, ok := c.connMap[name]
- if ok {
- delete(c.connMap, name)
- if item.client != nil {
- item.cancelFunc()
- item.client.Close()
- }
- }
- resp.Success = true
- return
-}
-
-// get a redis client from local cache or create a new open
-// if db >= 0, will also switch to db index
-func (c *connectionService) getRedisClient(connName string, db int) (item connectionItem, err error) {
- var ok bool
- var client redis.UniversalClient
- if item, ok = c.connMap[connName]; ok {
- client = item.client
- } else {
- selConn := c.conns.GetConnection(connName)
- if selConn == nil {
- err = fmt.Errorf("no match connection \"%s\"", connName)
- return
- }
-
- hook := redis2.NewHook(connName, func(cmd string, cost int64) {
- now := time.Now()
- //last := strings.LastIndex(cmd, ":")
- //if last != -1 {
- // cmd = cmd[:last]
- //}
- c.cmdHistory = append(c.cmdHistory, cmdHistoryItem{
- Timestamp: now.UnixMilli(),
- Server: connName,
- Cmd: cmd,
- Cost: cost,
- })
- })
-
- client, err = c.createRedisClient(selConn.ConnectionConfig)
- if err != nil {
- err = fmt.Errorf("create conenction error: %s", err.Error())
- return
- }
- // add hook to each node in cluster mode
- var cluster *redis.ClusterClient
- if cluster, ok = client.(*redis.ClusterClient); ok {
- err = cluster.ForEachShard(c.ctx, func(ctx context.Context, cli *redis.Client) error {
- cli.AddHook(hook)
- return nil
- })
- if err != nil {
- err = fmt.Errorf("get cluster nodes error: %s", err.Error())
- return
- }
- } else {
- client.AddHook(hook)
- }
-
- if _, err = client.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
- err = errors.New("can not connect to redis server:" + err.Error())
- return
- }
- ctx, cancelFunc := context.WithCancel(c.ctx)
- item = connectionItem{
- client: client,
- ctx: ctx,
- cancelFunc: cancelFunc,
- cursor: map[int]uint64{},
- stepSize: int64(selConn.LoadSize),
- }
- if item.stepSize <= 0 {
- item.stepSize = consts.DEFAULT_LOAD_SIZE
- }
- c.connMap[connName] = item
- }
-
- if db >= 0 {
- var rdb *redis.Client
- if rdb, ok = client.(*redis.Client); ok && rdb != nil {
- if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil {
- return
- }
- }
- }
- return
-}
-
-// save current scan cursor
-func (c *connectionService) setClientCursor(connName string, db int, cursor uint64) {
- if _, ok := c.connMap[connName]; ok {
- if cursor == 0 {
- delete(c.connMap[connName].cursor, db)
- } else {
- c.connMap[connName].cursor[db] = cursor
- }
- }
-}
-
-// parse command response content which use "redis info"
-// # Keyspace\r\ndb0:keys=2,expires=1,avg_ttl=1877111749\r\ndb1:keys=33,expires=0,avg_ttl=0\r\ndb3:keys=17,expires=0,avg_ttl=0\r\ndb5:keys=3,expires=0,avg_ttl=0\r\n
-func (c *connectionService) parseInfo(info string) map[string]map[string]string {
- parsedInfo := map[string]map[string]string{}
- lines := strings.Split(info, "\r\n")
- if len(lines) > 0 {
- var subInfo map[string]string
- for _, line := range lines {
- if strings.HasPrefix(line, "#") {
- subInfo = map[string]string{}
- parsedInfo[strings.TrimSpace(strings.TrimLeft(line, "#"))] = subInfo
- } else {
- items := strings.SplitN(line, ":", 2)
- if len(items) < 2 {
- continue
- }
- subInfo[items[0]] = items[1]
- }
- }
- }
- return parsedInfo
-}
-
-// parse db item value, content format like below
-// keys=2,expires=1,avg_ttl=1877111749
-func (c *connectionService) parseDBItemInfo(info string) map[string]int {
- ret := map[string]int{}
- items := strings.Split(info, ",")
- for _, item := range items {
- kv := strings.SplitN(item, "=", 2)
- if len(kv) > 1 {
- ret[kv[0]], _ = strconv.Atoi(kv[1])
- }
- }
- return ret
-}
-
-// ServerInfo get server info
-func (c *connectionService) ServerInfo(name string) (resp types.JSResp) {
- item, err := c.getRedisClient(name, 0)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- // get database info
- res, err := client.Info(ctx).Result()
- if err != nil {
- resp.Msg = "get server info fail:" + err.Error()
- return
- }
-
- resp.Success = true
- resp.Data = c.parseInfo(res)
- return
-}
-
-// OpenDatabase open select database, and list all keys
-// @param path contain connection name and db name
-func (c *connectionService) OpenDatabase(connName string, db int, match string, keyType string) (resp types.JSResp) {
- c.setClientCursor(connName, db, 0)
- return c.LoadNextKeys(connName, db, match, keyType)
-}
-
-// scan keys
-// @return loaded keys
-// @return next cursor
-// @return scan error
-func (c *connectionService) scanKeys(ctx context.Context, client redis.UniversalClient, match, keyType string, cursor uint64, count int64) ([]any, uint64, error) {
- var err error
- filterType := len(keyType) > 0
- scanSize := int64(Preferences().GetScanSize())
- // define sub scan function
- scan := func(ctx context.Context, cli redis.UniversalClient, appendFunc func(k []any)) error {
- var loadedKey []string
- var scanCount int64
- for {
- if filterType {
- loadedKey, cursor, err = cli.ScanType(ctx, cursor, match, scanSize, keyType).Result()
- } else {
- loadedKey, cursor, err = cli.Scan(ctx, cursor, match, scanSize).Result()
- }
- if err != nil {
- return err
- } else {
- ks := sliceutil.Map(loadedKey, func(i int) any {
- return strutil.EncodeRedisKey(loadedKey[i])
- })
- scanCount += int64(len(ks))
- appendFunc(ks)
- }
-
- if (count > 0 && scanCount > count) || cursor == 0 {
- break
- }
- }
- return nil
- }
-
- var keys []any
- if cluster, ok := client.(*redis.ClusterClient); ok {
- // cluster mode
- var mutex sync.Mutex
- err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
- return scan(ctx, cli, func(k []any) {
- mutex.Lock()
- keys = append(keys, k...)
- mutex.Unlock()
- })
- })
- } else {
- err = scan(ctx, client, func(k []any) {
- keys = append(keys, k...)
- })
- }
- if err != nil {
- return nil, cursor, err
- }
- return keys, cursor, nil
-}
-
-// LoadNextKeys load next key from saved cursor
-func (c *connectionService) LoadNextKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx, count := item.client, item.ctx, item.stepSize
- cursor := item.cursor[db]
- keys, cursor, err := c.scanKeys(ctx, client, match, keyType, cursor, count)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- c.setClientCursor(connName, db, cursor)
-
- resp.Success = true
- resp.Data = map[string]any{
- "keys": keys,
- "end": cursor == 0,
- }
- return
-}
-
-// LoadAllKeys load all keys
-func (c *connectionService) LoadAllKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- cursor := item.cursor[db]
- keys, _, err := c.scanKeys(ctx, client, match, keyType, cursor, 0)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- c.setClientCursor(connName, db, 0)
-
- resp.Success = true
- resp.Data = map[string]any{
- "keys": keys,
- }
- return
-}
-
-// GetKeyValue get value by key
-func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- var keyType string
- var dur time.Duration
- keyType, err = client.Type(ctx, key).Result()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- if keyType == "none" {
- resp.Msg = "key not exists"
- return
- }
-
- var ttl int64
- if dur, err = client.TTL(ctx, key).Result(); err != nil {
- ttl = -1
- } else {
- if dur < 0 {
- ttl = -1
- } else {
- ttl = int64(dur.Seconds())
- }
- }
-
- var value any
- var size, length int64
- var cursor uint64
- switch strings.ToLower(keyType) {
- case "string":
- var str string
- str, err = client.Get(ctx, key).Result()
- value, decodeType, viewAs = strutil.ConvertTo(str, decodeType, viewAs)
- length, _ = client.StrLen(ctx, key).Result()
- size, _ = client.MemoryUsage(ctx, key, 0).Result()
- case "list":
- value, err = client.LRange(ctx, key, 0, -1).Result()
- length, _ = client.LLen(ctx, key).Result()
- size, _ = client.MemoryUsage(ctx, key, 0).Result()
- case "hash":
- //value, err = client.HGetAll(ctx, key).Result()
- items := map[string]string{}
- scanSize := int64(Preferences().GetScanSize())
- for {
- var loadedVal []string
- loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", scanSize).Result()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- for i := 0; i < len(loadedVal); i += 2 {
- items[loadedVal[i]] = loadedVal[i+1]
- }
- if cursor == 0 {
- break
- }
- }
- value = items
- length, _ = client.HLen(ctx, key).Result()
- size, _ = client.MemoryUsage(ctx, key, 0).Result()
- case "set":
- //value, err = client.SMembers(ctx, key).Result()
- items := []string{}
- scanSize := int64(Preferences().GetScanSize())
- for {
- var loadedKey []string
- loadedKey, cursor, err = client.SScan(ctx, key, cursor, "*", scanSize).Result()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- items = append(items, loadedKey...)
- if cursor == 0 {
- break
- }
- }
- value = items
- length, _ = client.SCard(ctx, key).Result()
- size, _ = client.MemoryUsage(ctx, key, 0).Result()
- case "zset":
- //value, err = client.ZRangeWithScores(ctx, key, 0, -1).Result()
- var items []types.ZSetItem
- scanSize := int64(Preferences().GetScanSize())
- for {
- var loadedVal []string
- loadedVal, cursor, err = client.ZScan(ctx, key, cursor, "*", scanSize).Result()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- var score float64
- for i := 0; i < len(loadedVal); i += 2 {
- if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
- items = append(items, types.ZSetItem{
- Value: loadedVal[i],
- Score: score,
- })
- }
- }
- if cursor == 0 {
- break
- }
- }
- value = items
- length, _ = client.ZCard(ctx, key).Result()
- size, _ = client.MemoryUsage(ctx, key, 0).Result()
- case "stream":
- var msgs []redis.XMessage
- items := []types.StreamItem{}
- msgs, err = client.XRevRange(ctx, key, "+", "-").Result()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- for _, msg := range msgs {
- items = append(items, types.StreamItem{
- ID: msg.ID,
- Value: msg.Values,
- })
- }
- value = items
- length, _ = client.XLen(ctx, key).Result()
- size, _ = client.MemoryUsage(ctx, key, 0).Result()
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- resp.Success = true
- resp.Data = map[string]any{
- "type": keyType,
- "ttl": ttl,
- "value": value,
- "size": size,
- "length": length,
- "viewAs": viewAs,
- "decode": decodeType,
- }
- return
-}
-
-// SetKeyValue set value by key
-// @param ttl <= 0 means keep current ttl
-func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType string, value any, ttl int64, viewAs, decode string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- var expiration time.Duration
- if ttl < 0 {
- if expiration, err = client.PTTL(ctx, key).Result(); err != nil {
- expiration = redis.KeepTTL
- }
- } else {
- expiration = time.Duration(ttl) * time.Second
- }
- switch strings.ToLower(keyType) {
- case "string":
- if str, ok := value.(string); !ok {
- resp.Msg = "invalid string value"
- return
- } else {
- var saveStr string
- if saveStr, err = strutil.SaveAs(str, viewAs, decode); err != nil {
- resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error())
- return
- }
- _, err = client.Set(ctx, key, saveStr, 0).Result()
- // set expiration lonely, not "keepttl"
- if err == nil && expiration > 0 {
- client.Expire(ctx, key, expiration)
- }
- }
- case "list":
- if strs, ok := value.([]any); !ok {
- resp.Msg = "invalid list value"
- return
- } else {
- err = client.LPush(ctx, key, strs...).Err()
- if err == nil && expiration > 0 {
- client.Expire(ctx, key, expiration)
- }
- }
- case "hash":
- if strs, ok := value.([]any); !ok {
- resp.Msg = "invalid hash value"
- return
- } else {
- total := len(strs)
- if total > 1 {
- _, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
- for i := 0; i < total; i += 2 {
- pipe.HSet(ctx, key, strs[i], strs[i+1])
- }
- if expiration > 0 {
- pipe.Expire(ctx, key, expiration)
- }
- return nil
- })
- }
- }
- case "set":
- if strs, ok := value.([]any); !ok || len(strs) <= 0 {
- resp.Msg = "invalid set value"
- return
- } else {
- if len(strs) > 0 {
- err = client.SAdd(ctx, key, strs...).Err()
- if err == nil && expiration > 0 {
- client.Expire(ctx, key, expiration)
- }
- }
- }
- case "zset":
- if strs, ok := value.([]any); !ok || len(strs) <= 0 {
- resp.Msg = "invalid zset value"
- return
- } else {
- if len(strs) > 1 {
- var members []redis.Z
- for i := 0; i < len(strs); i += 2 {
- score, _ := strconv.ParseFloat(strs[i+1].(string), 64)
- members = append(members, redis.Z{
- Score: score,
- Member: strs[i],
- })
- }
- err = client.ZAdd(ctx, key, members...).Err()
- if err == nil && expiration > 0 {
- client.Expire(ctx, key, expiration)
- }
- }
- }
- case "stream":
- if strs, ok := value.([]any); !ok {
- resp.Msg = "invalid stream value"
- return
- } else {
- if len(strs) > 2 {
- err = client.XAdd(ctx, &redis.XAddArgs{
- Stream: key,
- ID: strs[0].(string),
- Values: strs[1:],
- }).Err()
- if err == nil && expiration > 0 {
- client.Expire(ctx, key, expiration)
- }
- }
- }
- }
-
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- resp.Success = true
- resp.Data = map[string]any{
- "value": value,
- }
- return
-}
-
-// SetHashValue set hash field
-func (c *connectionService) SetHashValue(connName string, db int, k any, field, newField, value string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- var removedField []string
- updatedField := map[string]string{}
- if len(field) <= 0 {
- // old filed is empty, add new field
- _, err = client.HSet(ctx, key, newField, value).Result()
- updatedField[newField] = value
- } else if len(newField) <= 0 {
- // new field is empty, delete old field
- _, err = client.HDel(ctx, key, field, value).Result()
- removedField = append(removedField, field)
- } else if field == newField {
- // replace field
- _, err = client.HSet(ctx, key, newField, value).Result()
- updatedField[newField] = value
- } else {
- // remove old field and add new field
- if _, err = client.HDel(ctx, key, field).Result(); err != nil {
- resp.Msg = err.Error()
- return
- }
- _, err = client.HSet(ctx, key, newField, value).Result()
- removedField = append(removedField, field)
- updatedField[newField] = value
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- resp.Data = map[string]any{
- "removed": removedField,
- "updated": updatedField,
- }
- return
-}
-
-// AddHashField add or update hash field
-func (c *connectionService) AddHashField(connName string, db int, k any, action int, fieldItems []any) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- updated := map[string]any{}
- switch action {
- case 1:
- // ignore duplicated fields
- for i := 0; i < len(fieldItems); i += 2 {
- _, err = client.HSetNX(ctx, key, fieldItems[i].(string), fieldItems[i+1]).Result()
- if err == nil {
- updated[fieldItems[i].(string)] = fieldItems[i+1]
- }
- }
- default:
- // overwrite duplicated fields
- total := len(fieldItems)
- if total > 1 {
- _, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
- for i := 0; i < total; i += 2 {
- client.HSet(ctx, key, fieldItems[i], fieldItems[i+1])
- }
- return nil
- })
- for i := 0; i < total; i += 2 {
- updated[fieldItems[i].(string)] = fieldItems[i+1]
- }
- }
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- resp.Data = map[string]any{
- "updated": updated,
- }
- return
-}
-
-// AddListItem add item to list or remove from it
-func (c *connectionService) AddListItem(connName string, db int, k any, action int, items []any) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- var leftPush, rightPush []any
- switch action {
- case 0:
- // push to head
- _, err = client.LPush(ctx, key, items...).Result()
- leftPush = append(leftPush, items...)
- default:
- // append to tail
- _, err = client.RPush(ctx, key, items...).Result()
- rightPush = append(rightPush, items...)
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- resp.Data = map[string]any{
- "left": leftPush,
- "right": rightPush,
- }
- return
-}
-
-// SetListItem update or remove list item by index
-func (c *connectionService) SetListItem(connName string, db int, k any, index int64, value string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- var removed []int64
- updated := map[int64]string{}
- if len(value) <= 0 {
- // remove from list
- err = client.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- err = client.LRem(ctx, key, 1, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- removed = append(removed, index)
- } else {
- // replace index value
- err = client.LSet(ctx, key, index, value).Err()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- updated[index] = value
- }
-
- resp.Success = true
- resp.Data = map[string]any{
- "removed": removed,
- "updated": updated,
- }
- return
-}
-
-// SetSetItem add members to set or remove from set
-func (c *connectionService) SetSetItem(connName string, db int, k any, remove bool, members []any) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- if remove {
- _, err = client.SRem(ctx, key, members...).Result()
- } else {
- _, err = client.SAdd(ctx, key, members...).Result()
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- return
-}
-
-// UpdateSetItem replace member of set
-func (c *connectionService) UpdateSetItem(connName string, db int, k any, value, newValue string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- _, _ = client.SRem(ctx, key, value).Result()
- _, err = client.SAdd(ctx, key, newValue).Result()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- return
-}
-
-// UpdateZSetValue update value of sorted set member
-func (c *connectionService) UpdateZSetValue(connName string, db int, k any, value, newValue string, score float64) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- updated := map[string]any{}
- var removed []string
- if len(newValue) <= 0 {
- // blank new value, delete value
- _, err = client.ZRem(ctx, key, value).Result()
- if err == nil {
- removed = append(removed, value)
- }
- } else if newValue == value {
- // update score only
- _, err = client.ZAdd(ctx, key, redis.Z{
- Score: score,
- Member: value,
- }).Result()
- } else {
- // remove old value and add new one
- _, err = client.ZRem(ctx, key, value).Result()
- if err == nil {
- removed = append(removed, value)
- }
-
- _, err = client.ZAdd(ctx, key, redis.Z{
- Score: score,
- Member: newValue,
- }).Result()
- if err == nil {
- updated[newValue] = score
- }
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- resp.Data = map[string]any{
- "updated": updated,
- "removed": removed,
- }
- return
-}
-
-// AddZSetValue add item to sorted set
-func (c *connectionService) AddZSetValue(connName string, db int, k any, action int, valueScore map[string]float64) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- members := maputil.ToSlice(valueScore, func(k string) redis.Z {
- return redis.Z{
- Score: valueScore[k],
- Member: k,
- }
- })
-
- switch action {
- case 1:
- // ignore duplicated fields
- _, err = client.ZAddNX(ctx, key, members...).Result()
- default:
- // overwrite duplicated fields
- _, err = client.ZAdd(ctx, key, members...).Result()
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- return
-}
-
-// AddStreamValue add stream field
-func (c *connectionService) AddStreamValue(connName string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- _, err = client.XAdd(ctx, &redis.XAddArgs{
- Stream: key,
- ID: ID,
- Values: fieldItems,
- }).Result()
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- return
-}
-
-// RemoveStreamValues remove stream values by id
-func (c *connectionService) RemoveStreamValues(connName string, db int, k any, IDs []string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- _, err = client.XDel(ctx, key, IDs...).Result()
- resp.Success = true
- return
-}
-
-// SetKeyTTL set ttl of key
-func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- var expiration time.Duration
- if ttl < 0 {
- if err = client.Persist(ctx, key).Err(); err != nil {
- resp.Msg = err.Error()
- return
- }
- } else {
- expiration = time.Duration(ttl) * time.Second
- if err = client.Expire(ctx, key, expiration).Err(); err != nil {
- resp.Msg = err.Error()
- return
- }
- }
-
- resp.Success = true
- return
-}
-
-// DeleteKey remove redis key
-func (c *connectionService) DeleteKey(connName string, db int, k any, async bool) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- key := strutil.DecodeRedisKey(k)
- var deletedKeys []string
- if strings.HasSuffix(key, "*") {
- // delete by prefix
- var mutex sync.Mutex
- del := func(ctx context.Context, cli redis.UniversalClient) error {
- handleDel := func(ks []string) error {
- pipe := cli.Pipeline()
- for _, k2 := range ks {
- if async {
- cli.Unlink(ctx, k2)
- } else {
- cli.Del(ctx, k2)
- }
- }
- pipe.Exec(ctx)
-
- mutex.Lock()
- deletedKeys = append(deletedKeys, ks...)
- mutex.Unlock()
-
- return nil
- }
-
- scanSize := int64(Preferences().GetScanSize())
- iter := cli.Scan(ctx, 0, key, scanSize).Iterator()
- resultKeys := make([]string, 0, 100)
- for iter.Next(ctx) {
- resultKeys = append(resultKeys, iter.Val())
- if len(resultKeys) >= 3 {
- handleDel(resultKeys)
- resultKeys = resultKeys[:0:cap(resultKeys)]
- }
- }
-
- if len(resultKeys) > 0 {
- handleDel(resultKeys)
- }
- return nil
- }
-
- if cluster, ok := client.(*redis.ClusterClient); ok {
- // cluster mode
- err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
- return del(ctx, cli)
- })
- } else {
- err = del(ctx, client)
- }
-
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- } else {
- // delete key only
- if async {
- if _, err = client.Unlink(ctx, key).Result(); err != nil {
- resp.Msg = err.Error()
- return
- }
- } else {
- if _, err = client.Del(ctx, key).Result(); err != nil {
- resp.Msg = err.Error()
- return
- }
- }
- deletedKeys = append(deletedKeys, key)
- }
-
- resp.Success = true
- resp.Data = map[string]any{
- "deleted": deletedKeys,
- }
- return
-}
-
-// FlushDB flush database
-func (c *connectionService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- flush := func(ctx context.Context, cli redis.UniversalClient) {
- cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
- pipe.Select(ctx, db)
- if async {
- pipe.FlushDBAsync(ctx)
- } else {
- pipe.FlushDB(ctx)
- }
- return nil
- })
- }
-
- client, ctx := item.client, item.ctx
- if cluster, ok := client.(*redis.ClusterClient); ok {
- // cluster mode
- err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
- flush(ctx, cli)
- return nil
- })
- } else {
- flush(ctx, client)
- }
-
- if err != nil {
- resp.Msg = err.Error()
- return
- }
- resp.Success = true
- return
-}
-
-// RenameKey rename key
-func (c *connectionService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- if _, ok := client.(*redis.ClusterClient); ok {
- resp.Msg = "RENAME not support in cluster mode yet"
- return
- }
-
- if _, err = client.RenameNX(ctx, key, newKey).Result(); err != nil {
- resp.Msg = err.Error()
- return
- }
-
- resp.Success = true
- return
-}
-
-// GetCmdHistory get redis command history
-func (c *connectionService) GetCmdHistory(pageNo, pageSize int) (resp types.JSResp) {
- resp.Success = true
- if pageSize <= 0 || pageNo <= 0 {
- // return all history
- resp.Data = map[string]any{
- "list": c.cmdHistory,
- "pageNo": 1,
- "pageSize": -1,
- }
- } else {
- total := len(c.cmdHistory)
- startIndex := total / pageSize * (pageNo - 1)
- endIndex := min(startIndex+pageSize, total)
- resp.Data = map[string]any{
- "list": c.cmdHistory[startIndex:endIndex],
- "pageNo": pageNo,
- "pageSize": pageSize,
- }
- }
- return
-}
-
-// CleanCmdHistory clean redis command history
-func (c *connectionService) CleanCmdHistory() (resp types.JSResp) {
- c.cmdHistory = []cmdHistoryItem{}
- resp.Success = true
- return
-}
-
-// GetSlowLogs get slow log list
-func (c *connectionService) GetSlowLogs(connName string, db int, num int64) (resp types.JSResp) {
- item, err := c.getRedisClient(connName, db)
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- client, ctx := item.client, item.ctx
- var logs []redis.SlowLog
- if cluster, ok := client.(*redis.ClusterClient); ok {
- // cluster mode
- var mu sync.Mutex
- err = cluster.ForEachShard(ctx, func(ctx context.Context, cli *redis.Client) error {
- if subLogs, _ := client.SlowLogGet(ctx, num).Result(); len(subLogs) > 0 {
- mu.Lock()
- logs = append(logs, subLogs...)
- mu.Unlock()
- }
- return nil
- })
- } else {
- logs, err = client.SlowLogGet(ctx, num).Result()
- }
- if err != nil {
- resp.Msg = err.Error()
- return
- }
-
- sort.Slice(logs, func(i, j int) bool {
- return logs[i].Time.UnixMilli() > logs[j].Time.UnixMilli()
- })
- if len(logs) > int(num) {
- logs = logs[:num]
- }
-
- list := sliceutil.Map(logs, func(i int) slowLogItem {
- var name string
- var e error
- if name, e = url.QueryUnescape(logs[i].ClientName); e != nil {
- name = logs[i].ClientName
- }
- return slowLogItem{
- Timestamp: logs[i].Time.UnixMilli(),
- Client: name,
- Addr: logs[i].ClientAddr,
- Cmd: sliceutil.JoinString(logs[i].Args, " "),
- Cost: logs[i].Duration.Milliseconds(),
- }
- })
-
- resp.Success = true
- resp.Data = map[string]any{
- "list": list,
- }
- return
-}
-
-// update or insert key info to database
-//func (c *connectionService) updateDBKey(connName string, db int, keys []string, separator string) {
-// dbStruct := map[string]any{}
-// for _, key := range keys {
-// keyPart := strings.Split(key, separator)
-// prefixLen := len(keyPart)-1
-// if prefixLen > 0 {
-// for i := 0; i < prefixLen; i++ {
-// if dbStruct[keyPart[i]]
-// keyPart[i]
-// }
-// }
-// log.Println("key", key)
-// }
-//}
diff --git a/backend/utils/string/common.go b/backend/utils/string/common.go
index e0746a9..11ab00d 100644
--- a/backend/utils/string/common.go
+++ b/backend/utils/string/common.go
@@ -14,7 +14,7 @@ func containsBinary(str string) bool {
//}
rs := []rune(str)
for _, r := range rs {
- if !unicode.IsPrint(r) {
+ if !unicode.IsPrint(r) && r != '\n' {
return true
}
}
diff --git a/frontend/src/AppContent.vue b/frontend/src/AppContent.vue
index 0f742d0..31490fa 100644
--- a/frontend/src/AppContent.vue
+++ b/frontend/src/AppContent.vue
@@ -9,7 +9,6 @@ import ConnectionPane from './components/sidebar/ConnectionPane.vue'
import ContentServerPane from './components/content/ContentServerPane.vue'
import useTabStore from './stores/tab.js'
import usePreferencesStore from './stores/preferences.js'
-import useConnectionStore from './stores/connections.js'
import ContentLogPane from './components/content/ContentLogPane.vue'
import ContentValueTab from '@/components/content/ContentValueTab.vue'
import ToolbarControlWidget from '@/components/common/ToolbarControlWidget.vue'
@@ -32,7 +31,6 @@ const data = reactive({
const tabStore = useTabStore()
const prefStore = usePreferencesStore()
-const connectionStore = useConnectionStore()
const logPaneRef = ref(null)
const exThemeVars = computed(() => {
return extraTheme(prefStore.isDark)
diff --git a/frontend/src/components/content/ContentLogPane.vue b/frontend/src/components/content/ContentLogPane.vue
index def1835..9b3204e 100644
--- a/frontend/src/components/content/ContentLogPane.vue
+++ b/frontend/src/components/content/ContentLogPane.vue
@@ -2,16 +2,16 @@
import { computed, h, nextTick, reactive, ref } from 'vue'
import IconButton from '@/components/common/IconButton.vue'
import Refresh from '@/components/icons/Refresh.vue'
-import useConnectionStore from 'stores/connections.js'
import { map, size, split, uniqBy } from 'lodash'
import { useI18n } from 'vue-i18n'
import Delete from '@/components/icons/Delete.vue'
import dayjs from 'dayjs'
import { useThemeVars } from 'naive-ui'
+import useBrowserStore from 'stores/browser.js'
const themeVars = useThemeVars()
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const i18n = useI18n()
const data = reactive({
loading: false,
@@ -36,7 +36,7 @@ const tableRef = ref(null)
const loadHistory = () => {
data.loading = true
- connectionStore
+ browserStore
.getCmdHistory()
.then((list) => {
data.history = list || []
@@ -50,7 +50,7 @@ const loadHistory = () => {
const cleanHistory = async () => {
$dialog.warning(i18n.t('log.confirm_clean_log'), () => {
data.loading = true
- connectionStore
+ browserStore
.cleanCmdHistory()
.then((success) => {
if (success) {
diff --git a/frontend/src/components/content/ContentValueTab.vue b/frontend/src/components/content/ContentValueTab.vue
index 4fe9163..c4a0857 100644
--- a/frontend/src/components/content/ContentValueTab.vue
+++ b/frontend/src/components/content/ContentValueTab.vue
@@ -8,6 +8,7 @@ import { useThemeVars } from 'naive-ui'
import useConnectionStore from 'stores/connections.js'
import { extraTheme } from '@/utils/extra_theme.js'
import usePreferencesStore from 'stores/preferences.js'
+import useBrowserStore from 'stores/browser.js'
/**
* Value content tab on head
@@ -17,13 +18,14 @@ const themeVars = useThemeVars()
const i18n = useI18n()
const tabStore = useTabStore()
const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const prefStore = usePreferencesStore()
const onCloseTab = (tabIndex) => {
const tab = get(tabStore.tabs, tabIndex)
if (tab != null) {
$dialog.warning(i18n.t('dialogue.close_confirm', { name: tab.name }), () => {
- connectionStore.closeConnection(tab.name)
+ browserStore.closeConnection(tab.name)
})
}
}
diff --git a/frontend/src/components/content_value/ContentServerStatus.vue b/frontend/src/components/content_value/ContentServerStatus.vue
index da0d904..9337202 100644
--- a/frontend/src/components/content_value/ContentServerStatus.vue
+++ b/frontend/src/components/content_value/ContentServerStatus.vue
@@ -4,13 +4,13 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import IconButton from '@/components/common/IconButton.vue'
import Filter from '@/components/icons/Filter.vue'
import Refresh from '@/components/icons/Refresh.vue'
-import useConnectionStore from 'stores/connections.js'
+import useBrowserStore from 'stores/browser.js'
const props = defineProps({
server: String,
})
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const serverInfo = ref({})
const autoRefresh = ref(false)
const loading = ref(false) // loading status for refresh
@@ -27,9 +27,9 @@ const refreshInfo = async (force) => {
} else {
autoLoading.value = true
}
- if (!isEmpty(props.server) && connectionStore.isConnected(props.server)) {
+ if (!isEmpty(props.server) && browserStore.isConnected(props.server)) {
try {
- serverInfo.value = await connectionStore.getServerInfo(props.server)
+ serverInfo.value = await browserStore.getServerInfo(props.server)
} finally {
loading.value = false
autoLoading.value = false
diff --git a/frontend/src/components/content_value/ContentSlog.vue b/frontend/src/components/content_value/ContentSlog.vue
index c1d1982..68b4021 100644
--- a/frontend/src/components/content_value/ContentSlog.vue
+++ b/frontend/src/components/content_value/ContentSlog.vue
@@ -1,15 +1,15 @@
diff --git a/frontend/src/components/content_value/ContentValueZSet.vue b/frontend/src/components/content_value/ContentValueZSet.vue
index 8fac7c7..8e90664 100644
--- a/frontend/src/components/content_value/ContentValueZSet.vue
+++ b/frontend/src/components/content_value/ContentValueZSet.vue
@@ -8,9 +8,9 @@ import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import { isEmpty } from 'lodash'
import useDialogStore from 'stores/dialog.js'
-import useConnectionStore from 'stores/connections.js'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
+import useBrowserStore from 'stores/browser.js'
const i18n = useI18n()
const themeVars = useThemeVars()
@@ -60,7 +60,7 @@ const filterOption = [
]
const filterType = ref(1)
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const dialogStore = useDialogStore()
const keyType = redisTypes.ZSET
const currentEditRow = ref({
@@ -171,14 +171,14 @@ const actionColumn = {
},
onDelete: async () => {
try {
- const { success, msg } = await connectionStore.removeZSetItem(
+ const { success, msg } = await browserStore.removeZSetItem(
props.name,
props.db,
keyName.value,
row.value,
)
if (success) {
- connectionStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
+ browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.value }))
} else {
$message.error(msg)
@@ -194,7 +194,7 @@ const actionColumn = {
$message.error(i18n.t('dialogue.spec_field_required', { key: i18n.t('common.value') }))
return
}
- const { success, msg } = await connectionStore.updateZSetItem(
+ const { success, msg } = await browserStore.updateZSetItem(
props.name,
props.db,
keyName.value,
@@ -203,7 +203,7 @@ const actionColumn = {
currentEditRow.value.score,
)
if (success) {
- connectionStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
+ browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ'))
} else {
$message.error(msg)
diff --git a/frontend/src/components/dialogs/AddFieldsDialog.vue b/frontend/src/components/dialogs/AddFieldsDialog.vue
index d9bb3b2..f6835ff 100644
--- a/frontend/src/components/dialogs/AddFieldsDialog.vue
+++ b/frontend/src/components/dialogs/AddFieldsDialog.vue
@@ -8,9 +8,9 @@ import { useI18n } from 'vue-i18n'
import AddListValue from '@/components/new_value/AddListValue.vue'
import AddHashValue from '@/components/new_value/AddHashValue.vue'
import AddZSetValue from '@/components/new_value/AddZSetValue.vue'
-import useConnectionStore from 'stores/connections.js'
import NewStreamValue from '@/components/new_value/NewStreamValue.vue'
import { isEmpty, size, slice } from 'lodash'
+import useBrowserStore from 'stores/browser.js'
const i18n = useI18n()
const newForm = reactive({
@@ -78,7 +78,7 @@ watch(
},
)
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const onAdd = async () => {
try {
const { server, db, key, keyCode, type } = newForm
@@ -92,14 +92,14 @@ const onAdd = async () => {
{
let data
if (newForm.opType === 1) {
- data = await connectionStore.prependListItem(server, db, keyName, value)
+ data = await browserStore.prependListItem(server, db, keyName, value)
} else {
- data = await connectionStore.appendListItem(server, db, keyName, value)
+ data = await browserStore.appendListItem(server, db, keyName, value)
}
const { success, msg } = data
if (success) {
if (newForm.reload) {
- connectionStore.loadKeyValue(server, db, keyName).then(() => {})
+ browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ'))
} else {
@@ -110,16 +110,10 @@ const onAdd = async () => {
case types.HASH:
{
- const { success, msg } = await connectionStore.addHashField(
- server,
- db,
- keyName,
- newForm.opType,
- value,
- )
+ const { success, msg } = await browserStore.addHashField(server, db, keyName, newForm.opType, value)
if (success) {
if (newForm.reload) {
- connectionStore.loadKeyValue(server, db, keyName).then(() => {})
+ browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ'))
} else {
@@ -130,10 +124,10 @@ const onAdd = async () => {
case types.SET:
{
- const { success, msg } = await connectionStore.addSetItem(server, db, keyName, value)
+ const { success, msg } = await browserStore.addSetItem(server, db, keyName, value)
if (success) {
if (newForm.reload) {
- connectionStore.loadKeyValue(server, db, keyName).then(() => {})
+ browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ'))
} else {
@@ -144,16 +138,10 @@ const onAdd = async () => {
case types.ZSET:
{
- const { success, msg } = await connectionStore.addZSetItem(
- server,
- db,
- keyName,
- newForm.opType,
- value,
- )
+ const { success, msg } = await browserStore.addZSetItem(server, db, keyName, newForm.opType, value)
if (success) {
if (newForm.reload) {
- connectionStore.loadKeyValue(server, db, keyName).then(() => {})
+ browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ'))
} else {
@@ -165,7 +153,7 @@ const onAdd = async () => {
case types.STREAM:
{
if (size(value) > 2) {
- const { success, msg } = await connectionStore.addStreamValue(
+ const { success, msg } = await browserStore.addStreamValue(
server,
db,
keyName,
@@ -174,7 +162,7 @@ const onAdd = async () => {
)
if (success) {
if (newForm.reload) {
- connectionStore.loadKeyValue(server, db, keyName).then(() => {})
+ browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ'))
} else {
diff --git a/frontend/src/components/dialogs/ConnectionDialog.vue b/frontend/src/components/dialogs/ConnectionDialog.vue
index 9d3c039..e428ddd 100644
--- a/frontend/src/components/dialogs/ConnectionDialog.vue
+++ b/frontend/src/components/dialogs/ConnectionDialog.vue
@@ -9,6 +9,7 @@ import useConnectionStore from 'stores/connections.js'
import FileOpenInput from '@/components/common/FileOpenInput.vue'
import { KeyViewType } from '@/consts/key_view_type.js'
import { useThemeVars } from 'naive-ui'
+import useBrowserStore from 'stores/browser.js'
/**
* Dialog for new or edit connection
@@ -17,6 +18,7 @@ import { useThemeVars } from 'naive-ui'
const themeVars = useThemeVars()
const dialogStore = useDialog()
const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const i18n = useI18n()
const editName = ref('')
@@ -45,7 +47,7 @@ const closingConnection = computed(() => {
if (isEmpty(editName.value)) {
return false
}
- return connectionStore.isConnected(editName.value)
+ return browserStore.isConnected(editName.value)
})
const groupOptions = computed(() => {
diff --git a/frontend/src/components/dialogs/DeleteKeyDialog.vue b/frontend/src/components/dialogs/DeleteKeyDialog.vue
index 9576876..7cb7203 100644
--- a/frontend/src/components/dialogs/DeleteKeyDialog.vue
+++ b/frontend/src/components/dialogs/DeleteKeyDialog.vue
@@ -2,8 +2,8 @@
import { reactive, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
-import useConnectionStore from 'stores/connections.js'
import { isEmpty, size } from 'lodash'
+import useBrowserStore from 'stores/browser.js'
const deleteForm = reactive({
server: '',
@@ -16,7 +16,7 @@ const deleteForm = reactive({
})
const dialogStore = useDialog()
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
watch(
() => dialogStore.deleteKeyDialogVisible,
(visible) => {
@@ -36,7 +36,7 @@ watch(
const scanAffectedKey = async () => {
try {
deleteForm.loadingAffected = true
- const { keys = [] } = await connectionStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
+ const { keys = [] } = await browserStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
deleteForm.affectedKeys = keys || []
deleteForm.showAffected = true
} finally {
@@ -53,7 +53,7 @@ const i18n = useI18n()
const onConfirmDelete = async () => {
try {
const { server, db, key, async } = deleteForm
- const success = await connectionStore.deleteKeyPrefix(server, db, key, async)
+ const success = await browserStore.deleteKeyPrefix(server, db, key, async)
if (success) {
$message.success(i18n.t('dialogue.handle_succ'))
}
diff --git a/frontend/src/components/dialogs/FlushDbDialog.vue b/frontend/src/components/dialogs/FlushDbDialog.vue
index 4c1042b..974be7b 100644
--- a/frontend/src/components/dialogs/FlushDbDialog.vue
+++ b/frontend/src/components/dialogs/FlushDbDialog.vue
@@ -2,7 +2,7 @@
import { reactive, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
-import useConnectionStore from 'stores/connections.js'
+import useBrowserStore from 'stores/browser.js'
const flushForm = reactive({
server: '',
@@ -13,7 +13,7 @@ const flushForm = reactive({
})
const dialogStore = useDialog()
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
watch(
() => dialogStore.flushDBDialogVisible,
(visible) => {
@@ -31,7 +31,7 @@ const i18n = useI18n()
const onConfirmFlush = async () => {
try {
const { server, db, async } = flushForm
- const success = await connectionStore.flushDatabase(server, db, async)
+ const success = await browserStore.flushDatabase(server, db, async)
if (success) {
$message.success(i18n.t('dialogue.handle_succ'))
}
diff --git a/frontend/src/components/dialogs/KeyFilterDialog.vue b/frontend/src/components/dialogs/KeyFilterDialog.vue
index 4aa8745..65382a0 100644
--- a/frontend/src/components/dialogs/KeyFilterDialog.vue
+++ b/frontend/src/components/dialogs/KeyFilterDialog.vue
@@ -2,8 +2,8 @@
import { computed, reactive, ref, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
-import useConnectionStore from 'stores/connections.js'
import { types } from '@/consts/support_redis_type.js'
+import useBrowserStore from 'stores/browser.js'
const i18n = useI18n()
const filterForm = reactive({
@@ -39,11 +39,11 @@ watch(
},
)
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const onConfirm = () => {
const { server, db, type, pattern } = filterForm
- connectionStore.setKeyFilter(server, db, pattern, type)
- connectionStore.reopenDatabase(server, db)
+ browserStore.setKeyFilter(server, db, pattern, type)
+ browserStore.reopenDatabase(server, db)
}
const onClose = () => {
diff --git a/frontend/src/components/dialogs/NewKeyDialog.vue b/frontend/src/components/dialogs/NewKeyDialog.vue
index 7187f9e..58e5696 100644
--- a/frontend/src/components/dialogs/NewKeyDialog.vue
+++ b/frontend/src/components/dialogs/NewKeyDialog.vue
@@ -10,10 +10,10 @@ import NewListValue from '@/components/new_value/NewListValue.vue'
import NewZSetValue from '@/components/new_value/NewZSetValue.vue'
import NewSetValue from '@/components/new_value/NewSetValue.vue'
import { useI18n } from 'vue-i18n'
-import useConnectionStore from 'stores/connections.js'
import { NSpace } from 'naive-ui'
import useTabStore from 'stores/tab.js'
import NewStreamValue from '@/components/new_value/NewStreamValue.vue'
+import useBrowserStore from 'stores/browser.js'
const i18n = useI18n()
const newForm = reactive({
@@ -33,7 +33,7 @@ const formRules = computed(() => {
}
})
const dbOptions = computed(() =>
- map(keys(connectionStore.databases[newForm.server]), (key) => ({
+ map(keys(browserStore.databases[newForm.server]), (key) => ({
label: key,
value: parseInt(key),
})),
@@ -101,7 +101,7 @@ const renderTypeLabel = (option) => {
)
}
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const tabStore = useTabStore()
const onAdd = async () => {
await newFormRef.value?.validate().catch((err) => {
@@ -117,7 +117,7 @@ const onAdd = async () => {
if (value == null) {
value = defaultValue[type]
}
- const { success, msg, nodeKey } = await connectionStore.setKey(
+ const { success, msg, nodeKey } = await browserStore.setKey(
server,
db,
key,
@@ -130,7 +130,7 @@ const onAdd = async () => {
if (success) {
// select current key
tabStore.setSelectedKeys(server, nodeKey)
- connectionStore.loadKeyValue(server, db, key).then(() => {})
+ browserStore.loadKeyValue(server, db, key).then(() => {})
} else if (!isEmpty(msg)) {
$message.error(msg)
}
diff --git a/frontend/src/components/dialogs/RenameKeyDialog.vue b/frontend/src/components/dialogs/RenameKeyDialog.vue
index a03a4d2..1c94923 100644
--- a/frontend/src/components/dialogs/RenameKeyDialog.vue
+++ b/frontend/src/components/dialogs/RenameKeyDialog.vue
@@ -2,7 +2,7 @@
import { reactive, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
-import useConnectionStore from 'stores/connections.js'
+import useBrowserStore from 'stores/browser.js'
const renameForm = reactive({
server: '',
@@ -12,7 +12,7 @@ const renameForm = reactive({
})
const dialogStore = useDialog()
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
watch(
() => dialogStore.renameDialogVisible,
(visible) => {
@@ -30,9 +30,9 @@ const i18n = useI18n()
const onRename = async () => {
try {
const { server, db, key, newKey } = renameForm
- const { success, msg } = await connectionStore.renameKey(server, db, key, newKey)
+ const { success, msg } = await browserStore.renameKey(server, db, key, newKey)
if (success) {
- await connectionStore.loadKeyValue(server, db, newKey)
+ await browserStore.loadKeyValue(server, db, newKey)
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
diff --git a/frontend/src/components/dialogs/SetTtlDialog.vue b/frontend/src/components/dialogs/SetTtlDialog.vue
index 5c12290..1444771 100644
--- a/frontend/src/components/dialogs/SetTtlDialog.vue
+++ b/frontend/src/components/dialogs/SetTtlDialog.vue
@@ -2,9 +2,9 @@
import { reactive, watch } from 'vue'
import useDialog from 'stores/dialog'
import useTabStore from 'stores/tab.js'
-import useConnectionStore from 'stores/connections.js'
import Binary from '@/components/icons/Binary.vue'
import { isEmpty } from 'lodash'
+import useBrowserStore from 'stores/browser.js'
const ttlForm = reactive({
server: '',
@@ -15,7 +15,7 @@ const ttlForm = reactive({
})
const dialogStore = useDialog()
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const tabStore = useTabStore()
watch(
@@ -51,7 +51,7 @@ const onConfirm = async () => {
return
}
const key = isEmpty(ttlForm.keyCode) ? ttlForm.key : ttlForm.keyCode
- const success = await connectionStore.setTTL(tab.name, tab.db, key, ttlForm.ttl)
+ const success = await browserStore.setTTL(tab.name, tab.db, key, ttlForm.ttl)
if (success) {
tabStore.updateTTL({
server: ttlForm.server,
diff --git a/frontend/src/components/sidebar/BrowserPane.vue b/frontend/src/components/sidebar/BrowserPane.vue
index c5765b6..56442bb 100644
--- a/frontend/src/components/sidebar/BrowserPane.vue
+++ b/frontend/src/components/sidebar/BrowserPane.vue
@@ -8,7 +8,6 @@ import { get } from 'lodash'
import Refresh from '@/components/icons/Refresh.vue'
import useDialogStore from 'stores/dialog.js'
import { useI18n } from 'vue-i18n'
-import useConnectionStore from 'stores/connections.js'
import { types } from '@/consts/support_redis_type.js'
import Search from '@/components/icons/Search.vue'
import Unlink from '@/components/icons/Unlink.vue'
@@ -24,7 +23,6 @@ const onInfo = () => {
}
const i18n = useI18n()
-const connectionStore = useConnectionStore()
const onDisconnect = () => {
browserTreeRef.value?.handleSelectContextMenu('server_close')
}
@@ -55,7 +53,7 @@ const filterTypeOptions = computed(() => {
// const viewType = ref(0)
// const onSwitchView = (selectView) => {
// const { server } = tabStore.currentTab
-// connectionStore.switchKeyView(server, selectView)
+// browserStore.switchKeyView(server, selectView)
// }
diff --git a/frontend/src/components/sidebar/BrowserTree.vue b/frontend/src/components/sidebar/BrowserTree.vue
index fdafb5a..391e25b 100644
--- a/frontend/src/components/sidebar/BrowserTree.vue
+++ b/frontend/src/components/sidebar/BrowserTree.vue
@@ -25,6 +25,7 @@ import IconButton from '@/components/common/IconButton.vue'
import { parseHexColor } from '@/utils/rgb.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
+import useBrowserStore from 'stores/browser.js'
const props = defineProps({
server: String,
@@ -37,6 +38,7 @@ const loading = ref(false)
const loadingConnections = ref(false)
const expandedKeys = ref([props.server])
const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const tabStore = useTabStore()
const dialogStore = useDialogStore()
@@ -53,7 +55,7 @@ const selectedKeys = computed(() => {
})
const data = computed(() => {
- const dbs = get(connectionStore.databases, props.server, [])
+ const dbs = get(browserStore.databases, props.server, [])
return dbs
})
@@ -229,7 +231,7 @@ const handleSelectContextMenu = (key) => {
if (selectedKey == null) {
return
}
- const node = connectionStore.getNode(selectedKey)
+ const node = browserStore.getNode(selectedKey)
const { db = 0, key: nodeKey, redisKey: rk = '', redisKeyCode: rkc, label } = node || {}
const redisKey = rkc || rk
const redisKeyName = !!rkc ? label : redisKey
@@ -241,23 +243,23 @@ const handleSelectContextMenu = (key) => {
case 'server_reload':
expandedKeys.value = [props.server]
tabStore.setSelectedKeys(props.server)
- connectionStore.openConnection(props.server, true).then(() => {
+ browserStore.openConnection(props.server, true).then(() => {
$message.success(i18n.t('dialogue.reload_succ'))
})
break
case 'server_close':
- connectionStore.closeConnection(props.server)
+ browserStore.closeConnection(props.server)
break
case 'db_open':
nextTick().then(() => expandKey(nodeKey))
break
case 'db_reload':
resetExpandKey(props.server, db)
- connectionStore.reopenDatabase(props.server, db)
+ browserStore.reopenDatabase(props.server, db)
break
case 'db_close':
resetExpandKey(props.server, db, true)
- connectionStore.closeDatabase(props.server, db)
+ browserStore.closeDatabase(props.server, db)
break
case 'db_flush':
dialogStore.openFlushDBDialog(props.server, db)
@@ -267,21 +269,21 @@ const handleSelectContextMenu = (key) => {
dialogStore.openNewKeyDialog(redisKey, props.server, db)
break
case 'db_filter':
- const { match: pattern, type } = connectionStore.getKeyFilter(props.server, db)
+ const { match: pattern, type } = browserStore.getKeyFilter(props.server, db)
dialogStore.openKeyFilterDialog(props.server, db, pattern, type)
break
// case 'key_reload':
- // connectionStore.loadKeys(props.server, db, redisKey)
+ // browserStore.loadKeys(props.server, db, redisKey)
// break
case 'value_reload':
- connectionStore.loadKeyValue(props.server, db, redisKey)
+ browserStore.loadKeyValue(props.server, db, redisKey)
break
case 'key_remove':
dialogStore.openDeleteKeyDialog(props.server, db, isEmpty(redisKey) ? '*' : redisKey + ':*')
break
case 'value_remove':
$dialog.warning(i18n.t('dialogue.remove_tip', { name: redisKeyName }), () => {
- connectionStore.deleteKey(props.server, db, redisKey).then((success) => {
+ browserStore.deleteKey(props.server, db, redisKey).then((success) => {
if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: redisKeyName }))
}
@@ -303,7 +305,7 @@ const handleSelectContextMenu = (key) => {
case 'db_loadmore':
if (node != null && !!!node.loading && !!!node.fullLoaded) {
node.loading = true
- connectionStore
+ browserStore
.loadMoreKeys(props.server, db)
.then((end) => {
// fully loaded
@@ -320,7 +322,7 @@ const handleSelectContextMenu = (key) => {
case 'db_loadall':
if (node != null && !!!node.loading) {
node.loading = true
- connectionStore
+ browserStore
.loadAllKeys(props.server, db)
.catch((e) => {
$message.error(e.message)
@@ -376,14 +378,14 @@ const onUpdateSelectedKeys = (keys, options) => {
const { key, db } = node
const redisKey = node.redisKeyCode || node.redisKey
if (!includes(selectedKeys.value, key)) {
- connectionStore.loadKeyValue(props.server, db, redisKey)
+ browserStore.loadKeyValue(props.server, db, redisKey)
}
return
}
}
}
// default is load blank key to display server status
- connectionStore.loadKeyValue(props.server, 0)
+ browserStore.loadKeyValue(props.server, 0)
} finally {
tabStore.setSelectedKeys(props.server, keys)
}
@@ -434,7 +436,7 @@ const renderLabel = ({ option }) => {
return h('b', {}, { default: () => option.label })
case ConnectionType.RedisDB:
const { name: server, db, opened = false } = option
- let { match: matchPattern, type: typeFilter } = connectionStore.getKeyFilter(server, db)
+ let { match: matchPattern, type: typeFilter } = browserStore.getKeyFilter(server, db)
const items = []
if (opened) {
items.push(`${option.label} (${option.keys || 0}/${Math.max(option.maxKeys || 0, option.keys || 0)})`)
@@ -457,8 +459,8 @@ const renderLabel = ({ option }) => {
},
onClose: () => {
// remove type filter
- connectionStore.setKeyFilter(server, db, matchPattern)
- connectionStore.reopenDatabase(server, db)
+ browserStore.setKeyFilter(server, db, matchPattern)
+ browserStore.reopenDatabase(server, db)
},
},
{ default: () => typeFilter },
@@ -476,8 +478,8 @@ const renderLabel = ({ option }) => {
size: 'small',
onClose: () => {
// remove key match pattern
- connectionStore.setKeyFilter(server, db, '*', typeFilter)
- connectionStore.reopenDatabase(server, db)
+ browserStore.setKeyFilter(server, db, '*', typeFilter)
+ browserStore.reopenDatabase(server, db)
},
},
{ default: () => matchPattern },
@@ -652,7 +654,7 @@ const onLoadTree = async (node) => {
case ConnectionType.RedisDB:
loading.value = true
try {
- await connectionStore.openDatabase(props.server, node.db)
+ await browserStore.openDatabase(props.server, node.db)
} catch (e) {
$message.error(e.message)
node.isLeaf = undefined
diff --git a/frontend/src/components/sidebar/ConnectionPane.vue b/frontend/src/components/sidebar/ConnectionPane.vue
index 18cd584..454ffe4 100644
--- a/frontend/src/components/sidebar/ConnectionPane.vue
+++ b/frontend/src/components/sidebar/ConnectionPane.vue
@@ -6,12 +6,10 @@ import AddLink from '@/components/icons/AddLink.vue'
import IconButton from '@/components/common/IconButton.vue'
import Filter from '@/components/icons/Filter.vue'
import ConnectionTree from './ConnectionTree.vue'
-import useConnectionStore from 'stores/connections.js'
import { ref } from 'vue'
const themeVars = useThemeVars()
const dialogStore = useDialogStore()
-const connectionStore = useConnectionStore()
const filterPattern = ref('')
diff --git a/frontend/src/components/sidebar/ConnectionTree.vue b/frontend/src/components/sidebar/ConnectionTree.vue
index 0676fa6..4cc9002 100644
--- a/frontend/src/components/sidebar/ConnectionTree.vue
+++ b/frontend/src/components/sidebar/ConnectionTree.vue
@@ -19,11 +19,13 @@ import Edit from '@/components/icons/Edit.vue'
import { hexGammaCorrection, parseHexColor, toHexColor } from '@/utils/rgb.js'
import IconButton from '@/components/common/IconButton.vue'
import usePreferencesStore from 'stores/preferences.js'
+import useBrowserStore from 'stores/browser.js'
const themeVars = useThemeVars()
const i18n = useI18n()
const connectingServer = ref('')
const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const tabStore = useTabStore()
const prefStore = usePreferencesStore()
const dialogStore = useDialogStore()
@@ -66,7 +68,7 @@ const menuOptions = {
},
],
[ConnectionType.Server]: ({ name }) => {
- const connected = connectionStore.isConnected(name)
+ const connected = browserStore.isConnected(name)
if (connected) {
return [
{
@@ -192,7 +194,7 @@ const renderPrefix = ({ option }) => {
},
)
case ConnectionType.Server:
- const connected = connectionStore.isConnected(option.name)
+ const connected = browserStore.isConnected(option.name)
const color = getServerMarkColor(option.name)
const icon = option.cluster === true ? Cluster : Server
return h(
@@ -265,7 +267,7 @@ const renderSuffix = ({ option }) => {
if (includes(selectedKeys.value, option.key)) {
switch (option.type) {
case ConnectionType.Server:
- const connected = connectionStore.isConnected(option.name)
+ const connected = browserStore.isConnected(option.name)
return renderIconMenu(getServerMenu(connected))
case ConnectionType.Group:
return renderIconMenu(getGroupMenu())
@@ -290,8 +292,8 @@ const onUpdateSelectedKeys = (keys, option) => {
const openConnection = async (name) => {
try {
connectingServer.value = name
- if (!connectionStore.isConnected(name)) {
- await connectionStore.openConnection(name)
+ if (!browserStore.isConnected(name)) {
+ await browserStore.openConnection(name)
}
// check if connection already canceled before finish open
if (!isEmpty(connectingServer.value)) {
@@ -388,9 +390,9 @@ const handleSelectContextMenu = (key) => {
break
case 'server_edit':
// ask for close relevant connections before edit
- if (connectionStore.isConnected(name)) {
+ if (browserStore.isConnected(name)) {
$dialog.warning(i18n.t('dialogue.edit_close_confirm'), () => {
- connectionStore.closeConnection(name)
+ browserStore.closeConnection(name)
dialogStore.openEditDialog(name)
})
} else {
@@ -404,7 +406,7 @@ const handleSelectContextMenu = (key) => {
removeConnection(name)
break
case 'server_close':
- connectionStore.closeConnection(name).then((closed) => {
+ browserStore.closeConnection(name).then((closed) => {
if (closed) {
$message.success(i18n.t('dialogue.handle_succ'))
}
@@ -475,7 +477,7 @@ const handleDrop = ({ node, dragNode, dropPosition }) => {
const onCancelOpen = () => {
if (!isEmpty(connectingServer.value)) {
- connectionStore.closeConnection(connectingServer.value)
+ browserStore.closeConnection(connectingServer.value)
connectingServer.value = ''
}
}
diff --git a/frontend/src/components/sidebar/NavMenu.vue b/frontend/src/components/sidebar/NavMenu.vue
index 444258b..46ac9cb 100644
--- a/frontend/src/components/sidebar/NavMenu.vue
+++ b/frontend/src/components/sidebar/NavMenu.vue
@@ -9,10 +9,10 @@ import Config from '@/components/icons/Config.vue'
import useDialogStore from 'stores/dialog.js'
import Github from '@/components/icons/Github.vue'
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
-import useConnectionStore from 'stores/connections.js'
import usePreferencesStore from 'stores/preferences.js'
import Record from '@/components/icons/Record.vue'
import { extraTheme } from '@/utils/extra_theme.js'
+import useBrowserStore from 'stores/browser.js'
const themeVars = useThemeVars()
@@ -34,7 +34,7 @@ const renderIcon = (icon) => {
return () => h(NIcon, null, { default: () => h(icon, { strokeWidth: 3 }) })
}
-const connectionStore = useConnectionStore()
+const browserStore = useBrowserStore()
const i18n = useI18n()
const menuOptions = computed(() => {
return [
@@ -42,7 +42,7 @@ const menuOptions = computed(() => {
label: i18n.t('ribbon.browser'),
key: 'browser',
icon: renderIcon(Database),
- show: connectionStore.anyConnectionOpened,
+ show: browserStore.anyConnectionOpened,
},
{
label: i18n.t('ribbon.server'),
diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js
new file mode 100644
index 0000000..3f7d695
--- /dev/null
+++ b/frontend/src/stores/browser.js
@@ -0,0 +1,1482 @@
+import { defineStore } from 'pinia'
+import { endsWith, find, get, isEmpty, join, remove, size, slice, sortedIndexBy, split, sumBy, toUpper } from 'lodash'
+import {
+ AddHashField,
+ AddListItem,
+ AddStreamValue,
+ AddZSetValue,
+ CleanCmdHistory,
+ CloseConnection,
+ DeleteKey,
+ FlushDB,
+ GetCmdHistory,
+ GetKeyValue,
+ GetSlowLogs,
+ LoadAllKeys,
+ LoadNextKeys,
+ OpenConnection,
+ OpenDatabase,
+ RemoveStreamValues,
+ RenameKey,
+ ServerInfo,
+ SetHashValue,
+ SetKeyTTL,
+ SetKeyValue,
+ SetListItem,
+ SetSetItem,
+ UpdateSetItem,
+ UpdateZSetValue,
+} from 'wailsjs/go/services/browserService.js'
+import useTabStore from 'stores/tab.js'
+import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
+import { BrowserTabType } from '@/consts/browser_tab_type.js'
+import { KeyViewType } from '@/consts/key_view_type.js'
+import { ConnectionType } from '@/consts/connection_type.js'
+import { types } from '@/consts/support_redis_type.js'
+import useConnectionStore from 'stores/connections.js'
+
+const useBrowserStore = defineStore('browser', {
+ /**
+ * @typedef {Object} DatabaseItem
+ * @property {string} key - tree node unique key
+ * @property {string} label
+ * @property {string} [name] - server name, type != ConnectionType.Group only
+ * @property {number} type
+ * @property {number} [db] - database index, type == ConnectionType.RedisDB only
+ * @property {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue only
+ * @property {string} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
+ * @property {number} [keys] - children key count
+ * @property {boolean} [isLeaf]
+ * @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
+ * @property {boolean} [expanded] - current node is expanded
+ * @property {DatabaseItem[]} [children]
+ * @property {boolean} [loading] - indicated that is loading children now
+ * @property {boolean} [fullLoaded] - indicated that all children already loaded
+ */
+
+ /**
+ * @typedef {Object} HistoryItem
+ * @property {string} time
+ * @property {string} server
+ * @property {string} cmd
+ * @property {number} cost
+ */
+
+ /**
+ * @typedef {Object} BrowserState
+ * @property {Object} serverStats
+ * @property {Object.} keyFilter key is 'server#db', 'server#-1' stores default filter pattern
+ * @property {Object.} typeFilter key is 'server#db'
+ * @property {Object.} viewType
+ * @property {Object.} databases
+ * @property {Object.>} nodeMap key format likes 'server#db', children key format likes 'key#type'
+ */
+
+ /**
+ *
+ * @returns {BrowserState}
+ */
+ state: () => ({
+ serverStats: {}, // current server status info
+ keyFilter: {}, // all key filters in opened connections group by 'server+db'
+ typeFilter: {}, // all key type filters in opened connections group by 'server+db'
+ viewType: {}, // view type selection for all opened connections group by 'server'
+ databases: {}, // all databases in opened connections group by 'server name'
+ nodeMap: {}, // all nodes in opened connections group by 'server#db' and 'type/key'
+ keySet: {}, // all keys set in opened connections group by 'server#db
+ }),
+ getters: {
+ anyConnectionOpened() {
+ return !isEmpty(this.databases)
+ },
+ },
+ actions: {
+ /**
+ * check if connection is connected
+ * @param name
+ * @returns {boolean}
+ */
+ isConnected(name) {
+ let dbs = get(this.databases, name, [])
+ return !isEmpty(dbs)
+ },
+
+ /**
+ * close all connections
+ * @returns {Promise}
+ */
+ async closeAllConnection() {
+ for (const name in this.databases) {
+ await CloseConnection(name)
+ }
+
+ this.databases = {}
+ this.nodeMap.clear()
+ this.keySet.clear()
+ this.serverStats = {}
+ const tabStore = useTabStore()
+ tabStore.removeAllTab()
+ },
+
+ /**
+ * get database by server name and index
+ * @param {string} connName
+ * @param {number} db
+ * @return {{}|null}
+ */
+ getDatabase(connName, db) {
+ const dbs = this.databases[connName]
+ if (dbs != null) {
+ const selDB = find(dbs, (item) => item.db === db)
+ if (selDB != null) {
+ return selDB
+ }
+ }
+ return null
+ },
+
+ /**
+ * switch key view
+ * @param {string} connName
+ * @param {number} viewType
+ */
+ async switchKeyView(connName, viewType) {
+ if (viewType !== KeyViewType.Tree && viewType !== KeyViewType.List) {
+ return
+ }
+
+ const t = get(this.viewType, connName, KeyViewType.Tree)
+ if (t === viewType) {
+ return
+ }
+
+ this.viewType[connName] = viewType
+ const dbs = get(this.databases, connName, [])
+ for (const dbItem of dbs) {
+ if (!dbItem.opened) {
+ continue
+ }
+
+ dbItem.children = undefined
+ dbItem.keys = 0
+ const { db = 0 } = dbItem
+ this._getNodeMap(connName, db).clear()
+ const keys = this._getKeySet(connName, db)
+ this._addKeyNodes(connName, db, keys)
+ this._tidyNode(connName, db, '')
+ }
+ },
+
+ /**
+ * open connection
+ * @param {string} name
+ * @param {boolean} [reload]
+ * @returns {Promise}
+ */
+ async openConnection(name, reload) {
+ if (this.isConnected(name)) {
+ if (reload !== true) {
+ return
+ } else {
+ // reload mode, try close connection first
+ await CloseConnection(name)
+ }
+ }
+
+ const { data, success, msg } = await OpenConnection(name)
+ if (!success) {
+ throw new Error(msg)
+ }
+ // append to db node to current connection
+ // const connNode = this.getConnection(name)
+ // if (connNode == null) {
+ // throw new Error('no such connection')
+ // }
+ const { db, view = KeyViewType.Tree } = data
+ if (isEmpty(db)) {
+ throw new Error('no db loaded')
+ }
+ const dbs = []
+ for (let i = 0; i < db.length; i++) {
+ this._getNodeMap(name, i).clear()
+ this._getKeySet(name, i).clear()
+ dbs.push({
+ key: `${name}/${db[i].name}`,
+ label: db[i].name,
+ name: name,
+ keys: 0,
+ maxKeys: db[i].keys,
+ db: db[i].index,
+ type: ConnectionType.RedisDB,
+ isLeaf: false,
+ children: undefined,
+ })
+ }
+ this.databases[name] = dbs
+ this.viewType[name] = view
+ },
+
+ /**
+ * close connection
+ * @param {string} name
+ * @returns {Promise}
+ */
+ async closeConnection(name) {
+ const { success, msg } = await CloseConnection(name)
+ if (!success) {
+ // throw new Error(msg)
+ return false
+ }
+
+ const dbs = this.databases[name]
+ if (!isEmpty(dbs)) {
+ for (const db of dbs) {
+ this.removeKeyFilter(name, db.db)
+ this._getNodeMap(name, db.db).clear()
+ this._getKeySet(name, db.db).clear()
+ }
+ }
+ this.removeKeyFilter(name, -1)
+ delete this.databases[name]
+ delete this.serverStats[name]
+
+ const tabStore = useTabStore()
+ tabStore.removeTabByName(name)
+ return true
+ },
+
+ /**
+ * open database and load all keys
+ * @param connName
+ * @param db
+ * @returns {Promise}
+ */
+ async openDatabase(connName, db) {
+ const { match: filterPattern, type: filterType } = this.getKeyFilter(connName, db)
+ const { data, success, msg } = await OpenDatabase(connName, db, filterPattern, filterType)
+ if (!success) {
+ throw new Error(msg)
+ }
+ const { keys = [], end = false } = data
+ const selDB = this.getDatabase(connName, db)
+ if (selDB == null) {
+ return
+ }
+
+ selDB.opened = true
+ selDB.fullLoaded = end
+ if (isEmpty(keys)) {
+ selDB.children = []
+ } else {
+ // append db node to current connection's children
+ this._addKeyNodes(connName, db, keys)
+ }
+ this._tidyNode(connName, db)
+ },
+
+ /**
+ * reopen database
+ * @param connName
+ * @param db
+ * @returns {Promise}
+ */
+ async reopenDatabase(connName, db) {
+ const selDB = this.getDatabase(connName, db)
+ if (selDB == null) {
+ return
+ }
+ selDB.children = undefined
+ selDB.isLeaf = false
+
+ this._getNodeMap(connName, db).clear()
+ this._getKeySet(connName, db).clear()
+ },
+
+ /**
+ * close database
+ * @param connName
+ * @param db
+ */
+ closeDatabase(connName, db) {
+ const selDB = this.getDatabase(connName, db)
+ if (selDB == null) {
+ return
+ }
+ delete selDB.children
+ selDB.isLeaf = false
+ selDB.opened = false
+
+ this._getNodeMap(connName, db).clear()
+ this._getKeySet(connName, db).clear()
+ },
+
+ /**
+ *
+ * @param server
+ * @returns {Promise<{}>}
+ */
+ async getServerInfo(server) {
+ try {
+ const { success, data } = await ServerInfo(server)
+ if (success) {
+ this.serverStats[server] = data
+ return data
+ }
+ } finally {
+ }
+ return {}
+ },
+
+ /**
+ * load redis key
+ * @param {string} server
+ * @param {number} db
+ * @param {string|number[]} [key] when key is null or blank, update tab to display normal content (blank content or server status)
+ * @param {string} [viewType]
+ * @param {string} [decodeType]
+ */
+ async loadKeyValue(server, db, key, viewType, decodeType) {
+ try {
+ const tab = useTabStore()
+ if (!isEmpty(key)) {
+ const { data, success, msg } = await GetKeyValue(server, db, key, viewType, decodeType)
+ if (success) {
+ const { type, ttl, value, size, length, viewAs, decode } = data
+ const k = decodeRedisKey(key)
+ const binaryKey = k !== key
+ tab.upsertTab({
+ subTab: BrowserTabType.KeyDetail,
+ server,
+ db,
+ type,
+ ttl,
+ keyCode: binaryKey ? key : undefined,
+ key: k,
+ value,
+ size,
+ length,
+ viewAs,
+ decode,
+ })
+ return
+ } else {
+ if (!isEmpty(msg)) {
+ $message.error('load key fail: ' + msg)
+ }
+ // its danger to delete "non-exists" key, just remove from tree view
+ await this.deleteKey(server, db, key, true)
+ // TODO: show key not found page or check exists on server first?
+ }
+ }
+
+ tab.upsertTab({
+ subTab: BrowserTabType.Status,
+ server,
+ db,
+ type: 'none',
+ ttl: -1,
+ key: null,
+ keyCode: null,
+ value: null,
+ size: 0,
+ length: 0,
+ })
+ } finally {
+ }
+ },
+
+ /**
+ * scan keys with prefix
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} match
+ * @param {string} matchType
+ * @param {boolean} [full]
+ * @returns {Promise<{keys: string[], end: boolean}>}
+ */
+ async scanKeys(connName, db, match, matchType, full) {
+ let resp
+ if (full) {
+ resp = await LoadAllKeys(connName, db, match || '*', matchType)
+ } else {
+ resp = await LoadNextKeys(connName, db, match || '*', matchType)
+ }
+ const { data, success, msg } = resp || {}
+ if (!success) {
+ throw new Error(msg)
+ }
+ const { keys = [], end } = data
+ return { keys, end, success }
+ },
+
+ /**
+ *
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|null} prefix
+ * @param {string|null} matchType
+ * @param {boolean} [all]
+ * @return {Promise<{keys: Array, end: boolean}>}
+ * @private
+ */
+ async _loadKeys(connName, db, prefix, matchType, all) {
+ let match = prefix
+ if (isEmpty(match)) {
+ match = '*'
+ } else {
+ const separator = this._getSeparator(connName)
+ if (!endsWith(prefix, separator + '*')) {
+ match = prefix + separator + '*'
+ }
+ }
+ return this.scanKeys(connName, db, match, matchType, all)
+ },
+
+ /**
+ * load more keys within the database
+ * @param {string} connName
+ * @param {number} db
+ * @return {Promise}
+ */
+ async loadMoreKeys(connName, db) {
+ const { match, type: keyType } = this.getKeyFilter(connName, db)
+ const { keys, end } = await this._loadKeys(connName, db, match, keyType, false)
+ // remove current keys below prefix
+ this._addKeyNodes(connName, db, keys)
+ this._tidyNode(connName, db, '')
+ return end
+ },
+
+ /**
+ * load all left keys within the database
+ * @param {string} connName
+ * @param {number} db
+ * @return {Promise}
+ */
+ async loadAllKeys(connName, db) {
+ const { match, type: keyType } = this.getKeyFilter(connName, db)
+ const { keys } = await this._loadKeys(connName, db, match, keyType, true)
+ this._addKeyNodes(connName, db, keys)
+ this._tidyNode(connName, db, '')
+ },
+
+ /**
+ * get custom separator of connection
+ * @param server
+ * @returns {string}
+ * @private
+ */
+ _getSeparator(server) {
+ const connStore = useConnectionStore()
+ const { keySeparator } = connStore.getDefaultSeparator(server)
+ if (isEmpty(keySeparator)) {
+ return ':'
+ }
+ return keySeparator
+ },
+
+ /**
+ * get node map
+ * @param {string} connName
+ * @param {number} db
+ * @returns {Map}
+ * @private
+ */
+ _getNodeMap(connName, db) {
+ if (!this.nodeMap.hasOwnProperty(`${connName}#${db}`)) {
+ this.nodeMap[`${connName}#${db}`] = new Map()
+ }
+ // construct a tree node list, the format of item key likes 'server/db#type/key'
+ return this.nodeMap[`${connName}#${db}`]
+ },
+
+ /**
+ * get all keys in a database
+ * @param {string} connName
+ * @param {number} db
+ * @return {Set}
+ * @private
+ */
+ _getKeySet(connName, db) {
+ if (!this.keySet.hasOwnProperty(`${connName}#${db}`)) {
+ this.keySet[`${connName}#${db}`] = new Set()
+ }
+ // construct a key set
+ return this.keySet[`${connName}#${db}`]
+ },
+
+ /**
+ * remove keys in db
+ * @param {string} connName
+ * @param {number} db
+ * @param {Array|Set} keys
+ * @param {boolean} [sortInsert]
+ * @return {{success: boolean, newKey: number, newLayer: number, replaceKey: number}}
+ * @private
+ */
+ _addKeyNodes(connName, db, keys, sortInsert) {
+ const result = {
+ success: false,
+ newLayer: 0,
+ newKey: 0,
+ replaceKey: 0,
+ }
+ if (isEmpty(keys)) {
+ return result
+ }
+ const separator = this._getSeparator(connName)
+ const selDB = this.getDatabase(connName, db)
+ if (selDB == null) {
+ return result
+ }
+
+ if (selDB.children == null) {
+ selDB.children = []
+ }
+ const nodeMap = this._getNodeMap(connName, db)
+ const keySet = this._getKeySet(connName, db)
+ const rootChildren = selDB.children
+ const viewType = get(this.viewType, connName, KeyViewType.Tree)
+ if (viewType === KeyViewType.List) {
+ // construct list view data
+ for (const key of keys) {
+ const k = decodeRedisKey(key)
+ const isBinaryKey = k !== key
+ const nodeKey = `${ConnectionType.RedisValue}/${nativeRedisKey(key)}`
+ const replaceKey = nodeMap.has(nodeKey)
+ const selectedNode = {
+ key: `${connName}/db${db}#${nodeKey}`,
+ label: k,
+ db,
+ keys: 0,
+ redisKey: k,
+ redisKeyCode: isBinaryKey ? key : undefined,
+ type: ConnectionType.RedisValue,
+ isLeaf: true,
+ }
+ nodeMap.set(nodeKey, selectedNode)
+ keySet.add(key)
+ if (!replaceKey) {
+ if (sortInsert) {
+ const index = sortedIndexBy(rootChildren, selectedNode, 'key')
+ rootChildren.splice(index, 0, selectedNode)
+ } else {
+ rootChildren.push(selectedNode)
+ }
+ result.newKey += 1
+ } else {
+ result.replaceKey += 1
+ }
+ }
+ } else {
+ // construct tree view data
+ for (const key of keys) {
+ const k = decodeRedisKey(key)
+ const isBinaryKey = k !== key
+ const keyParts = isBinaryKey ? [nativeRedisKey(key)] : split(k, separator)
+ const len = size(keyParts)
+ const lastIdx = len - 1
+ let handlePath = ''
+ let children = rootChildren
+ for (let i = 0; i < len; i++) {
+ handlePath += keyParts[i]
+ if (i !== lastIdx) {
+ // layer
+ const nodeKey = `${ConnectionType.RedisKey}/${handlePath}`
+ let selectedNode = nodeMap.get(nodeKey)
+ if (selectedNode == null) {
+ selectedNode = {
+ key: `${connName}/db${db}#${nodeKey}`,
+ label: keyParts[i],
+ db,
+ keys: 0,
+ redisKey: handlePath,
+ type: ConnectionType.RedisKey,
+ isLeaf: false,
+ children: [],
+ }
+ nodeMap.set(nodeKey, selectedNode)
+ if (sortInsert) {
+ const index = sortedIndexBy(children, selectedNode, 'key')
+ children.splice(index, 0, selectedNode)
+ } else {
+ children.push(selectedNode)
+ }
+ result.newLayer += 1
+ }
+ children = selectedNode.children
+ handlePath += separator
+ } else {
+ // key
+ const nodeKey = `${ConnectionType.RedisValue}/${handlePath}`
+ const replaceKey = nodeMap.has(nodeKey)
+ const selectedNode = {
+ key: `${connName}/db${db}#${nodeKey}`,
+ label: isBinaryKey ? k : keyParts[i],
+ db,
+ keys: 0,
+ redisKey: handlePath,
+ redisKeyCode: isBinaryKey ? key : undefined,
+ type: ConnectionType.RedisValue,
+ isLeaf: true,
+ }
+ nodeMap.set(nodeKey, selectedNode)
+ keySet.add(key)
+ if (!replaceKey) {
+ if (sortInsert) {
+ const index = sortedIndexBy(children, selectedNode, 'key')
+ children.splice(index, 0, selectedNode)
+ } else {
+ children.push(selectedNode)
+ }
+ result.newKey += 1
+ } else {
+ result.replaceKey += 1
+ }
+ }
+ }
+ }
+ }
+ return result
+ },
+
+ /**
+ *
+ * @param {DatabaseItem[]} nodeList
+ * @private
+ */
+ _sortNodes(nodeList) {
+ if (nodeList == null) {
+ return
+ }
+ nodeList.sort((a, b) => {
+ return a.key > b.key ? 1 : -1
+ })
+ },
+
+ /**
+ * tidy node by key
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} [key]
+ * @param {boolean} [skipResort]
+ * @private
+ */
+ _tidyNode(connName, db, key, skipResort) {
+ const nodeMap = this._getNodeMap(connName, db)
+ const dbNode = this.getDatabase(connName, db) || {}
+ const separator = this._getSeparator(connName)
+ const keyParts = split(key, separator)
+ const totalParts = size(keyParts)
+ let node
+ // find last exists ancestor key
+ let i = totalParts - 1
+ for (; i > 0; i--) {
+ const parentKey = join(slice(keyParts, 0, i), separator)
+ node = nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
+ if (node != null) {
+ break
+ }
+ }
+ if (node == null) {
+ node = dbNode
+ }
+ const keyCountUpdated = this._tidyNodeChildren(node, skipResort)
+
+ if (keyCountUpdated) {
+ // update key count of parent and above
+ for (; i > 0; i--) {
+ const parentKey = join(slice(keyParts, 0, i), separator)
+ const parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
+ if (parentNode == null) {
+ break
+ }
+ parentNode.keys = sumBy(parentNode.children, 'keys')
+ }
+ // update key count of db
+ dbNode.keys = sumBy(dbNode.children, 'keys')
+ }
+ return true
+ },
+
+ /**
+ * sort all node item's children and calculate keys count
+ * @param {DatabaseItem} node
+ * @param {boolean} skipSort skip sorting children
+ * @returns {boolean} return whether key count changed
+ * @private
+ */
+ _tidyNodeChildren(node, skipSort) {
+ let count = 0
+ if (!isEmpty(node.children)) {
+ if (skipSort !== true) {
+ this._sortNodes(node.children)
+ }
+
+ for (const elem of node.children) {
+ this._tidyNodeChildren(elem, skipSort)
+ count += elem.keys
+ }
+ } else {
+ if (node.type === ConnectionType.RedisValue) {
+ count += 1
+ } else {
+ // no children in db node or layer node, set count to 0
+ count = 0
+ }
+ }
+ if (node.keys !== count) {
+ node.keys = count
+ return true
+ }
+ return false
+ },
+
+ /**
+ * get tree node by key name
+ * @param key
+ * @return {DatabaseItem|null}
+ */
+ getNode(key) {
+ let idx = key.indexOf('#')
+ if (idx < 0) {
+ idx = size(key)
+ }
+ const dbPart = key.substring(0, idx)
+ // parse server and db index
+ const idx2 = dbPart.lastIndexOf('/db')
+ if (idx2 < 0) {
+ return null
+ }
+ const server = dbPart.substring(0, idx2)
+ const db = parseInt(dbPart.substring(idx2 + 3))
+ if (isNaN(db)) {
+ return null
+ }
+
+ if (size(key) > idx + 1) {
+ const keyPart = key.substring(idx + 1)
+ // contains redis key
+ const nodeMap = this._getNodeMap(server, db)
+ return nodeMap.get(keyPart)
+ } else {
+ return this.getDatabase(server, db)
+ }
+ },
+
+ /**
+ * set redis key
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string} keyType
+ * @param {any} value
+ * @param {number} ttl
+ * @param {string} [viewAs]
+ * @param {string} [decode]
+ * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>}
+ */
+ async setKey(connName, db, key, keyType, value, ttl, viewAs, decode) {
+ try {
+ const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl, viewAs, decode)
+ if (success) {
+ // update tree view data
+ const { newKey = 0 } = this._addKeyNodes(connName, db, [key], true)
+ if (newKey > 0) {
+ this._tidyNode(connName, db, key)
+ }
+ return { success, nodeKey: `${connName}/db${db}#${ConnectionType.RedisValue}/${key}` }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * update hash field
+ * when field is set, newField is null, delete field
+ * when field is null, newField is set, add new field
+ * when both field and newField are set, and field === newField, update field
+ * when both field and newField are set, and field !== newField, delete field and add newField
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string} field
+ * @param {string} newField
+ * @param {string} value
+ * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
+ */
+ async setHash(connName, db, key, field, newField, value) {
+ try {
+ const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '')
+ if (success) {
+ const { updated = {} } = data
+ return { success, updated }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * insert or update hash field item
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields
+ * @param {string[]} fieldItems field1, value1, filed2, value2...
+ * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
+ */
+ async addHashField(connName, db, key, action, fieldItems) {
+ try {
+ const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
+ if (success) {
+ const { updated = {} } = data
+ return { success, updated }
+ } else {
+ return { success: false, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * remove hash field
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} key
+ * @param {string} field
+ * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
+ */
+ async removeHashField(connName, db, key, field) {
+ try {
+ const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '')
+ if (success) {
+ const { removed = [] } = data
+ return { success, removed }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * insert list item
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {int} action 0: push to head, 1: push to tail
+ * @param {string[]}values
+ * @returns {Promise<*|{msg, success: boolean}>}
+ */
+ async addListItem(connName, db, key, action, values) {
+ try {
+ return AddListItem(connName, db, key, action, values)
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * prepend item to head of list
+ * @param connName
+ * @param db
+ * @param key
+ * @param values
+ * @returns {Promise<[msg]: string, success: boolean, [item]: []>}
+ */
+ async prependListItem(connName, db, key, values) {
+ try {
+ const { data, success, msg } = await AddListItem(connName, db, key, 0, values)
+ if (success) {
+ const { left = [] } = data
+ return { success, item: left }
+ } else {
+ return { success: false, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * append item to tail of list
+ * @param connName
+ * @param db
+ * @param key
+ * @param values
+ * @returns {Promise<[msg]: string, success: boolean, [item]: any[]>}
+ */
+ async appendListItem(connName, db, key, values) {
+ try {
+ const { data, success, msg } = await AddListItem(connName, db, key, 1, values)
+ if (success) {
+ const { right = [] } = data
+ return { success, item: right }
+ } else {
+ return { success: false, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * update value of list item by index
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {number} index
+ * @param {string} value
+ * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
+ */
+ async updateListItem(connName, db, key, index, value) {
+ try {
+ const { data, success, msg } = await SetListItem(connName, db, key, index, value)
+ if (success) {
+ const { updated = {} } = data
+ return { success, updated }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * remove list item
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {number} index
+ * @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>}
+ */
+ async removeListItem(connName, db, key, index) {
+ try {
+ const { data, success, msg } = await SetListItem(connName, db, key, index, '')
+ if (success) {
+ const { removed = [] } = data
+ return { success, removed }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * add item to set
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number} key
+ * @param {string} value
+ * @returns {Promise<{[msg]: string, success: boolean}>}
+ */
+ async addSetItem(connName, db, key, value) {
+ try {
+ const { success, msg } = await SetSetItem(connName, db, key, false, [value])
+ if (success) {
+ return { success }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * update value of set item
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string} value
+ * @param {string} newValue
+ * @returns {Promise<{[msg]: string, success: boolean}>}
+ */
+ async updateSetItem(connName, db, key, value, newValue) {
+ try {
+ const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue)
+ if (success) {
+ return { success: true }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * remove item from set
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string} value
+ * @returns {Promise<{[msg]: string, success: boolean}>}
+ */
+ async removeSetItem(connName, db, key, value) {
+ try {
+ const { success, msg } = await SetSetItem(connName, db, key, true, [value])
+ if (success) {
+ return { success }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * add item to sorted set
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {number} action
+ * @param {Object.} vs value: score
+ * @returns {Promise<{[msg]: string, success: boolean}>}
+ */
+ async addZSetItem(connName, db, key, action, vs) {
+ try {
+ const { success, msg } = await AddZSetValue(connName, db, key, action, vs)
+ if (success) {
+ return { success }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * update item of sorted set
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string} value
+ * @param {string} newValue
+ * @param {number} score
+ * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}, [removed]: []}>}
+ */
+ async updateZSetItem(connName, db, key, value, newValue, score) {
+ try {
+ const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score)
+ if (success) {
+ const { updated, removed } = data
+ return { success, updated, removed }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * remove item from sorted set
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string} value
+ * @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>}
+ */
+ async removeZSetItem(connName, db, key, value) {
+ try {
+ const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0)
+ if (success) {
+ const { removed } = data
+ return { success, removed }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * insert new stream field item
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string} id
+ * @param {string[]} values field1, value1, filed2, value2...
+ * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
+ */
+ async addStreamValue(connName, db, key, id, values) {
+ try {
+ const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values)
+ if (success) {
+ const { updated = {} } = data
+ return { success, updated }
+ } else {
+ return { success: false, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * remove stream field
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {string[]|string} ids
+ * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
+ */
+ async removeStreamValues(connName, db, key, ids) {
+ if (typeof ids === 'string') {
+ ids = [ids]
+ }
+ try {
+ const { data = {}, success, msg } = await RemoveStreamValues(connName, db, key, ids)
+ if (success) {
+ const { removed = [] } = data
+ return { success, removed }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * reset key's ttl
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} key
+ * @param {number} ttl
+ * @returns {Promise}
+ */
+ async setTTL(connName, db, key, ttl) {
+ try {
+ const { success, msg } = await SetKeyTTL(connName, db, key, ttl)
+ return success === true
+ } catch (e) {
+ return false
+ }
+ },
+
+ /**
+ *
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} [key]
+ * @param {boolean} [isLayer]
+ * @private
+ */
+ _deleteKeyNode(connName, db, key, isLayer) {
+ const dbRoot = this.getDatabase(connName, db) || {}
+ const separator = this._getSeparator(connName)
+
+ if (dbRoot == null) {
+ return false
+ }
+
+ const nodeMap = this._getNodeMap(connName, db)
+ const keySet = this._getKeySet(connName, db)
+ if (isLayer === true) {
+ this._deleteChildrenKeyNodes(nodeMap, keySet, key)
+ }
+ if (isEmpty(key)) {
+ // clear all key nodes
+ dbRoot.children = []
+ dbRoot.keys = 0
+ } else {
+ const keyParts = split(key, separator)
+ const totalParts = size(keyParts)
+ // remove from parent in tree node
+ const parentKey = slice(keyParts, 0, totalParts - 1)
+ let parentNode
+ if (isEmpty(parentKey)) {
+ parentNode = dbRoot
+ } else {
+ parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, separator)}`)
+ }
+
+ // not found parent node
+ if (parentNode == null) {
+ return false
+ }
+ remove(parentNode.children, {
+ type: isLayer ? ConnectionType.RedisKey : ConnectionType.RedisValue,
+ redisKey: key,
+ })
+
+ // check and remove empty layer node
+ let i = totalParts - 1
+ for (; i >= 0; i--) {
+ const anceKey = join(slice(keyParts, 0, i), separator)
+ if (i > 0) {
+ const anceNode = nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
+ const redisKey = join(slice(keyParts, 0, i + 1), separator)
+ remove(anceNode.children, { type: ConnectionType.RedisKey, redisKey })
+
+ if (isEmpty(anceNode.children)) {
+ nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`)
+ keySet.delete(anceNode.redisKeyCode || anceNode.redisKey)
+ } else {
+ break
+ }
+ } else {
+ // last one, remove from db node
+ remove(dbRoot.children, { type: ConnectionType.RedisKey, redisKey: keyParts[0] })
+ const node = nodeMap.get(`${ConnectionType.RedisValue}/${keyParts[0]}`)
+ if (node != null) {
+ nodeMap.delete(`${ConnectionType.RedisValue}/${keyParts[0]}`)
+ keySet.delete(node.redisKeyCode || node.redisKey)
+ }
+ }
+ }
+ }
+
+ return true
+ },
+
+ /**
+ * delete node and all it's children from nodeMap
+ * @param {Map} nodeMap
+ * @param {Set} keySet
+ * @param {string} [key] clean nodeMap if key is empty
+ * @private
+ */
+ _deleteChildrenKeyNodes(nodeMap, keySet, key) {
+ if (isEmpty(key)) {
+ nodeMap.clear()
+ keySet.clear()
+ } else {
+ const mapKey = `${ConnectionType.RedisKey}/${key}`
+ const node = nodeMap.get(mapKey)
+ for (const child of node.children || []) {
+ if (child.type === ConnectionType.RedisValue) {
+ if (!nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) {
+ console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`)
+ }
+ keySet.delete(child.redisKeyCode || child.redisKey)
+ } else if (child.type === ConnectionType.RedisKey) {
+ this._deleteChildrenKeyNodes(nodeMap, keySet, child.redisKey)
+ }
+ }
+ if (!nodeMap.delete(mapKey)) {
+ console.warn('delete map key', mapKey)
+ }
+ keySet.delete(node.redisKeyCode || node.redisKey)
+ }
+ },
+
+ /**
+ * delete redis key
+ * @param {string} connName
+ * @param {number} db
+ * @param {string|number[]} key
+ * @param {boolean} [soft] do not try to remove from redis if true, just remove from tree data
+ * @returns {Promise}
+ */
+ async deleteKey(connName, db, key, soft) {
+ try {
+ if (soft !== true) {
+ await DeleteKey(connName, db, key)
+ }
+
+ const k = nativeRedisKey(key)
+ // update tree view data
+ this._deleteKeyNode(connName, db, k)
+ this._tidyNode(connName, db, k, true)
+
+ // set tab content empty
+ const tab = useTabStore()
+ tab.emptyTab(connName)
+ return true
+ } finally {
+ }
+ return false
+ },
+
+ /**
+ * delete keys with prefix
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} prefix
+ * @param {boolean} async
+ * @returns {Promise}
+ */
+ async deleteKeyPrefix(connName, db, prefix, async) {
+ if (isEmpty(prefix)) {
+ return false
+ }
+ try {
+ if (!endsWith(prefix, '*')) {
+ prefix += '*'
+ }
+ const { data, success, msg } = await DeleteKey(connName, db, prefix, async)
+ if (success) {
+ // const { deleted: keys = [] } = data
+ // for (const key of keys) {
+ // await this._deleteKeyNode(connName, db, key)
+ // }
+ const separator = this._getSeparator(connName)
+ if (endsWith(prefix, '*')) {
+ prefix = prefix.substring(0, prefix.length - 1)
+ }
+ if (endsWith(prefix, separator)) {
+ prefix = prefix.substring(0, prefix.length - 1)
+ }
+ this._deleteKeyNode(connName, db, prefix, true)
+ this._tidyNode(connName, db, prefix, true)
+ return true
+ }
+ } finally {
+ }
+ return false
+ },
+
+ /**
+ * flush database
+ * @param connName
+ * @param db
+ * @param async
+ * @return {Promise}
+ */
+ async flushDatabase(connName, db, async) {
+ try {
+ const { success = false } = await FlushDB(connName, db, async)
+
+ if (success === true) {
+ // update tree view data
+ this._deleteKeyNode(connName, db)
+ // set tab content empty
+ const tab = useTabStore()
+ tab.emptyTab(connName)
+ return true
+ }
+ } finally {
+ }
+ return true
+ },
+
+ /**
+ * rename key
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} key
+ * @param {string} newKey
+ * @returns {Promise<{[msg]: string, success: boolean}>}
+ */
+ async renameKey(connName, db, key, newKey) {
+ const { success = false, msg } = await RenameKey(connName, db, key, newKey)
+ if (success) {
+ // delete old key and add new key struct
+ this._deleteKeyNode(connName, db, key)
+ this._addKeyNodes(connName, db, [newKey])
+ return { success: true }
+ } else {
+ return { success: false, msg }
+ }
+ },
+
+ /**
+ * get command history
+ * @param {number} [pageNo]
+ * @param {number} [pageSize]
+ * @returns {Promise}
+ */
+ async getCmdHistory(pageNo, pageSize) {
+ if (pageNo === undefined || pageSize === undefined) {
+ pageNo = -1
+ pageSize = -1
+ }
+ try {
+ const { success, data = { list: [] } } = await GetCmdHistory(pageNo, pageSize)
+ const { list } = data
+ return list
+ } catch {
+ return []
+ }
+ },
+
+ /**
+ * clean cmd history
+ * @return {Promise}
+ */
+ async cleanCmdHistory() {
+ try {
+ const { success } = await CleanCmdHistory()
+ return success === true
+ } catch {
+ return false
+ }
+ },
+
+ /**
+ * get slow log list
+ * @param {string} server
+ * @param {number} db
+ * @param {number} num
+ * @return {Promise<[]>}
+ */
+ async getSlowLog(server, db, num) {
+ try {
+ const { success, data = { list: [] } } = await GetSlowLogs(server, db, num)
+ const { list } = data
+ return list
+ } catch {
+ return []
+ }
+ },
+
+ /**
+ * get key filter pattern and filter type
+ * @param {string} server
+ * @param {number} db
+ * @returns {{match: string, type: string}}
+ */
+ getKeyFilter(server, db) {
+ let match, type
+ const key = `${server}#${db}`
+ if (!this.keyFilter.hasOwnProperty(key)) {
+ // get default key filter from connection profile
+ const conn = useConnectionStore()
+ match = conn.getDefaultKeyFilter(server)
+ } else {
+ match = this.keyFilter[key] || '*'
+ }
+ type = this.typeFilter[`${server}#${db}`] || ''
+ return {
+ match,
+ type: toUpper(type),
+ }
+ },
+
+ /**
+ * set key filter
+ * @param {string} server
+ * @param {number} db
+ * @param {string} pattern
+ * @param {string} [type]
+ */
+ setKeyFilter(server, db, pattern, type) {
+ this.keyFilter[`${server}#${db}`] = pattern || '*'
+ this.typeFilter[`${server}#${db}`] = types[toUpper(type)] || ''
+ },
+
+ removeKeyFilter(server, db) {
+ this.keyFilter[`${server}#${db}`] = '*'
+ delete this.typeFilter[`${server}#${db}`]
+ },
+ },
+})
+
+export default useBrowserStore
diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js
index febe4f6..a738bcc 100644
--- a/frontend/src/stores/connections.js
+++ b/frontend/src/stores/connections.js
@@ -1,60 +1,18 @@
import { defineStore } from 'pinia'
+import { get, isEmpty, uniq } from 'lodash'
import {
- endsWith,
- find,
- get,
- isEmpty,
- join,
- remove,
- size,
- slice,
- sortedIndexBy,
- split,
- sumBy,
- toUpper,
- uniq,
-} from 'lodash'
-import {
- AddHashField,
- AddListItem,
- AddStreamValue,
- AddZSetValue,
- CleanCmdHistory,
- CloseConnection,
CreateGroup,
DeleteConnection,
DeleteGroup,
- DeleteKey,
- FlushDB,
- GetCmdHistory,
GetConnection,
- GetKeyValue,
- GetSlowLogs,
ListConnection,
- LoadAllKeys,
- LoadNextKeys,
- OpenConnection,
- OpenDatabase,
- RemoveStreamValues,
RenameGroup,
- RenameKey,
SaveConnection,
SaveSortedConnection,
- ServerInfo,
- SetHashValue,
- SetKeyTTL,
- SetKeyValue,
- SetListItem,
- SetSetItem,
- UpdateSetItem,
- UpdateZSetValue,
} from 'wailsjs/go/services/connectionService.js'
import { ConnectionType } from '@/consts/connection_type.js'
-import useTabStore from './tab.js'
-import { types } from '@/consts/support_redis_type.js'
-import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
import { KeyViewType } from '@/consts/key_view_type.js'
-import { BrowserTabType } from '@/consts/browser_tab_type.js'
+import useBrowserStore from 'stores/browser.js'
const useConnectionStore = defineStore('connections', {
/**
@@ -68,48 +26,17 @@ const useConnectionStore = defineStore('connections', {
*/
/**
- * @typedef {Object} DatabaseItem
- * @property {string} key - tree node unique key
- * @property {string} label
- * @property {string} [name] - server name, type != ConnectionType.Group only
- * @property {number} type
- * @property {number} [db] - database index, type == ConnectionType.RedisDB only
- * @property {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue only
- * @property {string} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
- * @property {number} [keys] - children key count
- * @property {boolean} [isLeaf]
- * @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
- * @property {boolean} [expanded] - current node is expanded
- * @property {DatabaseItem[]} [children]
- * @property {boolean} [loading] - indicated that is loading children now
- * @property {boolean} [fullLoaded] - indicated that all children already loaded
+ * @typedef {Object} ConnectionProfile
+ * @property {string} defaultFilter
+ * @property {string} keySeparator
+ * @property {string} markColor
*/
/**
* @typedef {Object} ConnectionState
* @property {string[]} groups
* @property {ConnectionItem[]} connections
- * @property {Object} serverStats
* @property {Object.} serverProfile
- * @property {Object.} keyFilter key is 'server#db', 'server#-1' stores default filter pattern
- * @property {Object.} typeFilter key is 'server#db'
- * @property {Object.} databases
- * @property {Object.>} nodeMap key format likes 'server#db', children key format likes 'key#type'
- */
-
- /**
- * @typedef {Object} HistoryItem
- * @property {string} time
- * @property {string} server
- * @property {string} cmd
- * @property {number} cost
- */
-
- /**
- * @typedef {Object} ConnectionProfile
- * @property {string} defaultFilter
- * @property {string} keySeparator
- * @property {string} markColor
*/
/**
@@ -119,20 +46,9 @@ const useConnectionStore = defineStore('connections', {
state: () => ({
groups: [], // all group name set
connections: [], // all connections
- serverStats: {}, // current server status info
- serverProfile: {}, // all server profile
- keyFilter: {}, // all key filters in opened connections group by 'server+db'
- typeFilter: {}, // all key type filters in opened connections group by 'server+db'
- viewType: {}, // view type selection for all opened connections group by 'server'
- databases: {}, // all databases in opened connections group by 'server name'
- nodeMap: {}, // all nodes in opened connections group by 'server#db' and 'type/key'
- keySet: {}, // all keys set in opened connections group by 'server#db
+ serverProfile: {}, // all server profile in flat list
}),
- getters: {
- anyConnectionOpened() {
- return !isEmpty(this.databases)
- },
- },
+ getters: {},
actions: {
/**
* load all store connections struct from local profile
@@ -191,7 +107,6 @@ const useConnectionStore = defineStore('connections', {
children,
})
}
- this.setKeyFilter(conn.name, -1, conn.defaultFilter)
}
this.connections = conns
this.serverProfile = profiles
@@ -310,55 +225,6 @@ const useConnectionStore = defineStore('connections', {
return null
},
- /**
- * get database by server name and index
- * @param {string} connName
- * @param {number} db
- * @return {{}|null}
- */
- getDatabase(connName, db) {
- const dbs = this.databases[connName]
- if (dbs != null) {
- const selDB = find(dbs, (item) => item.db === db)
- if (selDB != null) {
- return selDB
- }
- }
- return null
- },
-
- /**
- * switch key view
- * @param {string} connName
- * @param {number} viewType
- */
- async switchKeyView(connName, viewType) {
- if (viewType !== KeyViewType.Tree && viewType !== KeyViewType.List) {
- return
- }
-
- const t = get(this.viewType, connName, KeyViewType.Tree)
- if (t === viewType) {
- return
- }
-
- this.viewType[connName] = viewType
- const dbs = get(this.databases, connName, [])
- for (const dbItem of dbs) {
- if (!dbItem.opened) {
- continue
- }
-
- dbItem.children = undefined
- dbItem.keys = 0
- const { db = 0 } = dbItem
- this._getNodeMap(connName, db).clear()
- const keys = this._getKeySet(connName, db)
- this._addKeyNodes(connName, db, keys)
- this._tidyNode(connName, db, '')
- }
- },
-
/**
* create a new connection or update current connection profile
* @param {string} name set null if create a new connection
@@ -403,111 +269,6 @@ const useConnectionStore = defineStore('connections', {
SaveSortedConnection(s)
},
- /**
- * check if connection is connected
- * @param name
- * @returns {boolean}
- */
- isConnected(name) {
- let dbs = get(this.databases, name, [])
- return !isEmpty(dbs)
- },
-
- /**
- * open connection
- * @param {string} name
- * @param {boolean} [reload]
- * @returns {Promise}
- */
- async openConnection(name, reload) {
- if (this.isConnected(name)) {
- if (reload !== true) {
- return
- } else {
- // reload mode, try close connection first
- await CloseConnection(name)
- }
- }
-
- const { data, success, msg } = await OpenConnection(name)
- if (!success) {
- throw new Error(msg)
- }
- // append to db node to current connection
- // const connNode = this.getConnection(name)
- // if (connNode == null) {
- // throw new Error('no such connection')
- // }
- const { db, view = KeyViewType.Tree } = data
- if (isEmpty(db)) {
- throw new Error('no db loaded')
- }
- const dbs = []
- for (let i = 0; i < db.length; i++) {
- this._getNodeMap(name, i).clear()
- this._getKeySet(name, i).clear()
- dbs.push({
- key: `${name}/${db[i].name}`,
- label: db[i].name,
- name: name,
- keys: 0,
- maxKeys: db[i].keys,
- db: db[i].index,
- type: ConnectionType.RedisDB,
- isLeaf: false,
- children: undefined,
- })
- }
- this.databases[name] = dbs
- this.viewType[name] = view
- },
-
- /**
- * close connection
- * @param {string} name
- * @returns {Promise}
- */
- async closeConnection(name) {
- const { success, msg } = await CloseConnection(name)
- if (!success) {
- // throw new Error(msg)
- return false
- }
-
- const dbs = this.databases[name]
- if (!isEmpty(dbs)) {
- for (const db of dbs) {
- this.removeKeyFilter(name, db.db)
- this._getNodeMap(name, db.db).clear()
- this._getKeySet(name, db.db).clear()
- }
- }
- this.removeKeyFilter(name, -1)
- delete this.databases[name]
- delete this.serverStats[name]
-
- const tabStore = useTabStore()
- tabStore.removeTabByName(name)
- return true
- },
-
- /**
- * close all connections
- * @returns {Promise}
- */
- async closeAllConnection() {
- for (const name in this.databases) {
- await CloseConnection(name)
- }
-
- this.databases = {}
- this.nodeMap.clear()
- this.keySet.clear()
- this.serverStats = {}
- const tabStore = useTabStore()
- tabStore.removeAllTab()
- },
-
/**
* remove connection
* @param name
@@ -515,7 +276,8 @@ const useConnectionStore = defineStore('connections', {
*/
async deleteConnection(name) {
// close connection first
- await this.closeConnection(name)
+ const browser = useBrowserStore()
+ await browser.closeConnection(name)
const { success, msg } = await DeleteConnection(name)
if (!success) {
return { success: false, msg }
@@ -572,1232 +334,23 @@ const useConnectionStore = defineStore('connections', {
},
/**
- * open database and load all keys
- * @param connName
- * @param db
- * @returns {Promise}
+ * get default key filter pattern by server name
+ * @param name
+ * @return {string}
*/
- async openDatabase(connName, db) {
- const { match: filterPattern, type: filterType } = this.getKeyFilter(connName, db)
- const { data, success, msg } = await OpenDatabase(connName, db, filterPattern, filterType)
- if (!success) {
- throw new Error(msg)
- }
- const { keys = [], end = false } = data
- const selDB = this.getDatabase(connName, db)
- if (selDB == null) {
- return
- }
-
- selDB.opened = true
- selDB.fullLoaded = end
- if (isEmpty(keys)) {
- selDB.children = []
- } else {
- // append db node to current connection's children
- this._addKeyNodes(connName, db, keys)
- }
- this._tidyNode(connName, db)
+ getDefaultKeyFilter(name) {
+ const { defaultFilter = '*' } = this.serverProfile[name] || {}
+ return defaultFilter
},
/**
- * reopen database
- * @param connName
- * @param db
- * @returns {Promise}
+ * get default key separator by server name
+ * @param name
+ * @return {string}
*/
- async reopenDatabase(connName, db) {
- const selDB = this.getDatabase(connName, db)
- if (selDB == null) {
- return
- }
- selDB.children = undefined
- selDB.isLeaf = false
-
- this._getNodeMap(connName, db).clear()
- this._getKeySet(connName, db).clear()
- },
-
- /**
- * close database
- * @param connName
- * @param db
- */
- closeDatabase(connName, db) {
- const selDB = this.getDatabase(connName, db)
- if (selDB == null) {
- return
- }
- delete selDB.children
- selDB.isLeaf = false
- selDB.opened = false
-
- this._getNodeMap(connName, db).clear()
- this._getKeySet(connName, db).clear()
- },
-
- /**
- *
- * @param server
- * @returns {Promise<{}>}
- */
- async getServerInfo(server) {
- try {
- const { success, data } = await ServerInfo(server)
- if (success) {
- this.serverStats[server] = data
- return data
- }
- } finally {
- }
- return {}
- },
-
- /**
- * load redis key
- * @param {string} server
- * @param {number} db
- * @param {string|number[]} [key] when key is null or blank, update tab to display normal content (blank content or server status)
- * @param {string} [viewType]
- * @param {string} [decodeType]
- */
- async loadKeyValue(server, db, key, viewType, decodeType) {
- try {
- const tab = useTabStore()
- if (!isEmpty(key)) {
- const { data, success, msg } = await GetKeyValue(server, db, key, viewType, decodeType)
- if (success) {
- const { type, ttl, value, size, length, viewAs, decode } = data
- const k = decodeRedisKey(key)
- const binaryKey = k !== key
- tab.upsertTab({
- subTab: BrowserTabType.KeyDetail,
- server,
- db,
- type,
- ttl,
- keyCode: binaryKey ? key : undefined,
- key: k,
- value,
- size,
- length,
- viewAs,
- decode,
- })
- return
- } else {
- if (!isEmpty(msg)) {
- $message.error('load key fail: ' + msg)
- }
- // its danger to delete "non-exists" key, just remove from tree view
- await this.deleteKey(server, db, key, true)
- // TODO: show key not found page or check exists on server first?
- }
- }
-
- tab.upsertTab({
- subTab: BrowserTabType.Status,
- server,
- db,
- type: 'none',
- ttl: -1,
- key: null,
- keyCode: null,
- value: null,
- size: 0,
- length: 0,
- })
- } finally {
- }
- },
-
- /**
- * scan keys with prefix
- * @param {string} connName
- * @param {number} db
- * @param {string} match
- * @param {string} matchType
- * @param {boolean} [full]
- * @returns {Promise<{keys: string[], end: boolean}>}
- */
- async scanKeys(connName, db, match, matchType, full) {
- let resp
- if (full) {
- resp = await LoadAllKeys(connName, db, match || '*', matchType)
- } else {
- resp = await LoadNextKeys(connName, db, match || '*', matchType)
- }
- const { data, success, msg } = resp || {}
- if (!success) {
- throw new Error(msg)
- }
- const { keys = [], end } = data
- return { keys, end, success }
- },
-
- /**
- *
- * @param {string} connName
- * @param {number} db
- * @param {string|null} prefix
- * @param {string|null} matchType
- * @param {boolean} [all]
- * @return {Promise<{keys: Array, end: boolean}>}
- * @private
- */
- async _loadKeys(connName, db, prefix, matchType, all) {
- let match = prefix
- if (isEmpty(match)) {
- match = '*'
- } else {
- const separator = this._getSeparator(connName)
- if (!endsWith(prefix, separator + '*')) {
- match = prefix + separator + '*'
- }
- }
- return this.scanKeys(connName, db, match, matchType, all)
- },
-
- /**
- * load more keys within the database
- * @param {string} connName
- * @param {number} db
- * @return {Promise}
- */
- async loadMoreKeys(connName, db) {
- const { match, type: keyType } = this.getKeyFilter(connName, db)
- const { keys, end } = await this._loadKeys(connName, db, match, keyType, false)
- // remove current keys below prefix
- this._addKeyNodes(connName, db, keys)
- this._tidyNode(connName, db, '')
- return end
- },
-
- /**
- * load all left keys within the database
- * @param {string} connName
- * @param {number} db
- * @return {Promise}
- */
- async loadAllKeys(connName, db) {
- const { match, type: keyType } = this.getKeyFilter(connName, db)
- const { keys } = await this._loadKeys(connName, db, match, keyType, true)
- this._addKeyNodes(connName, db, keys)
- this._tidyNode(connName, db, '')
- },
-
- /**
- * get custom separator of connection
- * @param server
- * @returns {string}
- * @private
- */
- _getSeparator(server) {
- const { keySeparator } = this.serverProfile[server] || { keySeparator: ':' }
- if (isEmpty(keySeparator)) {
- return ':'
- }
- return keySeparator
- },
-
- /**
- * get node map
- * @param {string} connName
- * @param {number} db
- * @returns {Map}
- * @private
- */
- _getNodeMap(connName, db) {
- if (!this.nodeMap.hasOwnProperty(`${connName}#${db}`)) {
- this.nodeMap[`${connName}#${db}`] = new Map()
- }
- // construct a tree node list, the format of item key likes 'server/db#type/key'
- return this.nodeMap[`${connName}#${db}`]
- },
-
- /**
- * get all keys in a database
- * @param {string} connName
- * @param {number} db
- * @return {Set}
- * @private
- */
- _getKeySet(connName, db) {
- if (!this.keySet.hasOwnProperty(`${connName}#${db}`)) {
- this.keySet[`${connName}#${db}`] = new Set()
- }
- // construct a key set
- return this.keySet[`${connName}#${db}`]
- },
-
- /**
- * remove keys in db
- * @param {string} connName
- * @param {number} db
- * @param {Array|Set} keys
- * @param {boolean} [sortInsert]
- * @return {{success: boolean, newKey: number, newLayer: number, replaceKey: number}}
- * @private
- */
- _addKeyNodes(connName, db, keys, sortInsert) {
- const result = {
- success: false,
- newLayer: 0,
- newKey: 0,
- replaceKey: 0,
- }
- if (isEmpty(keys)) {
- return result
- }
- const separator = this._getSeparator(connName)
- const selDB = this.getDatabase(connName, db)
- if (selDB == null) {
- return result
- }
-
- if (selDB.children == null) {
- selDB.children = []
- }
- const nodeMap = this._getNodeMap(connName, db)
- const keySet = this._getKeySet(connName, db)
- const rootChildren = selDB.children
- const viewType = get(this.viewType, connName, KeyViewType.Tree)
- if (viewType === KeyViewType.List) {
- // construct list view data
- for (const key of keys) {
- const k = decodeRedisKey(key)
- const isBinaryKey = k !== key
- const nodeKey = `${ConnectionType.RedisValue}/${nativeRedisKey(key)}`
- const replaceKey = nodeMap.has(nodeKey)
- const selectedNode = {
- key: `${connName}/db${db}#${nodeKey}`,
- label: k,
- db,
- keys: 0,
- redisKey: k,
- redisKeyCode: isBinaryKey ? key : undefined,
- type: ConnectionType.RedisValue,
- isLeaf: true,
- }
- nodeMap.set(nodeKey, selectedNode)
- keySet.add(key)
- if (!replaceKey) {
- if (sortInsert) {
- const index = sortedIndexBy(rootChildren, selectedNode, 'key')
- rootChildren.splice(index, 0, selectedNode)
- } else {
- rootChildren.push(selectedNode)
- }
- result.newKey += 1
- } else {
- result.replaceKey += 1
- }
- }
- } else {
- // construct tree view data
- for (const key of keys) {
- const k = decodeRedisKey(key)
- const isBinaryKey = k !== key
- const keyParts = isBinaryKey ? [nativeRedisKey(key)] : split(k, separator)
- const len = size(keyParts)
- const lastIdx = len - 1
- let handlePath = ''
- let children = rootChildren
- for (let i = 0; i < len; i++) {
- handlePath += keyParts[i]
- if (i !== lastIdx) {
- // layer
- const nodeKey = `${ConnectionType.RedisKey}/${handlePath}`
- let selectedNode = nodeMap.get(nodeKey)
- if (selectedNode == null) {
- selectedNode = {
- key: `${connName}/db${db}#${nodeKey}`,
- label: keyParts[i],
- db,
- keys: 0,
- redisKey: handlePath,
- type: ConnectionType.RedisKey,
- isLeaf: false,
- children: [],
- }
- nodeMap.set(nodeKey, selectedNode)
- if (sortInsert) {
- const index = sortedIndexBy(children, selectedNode, 'key')
- children.splice(index, 0, selectedNode)
- } else {
- children.push(selectedNode)
- }
- result.newLayer += 1
- }
- children = selectedNode.children
- handlePath += separator
- } else {
- // key
- const nodeKey = `${ConnectionType.RedisValue}/${handlePath}`
- const replaceKey = nodeMap.has(nodeKey)
- const selectedNode = {
- key: `${connName}/db${db}#${nodeKey}`,
- label: isBinaryKey ? k : keyParts[i],
- db,
- keys: 0,
- redisKey: handlePath,
- redisKeyCode: isBinaryKey ? key : undefined,
- type: ConnectionType.RedisValue,
- isLeaf: true,
- }
- nodeMap.set(nodeKey, selectedNode)
- keySet.add(key)
- if (!replaceKey) {
- if (sortInsert) {
- const index = sortedIndexBy(children, selectedNode, 'key')
- children.splice(index, 0, selectedNode)
- } else {
- children.push(selectedNode)
- }
- result.newKey += 1
- } else {
- result.replaceKey += 1
- }
- }
- }
- }
- }
- return result
- },
-
- /**
- *
- * @param {DatabaseItem[]} nodeList
- * @private
- */
- _sortNodes(nodeList) {
- if (nodeList == null) {
- return
- }
- nodeList.sort((a, b) => {
- return a.key > b.key ? 1 : -1
- })
- },
-
- /**
- * tidy node by key
- * @param {string} connName
- * @param {number} db
- * @param {string} [key]
- * @param {boolean} [skipResort]
- * @private
- */
- _tidyNode(connName, db, key, skipResort) {
- const nodeMap = this._getNodeMap(connName, db)
- const dbNode = this.getDatabase(connName, db) || {}
- const separator = this._getSeparator(connName)
- const keyParts = split(key, separator)
- const totalParts = size(keyParts)
- let node
- // find last exists ancestor key
- let i = totalParts - 1
- for (; i > 0; i--) {
- const parentKey = join(slice(keyParts, 0, i), separator)
- node = nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
- if (node != null) {
- break
- }
- }
- if (node == null) {
- node = dbNode
- }
- const keyCountUpdated = this._tidyNodeChildren(node, skipResort)
-
- if (keyCountUpdated) {
- // update key count of parent and above
- for (; i > 0; i--) {
- const parentKey = join(slice(keyParts, 0, i), separator)
- const parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
- if (parentNode == null) {
- break
- }
- parentNode.keys = sumBy(parentNode.children, 'keys')
- }
- // update key count of db
- dbNode.keys = sumBy(dbNode.children, 'keys')
- }
- return true
- },
-
- /**
- * sort all node item's children and calculate keys count
- * @param {DatabaseItem} node
- * @param {boolean} skipSort skip sorting children
- * @returns {boolean} return whether key count changed
- * @private
- */
- _tidyNodeChildren(node, skipSort) {
- let count = 0
- if (!isEmpty(node.children)) {
- if (skipSort !== true) {
- this._sortNodes(node.children)
- }
-
- for (const elem of node.children) {
- this._tidyNodeChildren(elem, skipSort)
- count += elem.keys
- }
- } else {
- if (node.type === ConnectionType.RedisValue) {
- count += 1
- } else {
- // no children in db node or layer node, set count to 0
- count = 0
- }
- }
- if (node.keys !== count) {
- node.keys = count
- return true
- }
- return false
- },
-
- /**
- * get tree node by key name
- * @param key
- * @return {DatabaseItem|null}
- */
- getNode(key) {
- let idx = key.indexOf('#')
- if (idx < 0) {
- idx = size(key)
- }
- const dbPart = key.substring(0, idx)
- // parse server and db index
- const idx2 = dbPart.lastIndexOf('/db')
- if (idx2 < 0) {
- return null
- }
- const server = dbPart.substring(0, idx2)
- const db = parseInt(dbPart.substring(idx2 + 3))
- if (isNaN(db)) {
- return null
- }
-
- if (size(key) > idx + 1) {
- const keyPart = key.substring(idx + 1)
- // contains redis key
- const nodeMap = this._getNodeMap(server, db)
- return nodeMap.get(keyPart)
- } else {
- return this.getDatabase(server, db)
- }
- },
-
- /**
- * set redis key
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string} keyType
- * @param {any} value
- * @param {number} ttl
- * @param {string} [viewAs]
- * @param {string} [decode]
- * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>}
- */
- async setKey(connName, db, key, keyType, value, ttl, viewAs, decode) {
- try {
- const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl, viewAs, decode)
- if (success) {
- // update tree view data
- const { newKey = 0 } = this._addKeyNodes(connName, db, [key], true)
- if (newKey > 0) {
- this._tidyNode(connName, db, key)
- }
- return { success, nodeKey: `${connName}/db${db}#${ConnectionType.RedisValue}/${key}` }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * update hash field
- * when field is set, newField is null, delete field
- * when field is null, newField is set, add new field
- * when both field and newField are set, and field === newField, update field
- * when both field and newField are set, and field !== newField, delete field and add newField
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string} field
- * @param {string} newField
- * @param {string} value
- * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
- */
- async setHash(connName, db, key, field, newField, value) {
- try {
- const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '')
- if (success) {
- const { updated = {} } = data
- return { success, updated }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * insert or update hash field item
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields
- * @param {string[]} fieldItems field1, value1, filed2, value2...
- * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
- */
- async addHashField(connName, db, key, action, fieldItems) {
- try {
- const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
- if (success) {
- const { updated = {} } = data
- return { success, updated }
- } else {
- return { success: false, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * remove hash field
- * @param {string} connName
- * @param {number} db
- * @param {string} key
- * @param {string} field
- * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
- */
- async removeHashField(connName, db, key, field) {
- try {
- const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '')
- if (success) {
- const { removed = [] } = data
- return { success, removed }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * insert list item
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {int} action 0: push to head, 1: push to tail
- * @param {string[]}values
- * @returns {Promise<*|{msg, success: boolean}>}
- */
- async addListItem(connName, db, key, action, values) {
- try {
- return AddListItem(connName, db, key, action, values)
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * prepend item to head of list
- * @param connName
- * @param db
- * @param key
- * @param values
- * @returns {Promise<[msg]: string, success: boolean, [item]: []>}
- */
- async prependListItem(connName, db, key, values) {
- try {
- const { data, success, msg } = await AddListItem(connName, db, key, 0, values)
- if (success) {
- const { left = [] } = data
- return { success, item: left }
- } else {
- return { success: false, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * append item to tail of list
- * @param connName
- * @param db
- * @param key
- * @param values
- * @returns {Promise<[msg]: string, success: boolean, [item]: any[]>}
- */
- async appendListItem(connName, db, key, values) {
- try {
- const { data, success, msg } = await AddListItem(connName, db, key, 1, values)
- if (success) {
- const { right = [] } = data
- return { success, item: right }
- } else {
- return { success: false, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * update value of list item by index
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {number} index
- * @param {string} value
- * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
- */
- async updateListItem(connName, db, key, index, value) {
- try {
- const { data, success, msg } = await SetListItem(connName, db, key, index, value)
- if (success) {
- const { updated = {} } = data
- return { success, updated }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * remove list item
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {number} index
- * @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>}
- */
- async removeListItem(connName, db, key, index) {
- try {
- const { data, success, msg } = await SetListItem(connName, db, key, index, '')
- if (success) {
- const { removed = [] } = data
- return { success, removed }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * add item to set
- * @param {string} connName
- * @param {number} db
- * @param {string|number} key
- * @param {string} value
- * @returns {Promise<{[msg]: string, success: boolean}>}
- */
- async addSetItem(connName, db, key, value) {
- try {
- const { success, msg } = await SetSetItem(connName, db, key, false, [value])
- if (success) {
- return { success }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * update value of set item
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string} value
- * @param {string} newValue
- * @returns {Promise<{[msg]: string, success: boolean}>}
- */
- async updateSetItem(connName, db, key, value, newValue) {
- try {
- const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue)
- if (success) {
- return { success: true }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * remove item from set
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string} value
- * @returns {Promise<{[msg]: string, success: boolean}>}
- */
- async removeSetItem(connName, db, key, value) {
- try {
- const { success, msg } = await SetSetItem(connName, db, key, true, [value])
- if (success) {
- return { success }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * add item to sorted set
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {number} action
- * @param {Object.} vs value: score
- * @returns {Promise<{[msg]: string, success: boolean}>}
- */
- async addZSetItem(connName, db, key, action, vs) {
- try {
- const { success, msg } = await AddZSetValue(connName, db, key, action, vs)
- if (success) {
- return { success }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * update item of sorted set
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string} value
- * @param {string} newValue
- * @param {number} score
- * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}, [removed]: []}>}
- */
- async updateZSetItem(connName, db, key, value, newValue, score) {
- try {
- const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score)
- if (success) {
- const { updated, removed } = data
- return { success, updated, removed }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * remove item from sorted set
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string} value
- * @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>}
- */
- async removeZSetItem(connName, db, key, value) {
- try {
- const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0)
- if (success) {
- const { removed } = data
- return { success, removed }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * insert new stream field item
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string} id
- * @param {string[]} values field1, value1, filed2, value2...
- * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
- */
- async addStreamValue(connName, db, key, id, values) {
- try {
- const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values)
- if (success) {
- const { updated = {} } = data
- return { success, updated }
- } else {
- return { success: false, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * remove stream field
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {string[]|string} ids
- * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
- */
- async removeStreamValues(connName, db, key, ids) {
- if (typeof ids === 'string') {
- ids = [ids]
- }
- try {
- const { data = {}, success, msg } = await RemoveStreamValues(connName, db, key, ids)
- if (success) {
- const { removed = [] } = data
- return { success, removed }
- } else {
- return { success, msg }
- }
- } catch (e) {
- return { success: false, msg: e.message }
- }
- },
-
- /**
- * reset key's ttl
- * @param {string} connName
- * @param {number} db
- * @param {string} key
- * @param {number} ttl
- * @returns {Promise}
- */
- async setTTL(connName, db, key, ttl) {
- try {
- const { success, msg } = await SetKeyTTL(connName, db, key, ttl)
- return success === true
- } catch (e) {
- return false
- }
- },
-
- /**
- *
- * @param {string} connName
- * @param {number} db
- * @param {string} [key]
- * @param {boolean} [isLayer]
- * @private
- */
- _deleteKeyNode(connName, db, key, isLayer) {
- const dbRoot = this.getDatabase(connName, db) || {}
- const separator = this._getSeparator(connName)
-
- if (dbRoot == null) {
- return false
- }
-
- const nodeMap = this._getNodeMap(connName, db)
- const keySet = this._getKeySet(connName, db)
- if (isLayer === true) {
- this._deleteChildrenKeyNodes(nodeMap, keySet, key)
- }
- if (isEmpty(key)) {
- // clear all key nodes
- dbRoot.children = []
- dbRoot.keys = 0
- } else {
- const keyParts = split(key, separator)
- const totalParts = size(keyParts)
- // remove from parent in tree node
- const parentKey = slice(keyParts, 0, totalParts - 1)
- let parentNode
- if (isEmpty(parentKey)) {
- parentNode = dbRoot
- } else {
- parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, separator)}`)
- }
-
- // not found parent node
- if (parentNode == null) {
- return false
- }
- remove(parentNode.children, {
- type: isLayer ? ConnectionType.RedisKey : ConnectionType.RedisValue,
- redisKey: key,
- })
-
- // check and remove empty layer node
- let i = totalParts - 1
- for (; i >= 0; i--) {
- const anceKey = join(slice(keyParts, 0, i), separator)
- if (i > 0) {
- const anceNode = nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
- const redisKey = join(slice(keyParts, 0, i + 1), separator)
- remove(anceNode.children, { type: ConnectionType.RedisKey, redisKey })
-
- if (isEmpty(anceNode.children)) {
- nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`)
- keySet.delete(anceNode.redisKeyCode || anceNode.redisKey)
- } else {
- break
- }
- } else {
- // last one, remove from db node
- remove(dbRoot.children, { type: ConnectionType.RedisKey, redisKey: keyParts[0] })
- const node = nodeMap.get(`${ConnectionType.RedisValue}/${keyParts[0]}`)
- if (node != null) {
- nodeMap.delete(`${ConnectionType.RedisValue}/${keyParts[0]}`)
- keySet.delete(node.redisKeyCode || node.redisKey)
- }
- }
- }
- }
-
- return true
- },
-
- /**
- * delete node and all it's children from nodeMap
- * @param {Map} nodeMap
- * @param {Set} keySet
- * @param {string} [key] clean nodeMap if key is empty
- * @private
- */
- _deleteChildrenKeyNodes(nodeMap, keySet, key) {
- if (isEmpty(key)) {
- nodeMap.clear()
- keySet.clear()
- } else {
- const mapKey = `${ConnectionType.RedisKey}/${key}`
- const node = nodeMap.get(mapKey)
- for (const child of node.children || []) {
- if (child.type === ConnectionType.RedisValue) {
- if (!nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) {
- console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`)
- }
- keySet.delete(child.redisKeyCode || child.redisKey)
- } else if (child.type === ConnectionType.RedisKey) {
- this._deleteChildrenKeyNodes(nodeMap, keySet, child.redisKey)
- }
- }
- if (!nodeMap.delete(mapKey)) {
- console.warn('delete map key', mapKey)
- }
- keySet.delete(node.redisKeyCode || node.redisKey)
- }
- },
-
- /**
- * delete redis key
- * @param {string} connName
- * @param {number} db
- * @param {string|number[]} key
- * @param {boolean} [soft] do not try to remove from redis if true, just remove from tree data
- * @returns {Promise}
- */
- async deleteKey(connName, db, key, soft) {
- try {
- if (soft !== true) {
- await DeleteKey(connName, db, key)
- }
-
- const k = nativeRedisKey(key)
- // update tree view data
- this._deleteKeyNode(connName, db, k)
- this._tidyNode(connName, db, k, true)
-
- // set tab content empty
- const tab = useTabStore()
- tab.emptyTab(connName)
- return true
- } finally {
- }
- return false
- },
-
- /**
- * delete keys with prefix
- * @param {string} connName
- * @param {number} db
- * @param {string} prefix
- * @param {boolean} async
- * @returns {Promise}
- */
- async deleteKeyPrefix(connName, db, prefix, async) {
- if (isEmpty(prefix)) {
- return false
- }
- try {
- if (!endsWith(prefix, '*')) {
- prefix += '*'
- }
- const { data, success, msg } = await DeleteKey(connName, db, prefix, async)
- if (success) {
- // const { deleted: keys = [] } = data
- // for (const key of keys) {
- // await this._deleteKeyNode(connName, db, key)
- // }
- const separator = this._getSeparator(connName)
- if (endsWith(prefix, '*')) {
- prefix = prefix.substring(0, prefix.length - 1)
- }
- if (endsWith(prefix, separator)) {
- prefix = prefix.substring(0, prefix.length - 1)
- }
- this._deleteKeyNode(connName, db, prefix, true)
- this._tidyNode(connName, db, prefix, true)
- return true
- }
- } finally {
- }
- return false
- },
-
- /**
- * flush database
- * @param connName
- * @param db
- * @param async
- * @return {Promise}
- */
- async flushDatabase(connName, db, async) {
- try {
- const { success = false } = await FlushDB(connName, db, async)
-
- if (success === true) {
- // update tree view data
- this._deleteKeyNode(connName, db)
- // set tab content empty
- const tab = useTabStore()
- tab.emptyTab(connName)
- return true
- }
- } finally {
- }
- return true
- },
-
- /**
- * rename key
- * @param {string} connName
- * @param {number} db
- * @param {string} key
- * @param {string} newKey
- * @returns {Promise<{[msg]: string, success: boolean}>}
- */
- async renameKey(connName, db, key, newKey) {
- const { success = false, msg } = await RenameKey(connName, db, key, newKey)
- if (success) {
- // delete old key and add new key struct
- this._deleteKeyNode(connName, db, key)
- this._addKeyNodes(connName, db, [newKey])
- return { success: true }
- } else {
- return { success: false, msg }
- }
- },
-
- /**
- * get command history
- * @param {number} [pageNo]
- * @param {number} [pageSize]
- * @returns {Promise}
- */
- async getCmdHistory(pageNo, pageSize) {
- if (pageNo === undefined || pageSize === undefined) {
- pageNo = -1
- pageSize = -1
- }
- try {
- const { success, data = { list: [] } } = await GetCmdHistory(pageNo, pageSize)
- const { list } = data
- return list
- } catch {
- return []
- }
- },
-
- /**
- * clean cmd history
- * @return {Promise}
- */
- async cleanCmdHistory() {
- try {
- const { success } = await CleanCmdHistory()
- return success === true
- } catch {
- return false
- }
- },
-
- /**
- * get slow log list
- * @param {string} server
- * @param {number} db
- * @param {number} num
- * @return {Promise<[]>}
- */
- async getSlowLog(server, db, num) {
- try {
- const { success, data = { list: [] } } = await GetSlowLogs(server, db, num)
- const { list } = data
- return list
- } catch {
- return []
- }
- },
-
- /**
- * get key filter pattern and filter type
- * @param {string} server
- * @param {number} db
- * @returns {{match: string, type: string}}
- */
- getKeyFilter(server, db) {
- let match, type
- const key = `${server}#${db}`
- if (!this.keyFilter.hasOwnProperty(key)) {
- match = this.keyFilter[`${server}#-1`] || '*'
- } else {
- match = this.keyFilter[key] || '*'
- }
- type = this.typeFilter[`${server}#${db}`] || ''
- return {
- match,
- type: toUpper(type),
- }
- },
-
- /**
- * set key filter
- * @param {string} server
- * @param {number} db
- * @param {string} pattern
- * @param {string} [type]
- */
- setKeyFilter(server, db, pattern, type) {
- this.keyFilter[`${server}#${db}`] = pattern || '*'
- this.typeFilter[`${server}#${db}`] = types[toUpper(type)] || ''
- },
-
- removeKeyFilter(server, db) {
- this.keyFilter[`${server}#${db}`] = '*'
- delete this.typeFilter[`${server}#${db}`]
+ getDefaultSeparator(name) {
+ const { defaultSeparator = ':' } = this.serverProfile[name] || {}
+ return defaultSeparator
},
},
})
diff --git a/frontend/src/utils/theme.js b/frontend/src/utils/theme.js
index 035aa6f..8dc030c 100644
--- a/frontend/src/utils/theme.js
+++ b/frontend/src/utils/theme.js
@@ -12,6 +12,7 @@ export const themeOverrides = {
primaryColorSuppl: '#FF6B6B',
borderRadius: '4px',
borderRadiusSmall: '3px',
+ heightMedium: '32px',
lineHeight: 1.5,
scrollbarWidth: '8px',
tabColor: '#FFFFFF',
diff --git a/main.go b/main.go
index 5e35703..f223639 100644
--- a/main.go
+++ b/main.go
@@ -29,6 +29,7 @@ func main() {
// Create an instance of the app structure
sysSvc := services.System()
connSvc := services.Connection()
+ browserSvc := services.Browser()
cliSvc := services.Cli()
prefSvc := services.Preferences()
prefSvc.SetAppVersion(version)
@@ -59,6 +60,7 @@ func main() {
OnStartup: func(ctx context.Context) {
sysSvc.Start(ctx)
connSvc.Start(ctx)
+ browserSvc.Start(ctx)
cliSvc.Start(ctx)
services.GA().SetSecretKey(gaMeasurementID, gaSecretKey)
@@ -68,12 +70,13 @@ func main() {
runtime2.WindowShow(ctx)
},
OnShutdown: func(ctx context.Context) {
- connSvc.Stop()
+ browserSvc.Stop()
cliSvc.CloseAll()
},
Bind: []interface{}{
sysSvc,
connSvc,
+ browserSvc,
cliSvc,
prefSvc,
},