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, },