feat: add partial keys loading. #2

This commit is contained in:
tiny-craft 2023-10-20 18:22:53 +08:00
parent 444f643d4a
commit f3cd292af5
19 changed files with 484 additions and 142 deletions

View File

@ -6,3 +6,5 @@ const DEFAULT_WINDOW_WIDTH = 1024
const DEFAULT_WINDOW_HEIGHT = 768 const DEFAULT_WINDOW_HEIGHT = 768
const MIN_WINDOW_WIDTH = 960 const MIN_WINDOW_WIDTH = 960
const MIN_WINDOW_HEIGHT = 640 const MIN_WINDOW_HEIGHT = 640
const DEFAULT_LOAD_SIZE = 10000
const DEFAULT_SCAN_SIZE = 3000

View File

@ -16,6 +16,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"tinyrdm/backend/consts"
. "tinyrdm/backend/storage" . "tinyrdm/backend/storage"
"tinyrdm/backend/types" "tinyrdm/backend/types"
"tinyrdm/backend/utils/coll" "tinyrdm/backend/utils/coll"
@ -43,6 +44,8 @@ type connectionItem struct {
client redis.UniversalClient client redis.UniversalClient
ctx context.Context ctx context.Context
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
cursor map[int]uint64 // current cursor of databases
stepSize int64
} }
type keyItem struct { type keyItem struct {
@ -390,12 +393,13 @@ func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp typ
// OpenConnection open redis server connection // OpenConnection open redis server connection
func (c *connectionService) OpenConnection(name string) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
// get connection config // get connection config
selConn := c.conns.GetConnection(name) selConn := c.conns.GetConnection(name)
@ -522,18 +526,18 @@ func (c *connectionService) CloseConnection(name string) (resp types.JSResp) {
return 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 // if db >= 0, will also switch to db index
func (c *connectionService) getRedisClient(connName string, db int) (redis.UniversalClient, context.Context, error) { func (c *connectionService) getRedisClient(connName string, db int) (item connectionItem, err error) {
item, ok := c.connMap[connName] var ok bool
var client redis.UniversalClient var client redis.UniversalClient
var ctx context.Context if item, ok = c.connMap[connName]; ok {
if ok { client = item.client
client, ctx = item.client, item.ctx
} else { } else {
selConn := c.conns.GetConnection(connName) selConn := c.conns.GetConnection(connName)
if selConn == nil { 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) { 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) client, err = c.createRedisClient(selConn.ConnectionConfig)
if err != nil { 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 // add hook to each node in cluster mode
var cluster *redis.ClusterClient var cluster *redis.ClusterClient
@ -563,33 +567,51 @@ func (c *connectionService) getRedisClient(connName string, db int) (redis.Unive
return nil return nil
}) })
if err != 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 { } else {
client.AddHook(hook) client.AddHook(hook)
} }
if _, err = client.Ping(c.ctx).Result(); err != nil && err != redis.Nil { 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)
ctx, cancelFunc = context.WithCancel(c.ctx) item = connectionItem{
c.connMap[connName] = connectionItem{
client: client, client: client,
ctx: ctx, ctx: ctx,
cancelFunc: cancelFunc, 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 { if db >= 0 {
var rdb *redis.Client var rdb *redis.Client
if rdb, ok = client.(*redis.Client); ok && rdb != nil { if rdb, ok = client.(*redis.Client); ok && rdb != nil {
if err := rdb.Do(ctx, "select", strconv.Itoa(db)).Err(); err != nil { if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil {
return nil, nil, err 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" // 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 // ServerInfo get server info
func (c *connectionService) ServerInfo(name string) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
// get database info // get database info
res, err := client.Info(ctx).Result() res, err := client.Info(ctx).Result()
if err != nil { if err != nil {
@ -652,31 +675,43 @@ func (c *connectionService) ServerInfo(name string) (resp types.JSResp) {
// OpenDatabase open select database, and list all keys // OpenDatabase open select database, and list all keys
// @param path contain connection name and db name // @param path contain connection name and db name
func (c *connectionService) OpenDatabase(connName string, db int, match string, keyType string) (resp types.JSResp) { 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 // scan keys
func (c *connectionService) ScanKeys(connName string, db int, match, keyType string) (resp types.JSResp) { // @return loaded keys
client, ctx, err := c.getRedisClient(connName, db) // @return next cursor
if err != nil { // @return scan error
resp.Msg = err.Error() func (c *connectionService) scanKeys(ctx context.Context, client redis.UniversalClient, match, keyType string, cursor uint64, count int64) ([]any, uint64, error) {
return var err error
}
filterType := len(keyType) > 0 filterType := len(keyType) > 0
var countPerScan int64 = 10000 scanSize := int64(Preferences().GetScanSize())
// define sub scan function // define sub scan function
scan := func(ctx context.Context, cli redis.UniversalClient, appendFunc func(k any)) error { scan := func(ctx context.Context, cli redis.UniversalClient, appendFunc func(k []any)) error {
var iter *redis.ScanIterator var loadedKey []string
if filterType { var scanCount int64
iter = cli.ScanType(ctx, 0, match, countPerScan, keyType).Iterator() for {
} else { if filterType {
iter = cli.Scan(ctx, 0, match, countPerScan).Iterator() 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) { return nil
appendFunc(strutil.EncodeRedisKey(iter.Val()))
}
return iter.Err()
} }
var keys []any var keys []any
@ -684,22 +719,64 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str
// cluster mode // cluster mode
var mutex sync.Mutex var mutex sync.Mutex
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error { 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() mutex.Lock()
keys = append(keys, k) keys = append(keys, k...)
mutex.Unlock() mutex.Unlock()
}) })
}) })
} else { } else {
err = scan(ctx, client, func(k any) { err = scan(ctx, client, func(k []any) {
keys = append(keys, k) 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return 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.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
"keys": keys, "keys": keys,
@ -709,12 +786,13 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str
// GetKeyValue get value by key // GetKeyValue get value by key
func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs string) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var keyType string var keyType string
var dur time.Duration var dur time.Duration
@ -755,9 +833,10 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
case "hash": case "hash":
//value, err = client.HGetAll(ctx, key).Result() //value, err = client.HGetAll(ctx, key).Result()
items := map[string]string{} items := map[string]string{}
scanSize := int64(Preferences().GetScanSize())
for { for {
var loadedVal []string 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -774,9 +853,10 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
case "set": case "set":
//value, err = client.SMembers(ctx, key).Result() //value, err = client.SMembers(ctx, key).Result()
items := []string{} items := []string{}
scanSize := int64(Preferences().GetScanSize())
for { for {
var loadedKey []string 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -791,9 +871,10 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
case "zset": case "zset":
//value, err = client.ZRangeWithScores(ctx, key, 0, -1).Result() //value, err = client.ZRangeWithScores(ctx, key, 0, -1).Result()
var items []types.ZSetItem var items []types.ZSetItem
scanSize := int64(Preferences().GetScanSize())
for { for {
var loadedVal []string 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -848,12 +929,13 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
// SetKeyValue set value by key // SetKeyValue set value by key
// @param ttl <= 0 means keep current ttl // @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) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var expiration time.Duration var expiration time.Duration
if ttl < 0 { if ttl < 0 {
@ -971,12 +1053,13 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
// SetHashValue set hash field // SetHashValue set hash field
func (c *connectionService) SetHashValue(connName string, db int, k any, field, newField, value string) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var removedField []string var removedField []string
updatedField := map[string]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 // AddHashField add or update hash field
func (c *connectionService) AddHashField(connName string, db int, k any, action int, fieldItems []any) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
updated := map[string]any{} updated := map[string]any{}
switch action { 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 // 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) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var leftPush, rightPush []any var leftPush, rightPush []any
switch action { 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 // 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) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var removed []int64 var removed []int64
updated := map[int64]string{} 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 // 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) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
if remove { if remove {
_, err = client.SRem(ctx, key, members...).Result() _, 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 // UpdateSetItem replace member of set
func (c *connectionService) UpdateSetItem(connName string, db int, k any, value, newValue string) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, _ = client.SRem(ctx, key, value).Result() _, _ = client.SRem(ctx, key, value).Result()
_, err = client.SAdd(ctx, key, newValue).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 // 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) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
updated := map[string]any{} updated := map[string]any{}
var removed []string var removed []string
@ -1233,12 +1322,13 @@ func (c *connectionService) UpdateZSetValue(connName string, db int, k any, valu
// AddZSetValue add item to sorted set // 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) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
members := maputil.ToSlice(valueScore, func(k string) redis.Z { members := maputil.ToSlice(valueScore, func(k string) redis.Z {
return redis.Z{ return redis.Z{
@ -1266,12 +1356,13 @@ func (c *connectionService) AddZSetValue(connName string, db int, k any, action
// AddStreamValue add stream field // AddStreamValue add stream field
func (c *connectionService) AddStreamValue(connName string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, err = client.XAdd(ctx, &redis.XAddArgs{ _, err = client.XAdd(ctx, &redis.XAddArgs{
Stream: key, Stream: key,
@ -1289,12 +1380,13 @@ func (c *connectionService) AddStreamValue(connName string, db int, k any, ID st
// RemoveStreamValues remove stream values by id // RemoveStreamValues remove stream values by id
func (c *connectionService) RemoveStreamValues(connName string, db int, k any, IDs []string) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, err = client.XDel(ctx, key, IDs...).Result() _, err = client.XDel(ctx, key, IDs...).Result()
resp.Success = true resp.Success = true
@ -1303,12 +1395,13 @@ func (c *connectionService) RemoveStreamValues(connName string, db int, k any, I
// SetKeyTTL set ttl of key // SetKeyTTL set ttl of key
func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var expiration time.Duration var expiration time.Duration
if ttl < 0 { if ttl < 0 {
@ -1330,12 +1423,13 @@ func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64)
// DeleteKey remove redis key // DeleteKey remove redis key
func (c *connectionService) DeleteKey(connName string, db int, k any, async bool) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var deletedKeys []string var deletedKeys []string
if strings.HasSuffix(key, "*") { if strings.HasSuffix(key, "*") {
@ -1360,7 +1454,8 @@ func (c *connectionService) DeleteKey(connName string, db int, k any, async bool
return nil 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) resultKeys := make([]string, 0, 100)
for iter.Next(ctx) { for iter.Next(ctx) {
resultKeys = append(resultKeys, iter.Val()) resultKeys = append(resultKeys, iter.Val())
@ -1414,12 +1509,13 @@ func (c *connectionService) DeleteKey(connName string, db int, k any, async bool
// RenameKey rename key // RenameKey rename key
func (c *connectionService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) { 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 { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx
if _, ok := client.(*redis.ClusterClient); ok { if _, ok := client.(*redis.ClusterClient); ok {
resp.Msg = "RENAME not support in cluster mode yet" resp.Msg = "RENAME not support in cluster mode yet"
return return

View File

@ -133,6 +133,15 @@ func (p *preferencesService) GetWindowSize() (width, height int) {
return 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 { type latestRelease struct {
Name string `json:"name"` Name string `json:"name"`
TagName string `json:"tag_name"` TagName string `json:"tag_name"`

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sync" "sync"
"tinyrdm/backend/consts"
"tinyrdm/backend/types" "tinyrdm/backend/types"
sliceutil "tinyrdm/backend/utils/slice" sliceutil "tinyrdm/backend/utils/slice"
) )
@ -36,6 +37,7 @@ func (c *ConnectionsStorage) defaultConnectionItem() types.ConnectionConfig {
ExecTimeout: 60, ExecTimeout: 60,
DBFilterType: "none", DBFilterType: "none",
DBFilterList: []int{}, DBFilterList: []int{},
LoadSize: consts.DEFAULT_LOAD_SIZE,
MarkColor: "", MarkColor: "",
Sentinel: types.ConnectionSentinel{ Sentinel: types.ConnectionSentinel{
Master: "mymaster", Master: "mymaster",

View File

@ -46,6 +46,7 @@ func (p *PreferencesStorage) GetPreferences() (ret types.Preferences) {
defer p.mutex.Unlock() defer p.mutex.Unlock()
ret = p.getPreferences() 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.AsideWidth = max(ret.Behavior.AsideWidth, consts.DEFAULT_ASIDE_WIDTH)
ret.Behavior.WindowWidth = max(ret.Behavior.WindowWidth, consts.MIN_WINDOW_WIDTH) ret.Behavior.WindowWidth = max(ret.Behavior.WindowWidth, consts.MIN_WINDOW_WIDTH)
ret.Behavior.WindowHeight = max(ret.Behavior.WindowHeight, consts.MIN_WINDOW_HEIGHT) ret.Behavior.WindowHeight = max(ret.Behavior.WindowHeight, consts.MIN_WINDOW_HEIGHT)

View File

@ -15,6 +15,7 @@ type ConnectionConfig struct {
ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"` ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"`
DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"` DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"`
DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,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"` MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"`
SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"` SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"`
SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"` SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`

View File

@ -19,6 +19,7 @@ func NewPreferences() Preferences {
Theme: "auto", Theme: "auto",
Language: "auto", Language: "auto",
FontSize: consts.DEFAULT_FONT_SIZE, FontSize: consts.DEFAULT_FONT_SIZE,
ScanSize: consts.DEFAULT_SCAN_SIZE,
CheckUpdate: true, CheckUpdate: true,
}, },
Editor: PreferencesEditor{ Editor: PreferencesEditor{
@ -38,6 +39,7 @@ type PreferencesGeneral struct {
Language string `json:"language" yaml:"language"` Language string `json:"language" yaml:"language"`
Font string `json:"font" yaml:"font,omitempty"` Font string `json:"font" yaml:"font,omitempty"`
FontSize int `json:"fontSize" yaml:"font_size"` FontSize int `json:"fontSize" yaml:"font_size"`
ScanSize int `json:"scanSize" yaml:"scan_size"`
UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"` UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"`
UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"` UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"`
CheckUpdate bool `json:"checkUpdate" yaml:"check_update"` CheckUpdate bool `json:"checkUpdate" yaml:"check_update"`

View File

@ -370,6 +370,9 @@ const onClose = () => {
</template> </template>
</n-input-number> </n-input-number>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="12" :label="$t('dialogue.connection.advn.load_size')">
<n-input-number v-model:value="generalForm.loadSize" :min="0" />
</n-form-item-gi>
<n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_type')"> <n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_type')">
<n-radio-group <n-radio-group
v-model:value="generalForm.dbFilterType" v-model:value="generalForm.dbFilterType"

View File

@ -74,11 +74,11 @@ const onClose = () => {
:show-require-mark="false" :show-require-mark="false"
label-placement="top" label-placement="top"
style="padding-right: 15px"> style="padding-right: 15px">
<n-form-item :label="$t('server')" path="key"> <n-form-item :label="$t('dialogue.key.server')" path="key">
<n-text>{{ filterForm.server }}</n-text> <n-input :value="filterForm.server" readonly></n-input>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.key.db_index')" path="db"> <n-form-item :label="$t('dialogue.key.db_index')" path="db">
<n-text>{{ filterForm.db }}</n-text> <n-input :value="filterForm.db + ''" readonly></n-input>
</n-form-item> </n-form-item>
<n-form-item :label="$t('interface.type')" path="type" required> <n-form-item :label="$t('interface.type')" path="type" required>
<n-select v-model:value="filterForm.type" :options="typeOptions" /> <n-select v-model:value="filterForm.type" :options="typeOptions" />
@ -87,7 +87,10 @@ const onClose = () => {
<n-input-group> <n-input-group>
<n-tooltip trigger="focus"> <n-tooltip trigger="focus">
<template #trigger> <template #trigger>
<n-input v-model:value="filterForm.pattern" clearable placeholder="Filter Pattern" /> <n-input
v-model:value="filterForm.pattern"
clearable
:placeholder="$t('dialogue.filter.filter_pattern')" />
</template> </template>
<div class="text-block">{{ $t('dialogue.filter.filter_pattern_tip') }}</div> <div class="text-block">{{ $t('dialogue.filter.filter_pattern_tip') }}</div>
</n-tooltip> </n-tooltip>

View File

@ -60,46 +60,58 @@ const onClose = () => {
:show-icon="false" :show-icon="false"
:title="$t('preferences.name')" :title="$t('preferences.name')"
preset="dialog" preset="dialog"
style="width: 500px"
transform-origin="center"> transform-origin="center">
<!-- FIXME: set loading will slow down appear animation of dialog in linux --> <!-- FIXME: set loading will slow down appear animation of dialog in linux -->
<!-- <n-spin :show="loading"> --> <!-- <n-spin :show="loading"> -->
<n-tabs v-model:value="tab" animated type="line"> <n-tabs v-model:value="tab" animated type="line">
<n-tab-pane :tab="$t('preferences.general.name')" display-directive="show" name="general"> <n-tab-pane :tab="$t('preferences.general.name')" display-directive="show" name="general">
<n-form :disabled="loading" :model="prefStore.general" :show-require-mark="false" label-placement="top"> <n-form :disabled="loading" :model="prefStore.general" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('preferences.general.theme')" required> <n-grid :x-gap="10">
<n-radio-group v-model:value="prefStore.general.theme" name="theme" size="medium"> <n-form-item-gi :span="24" :label="$t('preferences.general.theme')" required>
<n-radio-button v-for="opt in prefStore.themeOption" :key="opt.value" :value="opt.value"> <n-radio-group v-model:value="prefStore.general.theme" name="theme" size="medium">
{{ opt.label }} <n-radio-button
</n-radio-button> v-for="opt in prefStore.themeOption"
</n-radio-group> :key="opt.value"
</n-form-item> :value="opt.value">
<n-form-item :label="$t('preferences.general.language')" required> {{ opt.label }}
<n-select </n-radio-button>
v-model:value="prefStore.general.language" </n-radio-group>
:options="prefStore.langOption" </n-form-item-gi>
filterable /> <n-form-item-gi :span="24" :label="$t('preferences.general.language')" required>
</n-form-item> <n-select
<n-form-item :label="$t('preferences.general.font')" required> v-model:value="prefStore.general.language"
<n-select v-model:value="prefStore.general.font" :options="prefStore.fontOption" filterable /> :options="prefStore.langOption"
</n-form-item> filterable />
<n-form-item :label="$t('preferences.general.font_size')"> </n-form-item-gi>
<n-input-number v-model:value="prefStore.general.fontSize" :max="65535" :min="1" /> <n-form-item-gi :span="12" :label="$t('preferences.general.font')" required>
</n-form-item> <n-select
<n-form-item :label="$t('preferences.general.proxy')"> v-model:value="prefStore.general.font"
<n-space> :options="prefStore.fontOption"
<n-checkbox v-model:checked="prefStore.general.useSysProxy"> filterable />
{{ $t('preferences.general.use_system_proxy') }} </n-form-item-gi>
<n-form-item-gi :span="12" :label="$t('preferences.general.font_size')">
<n-input-number v-model:value="prefStore.general.fontSize" :max="65535" :min="1" />
</n-form-item-gi>
<n-form-item-gi :span="12" :label="$t('preferences.general.scan_size')">
<n-input-number v-model:value="prefStore.general.scanSize" :min="1" />
</n-form-item-gi>
<n-form-item-gi :span="24" :label="$t('preferences.general.proxy')">
<n-space>
<n-checkbox v-model:checked="prefStore.general.useSysProxy">
{{ $t('preferences.general.use_system_proxy') }}
</n-checkbox>
<n-checkbox v-model:checked="prefStore.general.useSysProxyHttp">
{{ $t('preferences.general.use_system_proxy_http') }}
</n-checkbox>
</n-space>
</n-form-item-gi>
<n-form-item-gi :span="24" :label="$t('preferences.general.update')">
<n-checkbox v-model:checked="prefStore.general.checkUpdate">
{{ $t('preferences.general.auto_check_update') }}
</n-checkbox> </n-checkbox>
<n-checkbox v-model:checked="prefStore.general.useSysProxyHttp"> </n-form-item-gi>
{{ $t('preferences.general.use_system_proxy_http') }} </n-grid>
</n-checkbox>
</n-space>
</n-form-item>
<n-form-item :label="$t('preferences.general.update')">
<n-checkbox v-model:checked="prefStore.general.checkUpdate">
{{ $t('preferences.general.auto_check_update') }}
</n-checkbox>
</n-form-item>
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>

View File

@ -0,0 +1,25 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M23.9999 29.0001L12 17.0001L19.9999 17.0001L19.9999 6.00011L27.9999 6.00011L27.9999 17.0001L35.9999 17.0001L23.9999 29.0001Z"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path d="M42 37L6 37" stroke="currentColor" :stroke-width="props.strokeWidth" stroke-linecap="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,51 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8 28H24"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M8 37H24"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M8 19H40"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M8 10H40"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M37 40V28"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M33 36L37 40L41 36"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,24 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
fill="none"
stroke="currentColor"
:stroke-width="strokeWidth"
stroke-linejoin="round" />
<circle cx="14" cy="24" r="3" fill="currentColor" />
<circle cx="24" cy="24" r="3" fill="currentColor" />
<circle cx="34" cy="24" r="3" fill="currentColor" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -23,6 +23,8 @@ import { typesBgColor, typesColor } from '@/consts/support_redis_type.js'
import useTabStore from 'stores/tab.js' import useTabStore from 'stores/tab.js'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import { parseHexColor } from '@/utils/rgb.js' import { parseHexColor } from '@/utils/rgb.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -146,11 +148,11 @@ const menuOptions = {
} }
}, },
[ConnectionType.RedisKey]: () => [ [ConnectionType.RedisKey]: () => [
{ // {
key: 'key_reload', // key: 'key_reload',
label: i18n.t('interface.reload'), // label: i18n.t('interface.reload'),
icon: renderIcon(Refresh), // icon: renderIcon(Refresh),
}, // },
{ {
key: 'key_newkey', key: 'key_newkey',
label: i18n.t('interface.new_key'), label: i18n.t('interface.new_key'),
@ -259,9 +261,9 @@ const handleSelectContextMenu = (key) => {
const { match: pattern, type } = connectionStore.getKeyFilter(props.server, db) const { match: pattern, type } = connectionStore.getKeyFilter(props.server, db)
dialogStore.openKeyFilterDialog(props.server, db, pattern, type) dialogStore.openKeyFilterDialog(props.server, db, pattern, type)
break break
case 'key_reload': // case 'key_reload':
connectionStore.loadKeys(props.server, db, redisKey) // connectionStore.loadKeys(props.server, db, redisKey)
break // break
case 'value_reload': case 'value_reload':
connectionStore.loadKeyValue(props.server, db, redisKey) connectionStore.loadKeyValue(props.server, db, redisKey)
break break
@ -289,6 +291,38 @@ const handleSelectContextMenu = (key) => {
$message.error(e.message) $message.error(e.message)
}) })
break 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: default:
console.warn('TODO: handle context menu:' + key) console.warn('TODO: handle context menu:' + key)
} }
@ -389,9 +423,14 @@ const renderLabel = ({ option }) => {
case ConnectionType.Server: case ConnectionType.Server:
return h('b', {}, { default: () => option.label }) return h('b', {}, { default: () => option.label })
case ConnectionType.RedisDB: case ConnectionType.RedisDB:
const { name: server, db } = option const { name: server, db, opened = false } = option
let { match: matchPattern, type: typeFilter } = connectionStore.getKeyFilter(server, db) 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 // show filter tag after label
// type filter tag // type filter tag
if (!isEmpty(typeFilter)) { if (!isEmpty(typeFilter)) {
@ -460,30 +499,53 @@ const renderIconMenu = (items) => {
) )
} }
const getDatabaseMenu = (opened) => { const getDatabaseMenu = (opened, loading, end) => {
const btns = [] const btns = []
if (opened) { if (opened) {
btns.push( btns.push(
h(IconButton, { h(IconButton, {
tTooltip: 'interface.filter_key', tTooltip: 'interface.filter_key',
icon: Filter, icon: Filter,
disabled: loading === true,
onClick: () => handleSelectContextMenu('db_filter'), onClick: () => handleSelectContextMenu('db_filter'),
}), }),
h(IconButton, { h(IconButton, {
tTooltip: 'interface.reload', tTooltip: 'interface.reload',
icon: Refresh, icon: Refresh,
disabled: loading === true,
onClick: () => handleSelectContextMenu('db_reload'), onClick: () => handleSelectContextMenu('db_reload'),
}), }),
h(IconButton, { h(IconButton, {
tTooltip: 'interface.new_key', tTooltip: 'interface.new_key',
icon: Add, icon: Add,
disabled: loading === true,
onClick: () => handleSelectContextMenu('db_newkey'), 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, { h(IconButton, {
tTooltip: 'interface.batch_delete', tTooltip: 'interface.batch_delete',
icon: Delete, icon: Delete,
disabled: loading === true,
onClick: () => handleSelectContextMenu('key_remove'), onClick: () => handleSelectContextMenu('key_remove'),
}), }),
// h(IconButton, {
// tTooltip: 'interface.more_action',
// icon: More,
// onClick: () => handleSelectContextMenu('more_action'),
// }),
) )
} else { } else {
btns.push( btns.push(
@ -499,11 +561,12 @@ const getDatabaseMenu = (opened) => {
const getLayerMenu = () => { const getLayerMenu = () => {
return [ return [
h(IconButton, { // disable reload by layer, due to conflict with partial loading keys
tTooltip: 'interface.reload', // h(IconButton, {
icon: Refresh, // tTooltip: 'interface.reload',
onClick: () => handleSelectContextMenu('key_reload'), // icon: Refresh,
}), // onClick: () => handleSelectContextMenu('key_reload'),
// }),
h(IconButton, { h(IconButton, {
tTooltip: 'interface.new_key', tTooltip: 'interface.new_key',
icon: Add, icon: Add,
@ -532,7 +595,7 @@ const renderSuffix = ({ option }) => {
if ((option.type === ConnectionType.RedisDB && option.opened) || includes(selectedKeys.value, option.key)) { if ((option.type === ConnectionType.RedisDB && option.opened) || includes(selectedKeys.value, option.key)) {
switch (option.type) { switch (option.type) {
case ConnectionType.RedisDB: case ConnectionType.RedisDB:
return renderIconMenu(getDatabaseMenu(option.opened)) return renderIconMenu(getDatabaseMenu(option.opened, option.loading, option.fullLoaded))
case ConnectionType.RedisKey: case ConnectionType.RedisKey:
return renderIconMenu(getLayerMenu()) return renderIconMenu(getLayerMenu())
case ConnectionType.RedisValue: case ConnectionType.RedisValue:

View File

@ -10,7 +10,6 @@ import useDialogStore from 'stores/dialog.js'
import Github from '@/components/icons/Github.vue' import Github from '@/components/icons/Github.vue'
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js' import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import Help from '@/components/icons/Help.vue'
import usePreferencesStore from 'stores/preferences.js' import usePreferencesStore from 'stores/preferences.js'
import Record from '@/components/icons/Record.vue' import Record from '@/components/icons/Record.vue'

View File

@ -31,6 +31,7 @@
"default": "Default", "default": "Default",
"font": "Font", "font": "Font",
"font_size": "Font Size", "font_size": "Font Size",
"scan_size": "Default Size for SCAN Command",
"proxy": "Proxy", "proxy": "Proxy",
"use_system_proxy": "Use system proxy", "use_system_proxy": "Use system proxy",
"use_system_proxy_http": "Use system proxy only for HTTP(S) request", "use_system_proxy_http": "Use system proxy only for HTTP(S) request",
@ -82,7 +83,10 @@
"copy_key": "Copy Key", "copy_key": "Copy Key",
"binary_key": "Binary Key Name", "binary_key": "Binary Key Name",
"remove_key": "Remove Key", "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", "nonexist_tab_content": "Selected key does not exist. Please retry",
"empty_server_content": "Select and open a connection from the left", "empty_server_content": "Select and open a connection from the left",
"empty_server_list": "No redis server", "empty_server_list": "No redis server",
@ -145,6 +149,7 @@
"dbfilter_hide_title": "Select the Databases to Hide", "dbfilter_hide_title": "Select the Databases to Hide",
"dbfilter_input": "Input Database Index", "dbfilter_input": "Input Database Index",
"dbfilter_input_tip": "Press Enter to confirm", "dbfilter_input_tip": "Press Enter to confirm",
"load_size": "Size of Keys Per Load",
"mark_color": "Mark Color" "mark_color": "Mark Color"
}, },
"ssl": { "ssl": {

View File

@ -31,6 +31,7 @@
"default": "默认", "default": "默认",
"font": "字体", "font": "字体",
"font_size": "字体尺寸", "font_size": "字体尺寸",
"scan_size": "SCAN命令默认数量",
"proxy": "代理", "proxy": "代理",
"use_system_proxy": "使用系统代理", "use_system_proxy": "使用系统代理",
"use_system_proxy_http": "仅在HTTP请求时使用系统代理", "use_system_proxy_http": "仅在HTTP请求时使用系统代理",
@ -83,6 +84,9 @@
"binary_key": "二进制键名", "binary_key": "二进制键名",
"remove_key": "删除键", "remove_key": "删除键",
"new_key": "添加新键", "new_key": "添加新键",
"load_more": "加载更多键",
"load_all": "加载所有键",
"more_action": "更多操作",
"nonexist_tab_content": "所选键不存在,请尝试刷新重试", "nonexist_tab_content": "所选键不存在,请尝试刷新重试",
"empty_server_content": "可以从左边选择并打开连接", "empty_server_content": "可以从左边选择并打开连接",
"empty_server_list": "还没添加Redis服务器", "empty_server_list": "还没添加Redis服务器",
@ -145,6 +149,7 @@
"dbfilter_hide_title": "需要隐藏的数据库", "dbfilter_hide_title": "需要隐藏的数据库",
"dbfilter_input": "输入数据库索引", "dbfilter_input": "输入数据库索引",
"dbfilter_input_tip": "按回车确认", "dbfilter_input_tip": "按回车确认",
"load_size": "单次加载键数量",
"mark_color": "标记颜色" "mark_color": "标记颜色"
}, },
"ssl": { "ssl": {

View File

@ -5,7 +5,6 @@ import {
get, get,
isEmpty, isEmpty,
join, join,
map,
remove, remove,
size, size,
slice, slice,
@ -30,6 +29,8 @@ import {
GetConnection, GetConnection,
GetKeyValue, GetKeyValue,
ListConnection, ListConnection,
LoadAllKeys,
LoadNextKeys,
OpenConnection, OpenConnection,
OpenDatabase, OpenDatabase,
RemoveStreamValues, RemoveStreamValues,
@ -37,7 +38,6 @@ import {
RenameKey, RenameKey,
SaveConnection, SaveConnection,
SaveSortedConnection, SaveSortedConnection,
ScanKeys,
ServerInfo, ServerInfo,
SetHashValue, SetHashValue,
SetKeyTTL, SetKeyTTL,
@ -77,6 +77,8 @@ const useConnectionStore = defineStore('connections', {
* @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only * @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
* @property {boolean} [expanded] - current node is expanded * @property {boolean} [expanded] - current node is expanded
* @property {DatabaseItem[]} [children] * @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, execTimeout: 60,
dbFilterType: 'none', dbFilterType: 'none',
dbFilterList: [], dbFilterList: [],
loadSize: 10000,
markColor: '', markColor: '',
ssl: { ssl: {
enable: false, enable: false,
@ -407,7 +410,8 @@ const useConnectionStore = defineStore('connections', {
key: `${name}/${db[i].name}`, key: `${name}/${db[i].name}`,
label: db[i].name, label: db[i].name,
name: name, name: name,
keys: db[i].keys, keys: 0,
maxKeys: db[i].keys,
db: db[i].index, db: db[i].index,
type: ConnectionType.RedisDB, type: ConnectionType.RedisDB,
isLeaf: false, isLeaf: false,
@ -535,13 +539,14 @@ const useConnectionStore = defineStore('connections', {
if (!success) { if (!success) {
throw new Error(msg) throw new Error(msg)
} }
const { keys = [] } = data const { keys = [], end = false } = data
const selDB = this.getDatabase(connName, db) const selDB = this.getDatabase(connName, db)
if (selDB == null) { if (selDB == null) {
return return
} }
selDB.opened = true selDB.opened = true
selDB.fullLoaded = end
if (isEmpty(keys)) { if (isEmpty(keys)) {
selDB.children = [] selDB.children = []
} else { } else {
@ -658,44 +663,77 @@ const useConnectionStore = defineStore('connections', {
* scan keys with prefix * scan keys with prefix
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {string} [prefix] full reload database if prefix is null * @param {string} match
* @returns {Promise<{keys: string[]}>} * @param {string} matchType
* @param {boolean} [full]
* @returns {Promise<{keys: string[], end: boolean}>}
*/ */
async scanKeys(connName, db, prefix) { async scanKeys(connName, db, match, matchType, full) {
const { data, success, msg } = await ScanKeys(connName, db, prefix || '*') 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) { if (!success) {
throw new Error(msg) throw new Error(msg)
} }
const { keys = [] } = data const { keys = [], end } = data
return { keys, success } return { keys, end, success }
}, },
/** /**
* load keys with prefix *
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {string} [prefix] * @param {string|null} prefix
* @returns {Promise<void>} * @param {string|null} matchType
* @param {boolean} [all]
* @return {Promise<{keys: Array<string|number[]>, end: boolean}>}
* @private
*/ */
async loadKeys(connName, db, prefix) { async _loadKeys(connName, db, prefix, matchType, all) {
let scanPrefix = prefix let match = prefix
if (isEmpty(scanPrefix)) { if (isEmpty(match)) {
scanPrefix = '*' match = '*'
} else { } else {
const separator = this._getSeparator(connName) const separator = this._getSeparator(connName)
if (!endsWith(prefix, separator + '*')) { if (!endsWith(prefix, separator + '*')) {
scanPrefix = prefix + separator + '*' match = prefix + separator + '*'
} }
} }
const { keys, success } = await this.scanKeys(connName, db, scanPrefix) return this.scanKeys(connName, db, match, matchType, all)
if (!success) { },
return
}
/**
* load more keys within the database
* @param {string} connName
* @param {number} db
* @return {Promise<boolean>}
*/
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 // remove current keys below prefix
this._deleteKeyNode(connName, db, prefix, true)
this._addKeyNodes(connName, db, keys) 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<void>}
*/
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, '')
}, },
/** /**

View File

@ -42,6 +42,7 @@ const usePreferencesStore = defineStore('preferences', {
language: 'auto', language: 'auto',
font: '', font: '',
fontSize: 14, fontSize: 14,
scanSize: 3000,
useSysProxy: false, useSysProxy: false,
useSysProxyHttp: false, useSysProxyHttp: false,
checkUpdate: false, checkUpdate: false,