diff --git a/backend/consts/default_config.go b/backend/consts/default_config.go index 42aceed..9ecfd6c 100644 --- a/backend/consts/default_config.go +++ b/backend/consts/default_config.go @@ -6,3 +6,5 @@ const DEFAULT_WINDOW_WIDTH = 1024 const DEFAULT_WINDOW_HEIGHT = 768 const MIN_WINDOW_WIDTH = 960 const MIN_WINDOW_HEIGHT = 640 +const DEFAULT_LOAD_SIZE = 10000 +const DEFAULT_SCAN_SIZE = 3000 diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index 2cd9458..c3f0940 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -16,6 +16,7 @@ import ( "sync" "sync/atomic" "time" + "tinyrdm/backend/consts" . "tinyrdm/backend/storage" "tinyrdm/backend/types" "tinyrdm/backend/utils/coll" @@ -43,6 +44,8 @@ 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 { @@ -390,12 +393,13 @@ func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp typ // OpenConnection open redis server connection func (c *connectionService) OpenConnection(name string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(name, 0) + 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) @@ -522,18 +526,18 @@ func (c *connectionService) CloseConnection(name string) (resp types.JSResp) { return } -// get redis client from local cache or create a new open +// 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) (redis.UniversalClient, context.Context, error) { - item, ok := c.connMap[connName] +func (c *connectionService) getRedisClient(connName string, db int) (item connectionItem, err error) { + var ok bool var client redis.UniversalClient - var ctx context.Context - if ok { - client, ctx = item.client, item.ctx + if item, ok = c.connMap[connName]; ok { + client = item.client } else { selConn := c.conns.GetConnection(connName) if selConn == nil { - return nil, nil, fmt.Errorf("no match connection \"%s\"", connName) + err = fmt.Errorf("no match connection \"%s\"", connName) + return } hook := redis2.NewHook(connName, func(cmd string, cost int64) { @@ -550,10 +554,10 @@ func (c *connectionService) getRedisClient(connName string, db int) (redis.Unive }) }) - var err error client, err = c.createRedisClient(selConn.ConnectionConfig) if err != nil { - return nil, nil, fmt.Errorf("create conenction error: %s", err.Error()) + err = fmt.Errorf("create conenction error: %s", err.Error()) + return } // add hook to each node in cluster mode var cluster *redis.ClusterClient @@ -563,33 +567,51 @@ func (c *connectionService) getRedisClient(connName string, db int) (redis.Unive return nil }) if err != nil { - return nil, nil, fmt.Errorf("get cluster nodes error: %s", err.Error()) + 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 { - return nil, nil, errors.New("can not connect to redis server:" + err.Error()) + err = errors.New("can not connect to redis server:" + err.Error()) + return } - var cancelFunc context.CancelFunc - ctx, cancelFunc = context.WithCancel(c.ctx) - c.connMap[connName] = connectionItem{ + 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(ctx, "select", strconv.Itoa(db)).Err(); err != nil { - return nil, nil, err + if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil { + return } } } - return client, ctx, nil + 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" @@ -631,12 +653,13 @@ func (c *connectionService) parseDBItemInfo(info string) map[string]int { // ServerInfo get server info func (c *connectionService) ServerInfo(name string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(name, 0) + 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 { @@ -652,31 +675,43 @@ func (c *connectionService) ServerInfo(name string) (resp types.JSResp) { // 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) { - return c.ScanKeys(connName, db, match, keyType) + c.setClientCursor(connName, db, 0) + return c.LoadNextKeys(connName, db, match, keyType) } -// ScanKeys scan all keys -func (c *connectionService) ScanKeys(connName string, db int, match, keyType string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) - if err != nil { - resp.Msg = err.Error() - return - } - +// 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 - var countPerScan int64 = 10000 + scanSize := int64(Preferences().GetScanSize()) // define sub scan function - scan := func(ctx context.Context, cli redis.UniversalClient, appendFunc func(k any)) error { - var iter *redis.ScanIterator - if filterType { - iter = cli.ScanType(ctx, 0, match, countPerScan, keyType).Iterator() - } else { - iter = cli.Scan(ctx, 0, match, countPerScan).Iterator() + 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 + } } - for iter.Next(ctx) { - appendFunc(strutil.EncodeRedisKey(iter.Val())) - } - return iter.Err() + return nil } var keys []any @@ -684,22 +719,64 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str // 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) { + return scan(ctx, cli, func(k []any) { mutex.Lock() - keys = append(keys, k) + keys = append(keys, k...) mutex.Unlock() }) }) } else { - err = scan(ctx, client, func(k any) { - keys = append(keys, k) + 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 + keys, _, err := c.scanKeys(ctx, client, match, keyType, 0, 0) + if err != nil { + resp.Msg = err.Error() + return + } + c.setClientCursor(connName, db, 0) + resp.Success = true resp.Data = map[string]any{ "keys": keys, @@ -709,12 +786,13 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str // GetKeyValue get value by key func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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 @@ -755,9 +833,10 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s 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, "*", 10000).Result() + loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", scanSize).Result() if err != nil { resp.Msg = err.Error() return @@ -774,9 +853,10 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s 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, "*", 10000).Result() + loadedKey, cursor, err = client.SScan(ctx, key, cursor, "*", scanSize).Result() if err != nil { resp.Msg = err.Error() return @@ -791,9 +871,10 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s 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, "*", 10000).Result() + loadedVal, cursor, err = client.ZScan(ctx, key, cursor, "*", scanSize).Result() if err != nil { resp.Msg = err.Error() return @@ -848,12 +929,13 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s // 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 string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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 { @@ -971,12 +1053,13 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType // SetHashValue set hash field func (c *connectionService) SetHashValue(connName string, db int, k any, field, newField, value string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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{} @@ -1017,12 +1100,13 @@ func (c *connectionService) SetHashValue(connName string, db int, k any, field, // AddHashField add or update hash field func (c *connectionService) AddHashField(connName string, db int, k any, action int, fieldItems []any) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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 { @@ -1063,12 +1147,13 @@ func (c *connectionService) AddHashField(connName string, db int, k any, action // 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) { - client, ctx, err := c.getRedisClient(connName, db) + 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 { @@ -1096,12 +1181,13 @@ func (c *connectionService) AddListItem(connName string, db int, k any, action i // 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) { - client, ctx, err := c.getRedisClient(connName, db) + 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{} @@ -1139,12 +1225,13 @@ func (c *connectionService) SetListItem(connName string, db int, k any, index in // 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) { - client, ctx, err := c.getRedisClient(connName, db) + 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() @@ -1162,12 +1249,13 @@ func (c *connectionService) SetSetItem(connName string, db int, k any, remove bo // UpdateSetItem replace member of set func (c *connectionService) UpdateSetItem(connName string, db int, k any, value, newValue string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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() @@ -1182,12 +1270,13 @@ func (c *connectionService) UpdateSetItem(connName string, db int, k any, value, // 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) { - client, ctx, err := c.getRedisClient(connName, db) + 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 @@ -1233,12 +1322,13 @@ func (c *connectionService) UpdateZSetValue(connName string, db int, k any, valu // 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) { - client, ctx, err := c.getRedisClient(connName, db) + 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{ @@ -1266,12 +1356,13 @@ func (c *connectionService) AddZSetValue(connName string, db int, k any, action // AddStreamValue add stream field func (c *connectionService) AddStreamValue(connName string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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, @@ -1289,12 +1380,13 @@ func (c *connectionService) AddStreamValue(connName string, db int, k any, ID st // RemoveStreamValues remove stream values by id func (c *connectionService) RemoveStreamValues(connName string, db int, k any, IDs []string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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 @@ -1303,12 +1395,13 @@ func (c *connectionService) RemoveStreamValues(connName string, db int, k any, I // SetKeyTTL set ttl of key func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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 { @@ -1330,12 +1423,13 @@ func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64) // DeleteKey remove redis key func (c *connectionService) DeleteKey(connName string, db int, k any, async bool) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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, "*") { @@ -1360,7 +1454,8 @@ func (c *connectionService) DeleteKey(connName string, db int, k any, async bool return nil } - iter := cli.Scan(ctx, 0, key, 10000).Iterator() + 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()) @@ -1414,12 +1509,13 @@ func (c *connectionService) DeleteKey(connName string, db int, k any, async bool // RenameKey rename key func (c *connectionService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) { - client, ctx, err := c.getRedisClient(connName, db) + 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 diff --git a/backend/services/preferences_service.go b/backend/services/preferences_service.go index f54b65a..0f5e268 100644 --- a/backend/services/preferences_service.go +++ b/backend/services/preferences_service.go @@ -133,6 +133,15 @@ func (p *preferencesService) GetWindowSize() (width, height int) { return } +func (p *preferencesService) GetScanSize() int { + data := p.pref.GetPreferences() + size := data.General.ScanSize + if size <= 0 { + size = consts.DEFAULT_SCAN_SIZE + } + return size +} + type latestRelease struct { Name string `json:"name"` TagName string `json:"tag_name"` diff --git a/backend/storage/connections.go b/backend/storage/connections.go index e98cbf3..07955c9 100644 --- a/backend/storage/connections.go +++ b/backend/storage/connections.go @@ -4,6 +4,7 @@ import ( "errors" "gopkg.in/yaml.v3" "sync" + "tinyrdm/backend/consts" "tinyrdm/backend/types" sliceutil "tinyrdm/backend/utils/slice" ) @@ -36,6 +37,7 @@ func (c *ConnectionsStorage) defaultConnectionItem() types.ConnectionConfig { ExecTimeout: 60, DBFilterType: "none", DBFilterList: []int{}, + LoadSize: consts.DEFAULT_LOAD_SIZE, MarkColor: "", Sentinel: types.ConnectionSentinel{ Master: "mymaster", diff --git a/backend/storage/preferences.go b/backend/storage/preferences.go index 14f3dc8..f215c31 100644 --- a/backend/storage/preferences.go +++ b/backend/storage/preferences.go @@ -46,6 +46,7 @@ func (p *PreferencesStorage) GetPreferences() (ret types.Preferences) { defer p.mutex.Unlock() ret = p.getPreferences() + ret.General.ScanSize = max(ret.General.ScanSize, consts.DEFAULT_SCAN_SIZE) ret.Behavior.AsideWidth = max(ret.Behavior.AsideWidth, consts.DEFAULT_ASIDE_WIDTH) ret.Behavior.WindowWidth = max(ret.Behavior.WindowWidth, consts.MIN_WINDOW_WIDTH) ret.Behavior.WindowHeight = max(ret.Behavior.WindowHeight, consts.MIN_WINDOW_HEIGHT) diff --git a/backend/types/connection.go b/backend/types/connection.go index 9ef43da..f7ff5f4 100644 --- a/backend/types/connection.go +++ b/backend/types/connection.go @@ -15,6 +15,7 @@ type ConnectionConfig struct { ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"` DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"` DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"` + LoadSize int `json:"loadSize,omitempty" yaml:"load_size,omitempty"` MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"` SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"` SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"` diff --git a/backend/types/preferences.go b/backend/types/preferences.go index fd4b956..79ebd81 100644 --- a/backend/types/preferences.go +++ b/backend/types/preferences.go @@ -19,6 +19,7 @@ func NewPreferences() Preferences { Theme: "auto", Language: "auto", FontSize: consts.DEFAULT_FONT_SIZE, + ScanSize: consts.DEFAULT_SCAN_SIZE, CheckUpdate: true, }, Editor: PreferencesEditor{ @@ -38,6 +39,7 @@ type PreferencesGeneral struct { Language string `json:"language" yaml:"language"` Font string `json:"font" yaml:"font,omitempty"` FontSize int `json:"fontSize" yaml:"font_size"` + ScanSize int `json:"scanSize" yaml:"scan_size"` UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"` UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"` CheckUpdate bool `json:"checkUpdate" yaml:"check_update"` diff --git a/frontend/src/components/dialogs/ConnectionDialog.vue b/frontend/src/components/dialogs/ConnectionDialog.vue index f92f94e..cf33be0 100644 --- a/frontend/src/components/dialogs/ConnectionDialog.vue +++ b/frontend/src/components/dialogs/ConnectionDialog.vue @@ -370,6 +370,9 @@ const onClose = () => { + + + { :show-require-mark="false" label-placement="top" style="padding-right: 15px"> - - {{ filterForm.server }} + + - {{ filterForm.db }} + @@ -87,7 +87,10 @@ const onClose = () => {
{{ $t('dialogue.filter.filter_pattern_tip') }}
diff --git a/frontend/src/components/dialogs/PreferencesDialog.vue b/frontend/src/components/dialogs/PreferencesDialog.vue index b5d2cb8..d4b741c 100644 --- a/frontend/src/components/dialogs/PreferencesDialog.vue +++ b/frontend/src/components/dialogs/PreferencesDialog.vue @@ -60,46 +60,58 @@ const onClose = () => { :show-icon="false" :title="$t('preferences.name')" preset="dialog" + style="width: 500px" transform-origin="center"> - - - - {{ opt.label }} - - - - - - - - - - - - - - - - {{ $t('preferences.general.use_system_proxy') }} + + + + + {{ opt.label }} + + + + + + + + + + + + + + + + + + + {{ $t('preferences.general.use_system_proxy') }} + + + {{ $t('preferences.general.use_system_proxy_http') }} + + + + + + {{ $t('preferences.general.auto_check_update') }} - - {{ $t('preferences.general.use_system_proxy_http') }} - - - - - - {{ $t('preferences.general.auto_check_update') }} - - +
+ diff --git a/frontend/src/components/icons/LoadAll.vue b/frontend/src/components/icons/LoadAll.vue new file mode 100644 index 0000000..eae85ad --- /dev/null +++ b/frontend/src/components/icons/LoadAll.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/frontend/src/components/icons/LoadList.vue b/frontend/src/components/icons/LoadList.vue new file mode 100644 index 0000000..0511eb3 --- /dev/null +++ b/frontend/src/components/icons/LoadList.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/src/components/icons/More.vue b/frontend/src/components/icons/More.vue new file mode 100644 index 0000000..216276d --- /dev/null +++ b/frontend/src/components/icons/More.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/frontend/src/components/sidebar/BrowserTree.vue b/frontend/src/components/sidebar/BrowserTree.vue index f0d406c..605cd0e 100644 --- a/frontend/src/components/sidebar/BrowserTree.vue +++ b/frontend/src/components/sidebar/BrowserTree.vue @@ -23,6 +23,8 @@ import { typesBgColor, typesColor } from '@/consts/support_redis_type.js' import useTabStore from 'stores/tab.js' 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' const props = defineProps({ server: String, @@ -146,11 +148,11 @@ const menuOptions = { } }, [ConnectionType.RedisKey]: () => [ - { - key: 'key_reload', - label: i18n.t('interface.reload'), - icon: renderIcon(Refresh), - }, + // { + // key: 'key_reload', + // label: i18n.t('interface.reload'), + // icon: renderIcon(Refresh), + // }, { key: 'key_newkey', label: i18n.t('interface.new_key'), @@ -259,9 +261,9 @@ const handleSelectContextMenu = (key) => { const { match: pattern, type } = connectionStore.getKeyFilter(props.server, db) dialogStore.openKeyFilterDialog(props.server, db, pattern, type) break - case 'key_reload': - connectionStore.loadKeys(props.server, db, redisKey) - break + // case 'key_reload': + // connectionStore.loadKeys(props.server, db, redisKey) + // break case 'value_reload': connectionStore.loadKeyValue(props.server, db, redisKey) break @@ -289,6 +291,38 @@ const handleSelectContextMenu = (key) => { $message.error(e.message) }) break + case 'db_loadmore': + if (node != null && !!!node.loading && !!!node.fullLoaded) { + node.loading = true + connectionStore + .loadMoreKeys(props.server, db) + .then((end) => { + // fully loaded + node.fullLoaded = end === true + }) + .catch((e) => { + $message.error(e.message) + }) + .finally(() => { + delete node.loading + }) + } + break + case 'db_loadall': + if (node != null && !!!node.loading) { + node.loading = true + connectionStore + .loadAllKeys(props.server, db) + .catch((e) => { + $message.error(e.message) + }) + .finally(() => { + delete node.loading + node.fullLoaded = true + }) + } + break + case 'more_action': default: console.warn('TODO: handle context menu:' + key) } @@ -389,9 +423,14 @@ const renderLabel = ({ option }) => { case ConnectionType.Server: return h('b', {}, { default: () => option.label }) case ConnectionType.RedisDB: - const { name: server, db } = option + const { name: server, db, opened = false } = option let { match: matchPattern, type: typeFilter } = connectionStore.getKeyFilter(server, db) - const items = [`${option.label} (${option.keys || 0})`] + const items = [] + if (opened) { + items.push(`${option.label} (${option.keys || 0}/${Math.max(option.maxKeys || 0, option.keys || 0)})`) + } else { + items.push(`${option.label} (${Math.max(option.maxKeys || 0, option.keys || 0)})`) + } // show filter tag after label // type filter tag if (!isEmpty(typeFilter)) { @@ -460,30 +499,53 @@ const renderIconMenu = (items) => { ) } -const getDatabaseMenu = (opened) => { +const getDatabaseMenu = (opened, loading, end) => { const btns = [] if (opened) { btns.push( h(IconButton, { tTooltip: 'interface.filter_key', icon: Filter, + disabled: loading === true, onClick: () => handleSelectContextMenu('db_filter'), }), h(IconButton, { tTooltip: 'interface.reload', icon: Refresh, + disabled: loading === true, onClick: () => handleSelectContextMenu('db_reload'), }), h(IconButton, { tTooltip: 'interface.new_key', icon: Add, + disabled: loading === true, onClick: () => handleSelectContextMenu('db_newkey'), }), + h(IconButton, { + tTooltip: 'interface.load_more', + icon: LoadList, + disabled: end === true, + loading: loading === true, + onClick: () => handleSelectContextMenu('db_loadmore'), + }), + h(IconButton, { + tTooltip: 'interface.load_all', + icon: LoadAll, + disabled: end === true, + loading: loading === true, + onClick: () => handleSelectContextMenu('db_loadall'), + }), h(IconButton, { tTooltip: 'interface.batch_delete', icon: Delete, + disabled: loading === true, onClick: () => handleSelectContextMenu('key_remove'), }), + // h(IconButton, { + // tTooltip: 'interface.more_action', + // icon: More, + // onClick: () => handleSelectContextMenu('more_action'), + // }), ) } else { btns.push( @@ -499,11 +561,12 @@ const getDatabaseMenu = (opened) => { const getLayerMenu = () => { return [ - h(IconButton, { - tTooltip: 'interface.reload', - icon: Refresh, - onClick: () => handleSelectContextMenu('key_reload'), - }), + // disable reload by layer, due to conflict with partial loading keys + // h(IconButton, { + // tTooltip: 'interface.reload', + // icon: Refresh, + // onClick: () => handleSelectContextMenu('key_reload'), + // }), h(IconButton, { tTooltip: 'interface.new_key', icon: Add, @@ -532,7 +595,7 @@ const renderSuffix = ({ option }) => { if ((option.type === ConnectionType.RedisDB && option.opened) || includes(selectedKeys.value, option.key)) { switch (option.type) { case ConnectionType.RedisDB: - return renderIconMenu(getDatabaseMenu(option.opened)) + return renderIconMenu(getDatabaseMenu(option.opened, option.loading, option.fullLoaded)) case ConnectionType.RedisKey: return renderIconMenu(getLayerMenu()) case ConnectionType.RedisValue: diff --git a/frontend/src/components/sidebar/NavMenu.vue b/frontend/src/components/sidebar/NavMenu.vue index 7d8668f..f6f69a4 100644 --- a/frontend/src/components/sidebar/NavMenu.vue +++ b/frontend/src/components/sidebar/NavMenu.vue @@ -10,7 +10,6 @@ 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 Help from '@/components/icons/Help.vue' import usePreferencesStore from 'stores/preferences.js' import Record from '@/components/icons/Record.vue' diff --git a/frontend/src/langs/en.json b/frontend/src/langs/en.json index 51f7237..38506b5 100644 --- a/frontend/src/langs/en.json +++ b/frontend/src/langs/en.json @@ -31,6 +31,7 @@ "default": "Default", "font": "Font", "font_size": "Font Size", + "scan_size": "Default Size for SCAN Command", "proxy": "Proxy", "use_system_proxy": "Use system proxy", "use_system_proxy_http": "Use system proxy only for HTTP(S) request", @@ -82,7 +83,10 @@ "copy_key": "Copy Key", "binary_key": "Binary Key Name", "remove_key": "Remove Key", - "new_key": "Add New Key", + "new_key": "Add Key", + "load_more": "Load More Keys", + "load_all": "Load All Keys", + "more_action": "More Action", "nonexist_tab_content": "Selected key does not exist. Please retry", "empty_server_content": "Select and open a connection from the left", "empty_server_list": "No redis server", @@ -145,6 +149,7 @@ "dbfilter_hide_title": "Select the Databases to Hide", "dbfilter_input": "Input Database Index", "dbfilter_input_tip": "Press Enter to confirm", + "load_size": "Size of Keys Per Load", "mark_color": "Mark Color" }, "ssl": { diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index a74b27b..f1aaff4 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -31,6 +31,7 @@ "default": "默认", "font": "字体", "font_size": "字体尺寸", + "scan_size": "SCAN命令默认数量", "proxy": "代理", "use_system_proxy": "使用系统代理", "use_system_proxy_http": "仅在HTTP请求时使用系统代理", @@ -83,6 +84,9 @@ "binary_key": "二进制键名", "remove_key": "删除键", "new_key": "添加新键", + "load_more": "加载更多键", + "load_all": "加载所有键", + "more_action": "更多操作", "nonexist_tab_content": "所选键不存在,请尝试刷新重试", "empty_server_content": "可以从左边选择并打开连接", "empty_server_list": "还没添加Redis服务器", @@ -145,6 +149,7 @@ "dbfilter_hide_title": "需要隐藏的数据库", "dbfilter_input": "输入数据库索引", "dbfilter_input_tip": "按回车确认", + "load_size": "单次加载键数量", "mark_color": "标记颜色" }, "ssl": { diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js index d329f57..00e48da 100644 --- a/frontend/src/stores/connections.js +++ b/frontend/src/stores/connections.js @@ -5,7 +5,6 @@ import { get, isEmpty, join, - map, remove, size, slice, @@ -30,6 +29,8 @@ import { GetConnection, GetKeyValue, ListConnection, + LoadAllKeys, + LoadNextKeys, OpenConnection, OpenDatabase, RemoveStreamValues, @@ -37,7 +38,6 @@ import { RenameKey, SaveConnection, SaveSortedConnection, - ScanKeys, ServerInfo, SetHashValue, SetKeyTTL, @@ -77,6 +77,8 @@ const useConnectionStore = defineStore('connections', { * @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 */ /** @@ -230,6 +232,7 @@ const useConnectionStore = defineStore('connections', { execTimeout: 60, dbFilterType: 'none', dbFilterList: [], + loadSize: 10000, markColor: '', ssl: { enable: false, @@ -407,7 +410,8 @@ const useConnectionStore = defineStore('connections', { key: `${name}/${db[i].name}`, label: db[i].name, name: name, - keys: db[i].keys, + keys: 0, + maxKeys: db[i].keys, db: db[i].index, type: ConnectionType.RedisDB, isLeaf: false, @@ -535,13 +539,14 @@ const useConnectionStore = defineStore('connections', { if (!success) { throw new Error(msg) } - const { keys = [] } = data + 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 { @@ -658,44 +663,77 @@ const useConnectionStore = defineStore('connections', { * scan keys with prefix * @param {string} connName * @param {number} db - * @param {string} [prefix] full reload database if prefix is null - * @returns {Promise<{keys: string[]}>} + * @param {string} match + * @param {string} matchType + * @param {boolean} [full] + * @returns {Promise<{keys: string[], end: boolean}>} */ - async scanKeys(connName, db, prefix) { - const { data, success, msg } = await ScanKeys(connName, db, prefix || '*') + 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 = [] } = data - return { keys, success } + const { keys = [], end } = data + return { keys, end, success } }, /** - * load keys with prefix + * * @param {string} connName * @param {number} db - * @param {string} [prefix] - * @returns {Promise} + * @param {string|null} prefix + * @param {string|null} matchType + * @param {boolean} [all] + * @return {Promise<{keys: Array, end: boolean}>} + * @private */ - async loadKeys(connName, db, prefix) { - let scanPrefix = prefix - if (isEmpty(scanPrefix)) { - scanPrefix = '*' + async _loadKeys(connName, db, prefix, matchType, all) { + let match = prefix + if (isEmpty(match)) { + match = '*' } else { const separator = this._getSeparator(connName) if (!endsWith(prefix, separator + '*')) { - scanPrefix = prefix + separator + '*' + match = prefix + separator + '*' } } - const { keys, success } = await this.scanKeys(connName, db, scanPrefix) - if (!success) { - return - } + 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._deleteKeyNode(connName, db, prefix, true) this._addKeyNodes(connName, db, keys) - this._tidyNode(connName, db, prefix) + 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) + // remove current keys below prefix + this._deleteKeyNode(connName, db, '', true) + this._addKeyNodes(connName, db, keys) + this._tidyNode(connName, db, '') }, /** diff --git a/frontend/src/stores/preferences.js b/frontend/src/stores/preferences.js index e04d264..fbb98c0 100644 --- a/frontend/src/stores/preferences.js +++ b/frontend/src/stores/preferences.js @@ -42,6 +42,7 @@ const usePreferencesStore = defineStore('preferences', { language: 'auto', font: '', fontSize: 14, + scanSize: 3000, useSysProxy: false, useSysProxyHttp: false, checkUpdate: false,