Compare commits

..

10 Commits

Author SHA1 Message Date
tiny-craft 21c63e2ac2 chore: update doc and dependencies 2023-11-10 16:34:40 +08:00
tiny-craft 65e077c0c0 fix: compatibility with older Redis versions without UNLINK command support #69
perf: add waiting indicator for deleting keys and flushing database
2023-11-10 16:26:12 +08:00
tiny-craft 5a58a57cd5 Merge remote-tracking branch 'origin/main' 2023-11-10 15:25:20 +08:00
Lykin 73c14204b7
Merge pull request #71 from leognutzmann/main
feat: Portuguese language support
2023-11-10 15:19:28 +08:00
tiny-craft 7b3c3df230 feat: add key layer reload/refresh back after database full loaded #72
perf: update database max keys each load next/all.
2023-11-10 13:01:18 +08:00
tiny-craft fa4929cd63 perf: add reset handle for all content components 2023-11-09 20:31:07 +08:00
tiny-craft 352e7b714d refactor: optimize the key renaming logic 2023-11-09 14:42:35 +08:00
tiny-craft 9618990de8 feat: add partial entries loading for complex type(list/hash/set/zset/stream) #70
refactor: split "key value loading" into "key summary loading" and "key detail loading"
2023-11-09 14:42:35 +08:00
tiny-craft f2ebd7f358 chore: update dependencies 2023-11-08 00:37:30 +08:00
Leonardo Gnutzmann 3d0310a9c1 feat: add portuguese 2023-11-06 16:23:11 -03:00
33 changed files with 2017 additions and 563 deletions

View File

@ -1,6 +1,6 @@
## Tiny RDM Contribute Guide ## Tiny RDM Contribute Guide
### Multi-language Contributions {#language} ### Multi-language Contributions
#### Adding New Language #### Adding New Language
@ -21,7 +21,7 @@
``` ```
4. Submit review once there are no issues with the translation context in the application. (learn how to submit) 4. Submit review once there are no issues with the translation context in the application. (learn how to submit)
### Code Submission`(To be completed)` {#pull_request} ### Code Submission`(To be completed)`
#### Pull Request Title #### Pull Request Title
The format of PR's title like "<type>: <description>" The format of PR's title like "<type>: <description>"

View File

@ -1,6 +1,6 @@
## Tiny RDM 代码贡献指南 ## Tiny RDM 代码贡献指南
### 多国语言贡献 {#language} ### 多国语言贡献
#### 增加新的语言 #### 增加新的语言
1. 创建文件:在[frontend/src/langs](../frontend/src/langs/)目录下新增语言配置JSON文件文件名格式为“{语言}-{地区}.json”如英文为“en-us.json”简体中文为“zh-cn.json”建议直接复制[en-us.json](../frontend/src/langs/en-us.json)文件进行改名。 1. 创建文件:在[frontend/src/langs](../frontend/src/langs/)目录下新增语言配置JSON文件文件名格式为“{语言}-{地区}.json”如英文为“en-us.json”简体中文为“zh-cn.json”建议直接复制[en-us.json](../frontend/src/langs/en-us.json)文件进行改名。
@ -19,7 +19,7 @@
``` ```
4. 检查应用中对应翻译语境无问题后,可提交审核([查看如何提交](#pull_request) 4. 检查应用中对应翻译语境无问题后,可提交审核([查看如何提交](#pull_request)
### 代码提交`(待完善)` {#pull_request} ### 代码提交`(待完善)`
#### PR提交规范 #### PR提交规范
PR提交格式为“<type>: <description> PR提交格式为“<type>: <description>

View File

@ -29,11 +29,21 @@ type slowLogItem struct {
Cost int64 `json:"cost"` Cost int64 `json:"cost"`
} }
type entryCursor struct {
DB int
Type string
Key string
Pattern string
Cursor uint64
XLast string // last stream pos
}
type connectionItem struct { 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 cursor map[int]uint64 // current cursor of databases
entryCursor map[int]entryCursor // current entry cursor of databases
stepSize int64 stepSize int64
} }
@ -265,6 +275,7 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
ctx: ctx, ctx: ctx,
cancelFunc: cancelFunc, cancelFunc: cancelFunc,
cursor: map[int]uint64{}, cursor: map[int]uint64{},
entryCursor: map[int]entryCursor{},
stepSize: int64(selConn.LoadSize), stepSize: int64(selConn.LoadSize),
} }
if item.stepSize <= 0 { if item.stepSize <= 0 {
@ -479,14 +490,355 @@ func (b *browserService) LoadAllKeys(connName string, db int, match, keyType str
return return
} }
b.setClientCursor(connName, db, 0) b.setClientCursor(connName, db, 0)
maxKeys := b.loadDBSize(ctx, client)
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
"keys": keys, "keys": keys,
"maxKeys": maxKeys,
} }
return return
} }
// GetKeySummary get key summary info
func (b *browserService) GetKeySummary(param types.KeySummaryParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB)
if err != nil {
resp.Msg = err.Error()
return
}
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key)
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 data types.KeySummary
data.Type = strings.ToLower(keyType)
if dur, err = client.TTL(ctx, key).Result(); err != nil {
data.TTL = -1
} else {
if dur < 0 {
data.TTL = -1
} else {
data.TTL = int64(dur.Seconds())
}
}
data.Size, _ = client.MemoryUsage(ctx, key, 0).Result()
switch data.Type {
case "string":
data.Length, _ = client.StrLen(ctx, key).Result()
case "list":
data.Length, _ = client.LLen(ctx, key).Result()
case "hash":
data.Length, _ = client.HLen(ctx, key).Result()
case "set":
data.Length, _ = client.SCard(ctx, key).Result()
case "zset":
data.Length, _ = client.ZCard(ctx, key).Result()
case "stream":
data.Length, _ = client.XLen(ctx, key).Result()
default:
resp.Msg = "unknown key type"
return
}
resp.Success = true
resp.Data = data
return
}
// GetKeyDetail get key detail
func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB)
if err != nil {
resp.Msg = err.Error()
return
}
client, ctx, entryCors := item.client, item.ctx, item.entryCursor
key := strutil.DecodeRedisKey(param.Key)
var keyType string
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 data types.KeyDetail
//var cursor uint64
matchPattern := param.MatchPattern
if len(matchPattern) <= 0 {
matchPattern = "*"
}
// define get entry cursor function
getEntryCursor := func() (uint64, string) {
if entry, ok := entryCors[param.DB]; !ok || entry.Key != key || entry.Pattern != matchPattern {
// not the same key or match pattern, reset cursor
entry = entryCursor{
DB: param.DB,
Key: key,
Pattern: matchPattern,
Cursor: 0,
}
entryCors[param.DB] = entry
return 0, ""
} else {
return entry.Cursor, entry.XLast
}
}
// define set entry cursor function
setEntryCursor := func(cursor uint64) {
entryCors[param.DB] = entryCursor{
DB: param.DB,
Type: "",
Key: key,
Pattern: matchPattern,
Cursor: cursor,
}
}
// define set last stream pos function
setEntryXLast := func(last string) {
entryCors[param.DB] = entryCursor{
DB: param.DB,
Type: "",
Key: key,
Pattern: matchPattern,
XLast: last,
}
}
switch strings.ToLower(keyType) {
case "string":
var str string
str, err = client.Get(ctx, key).Result()
data.Value, data.DecodeType, data.ViewAs = strutil.ConvertTo(str, param.DecodeType, param.ViewAs)
case "list":
loadListHandle := func() ([]string, bool, error) {
var items []string
var cursor uint64
if param.Full {
// load all
cursor = 0
items, err = client.LRange(ctx, key, 0, -1).Result()
} else {
cursor, _ = getEntryCursor()
scanSize := int64(Preferences().GetScanSize())
items, err = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result()
cursor = cursor + uint64(scanSize)
if len(items) < int(scanSize) {
cursor = 0
}
}
setEntryCursor(cursor)
if err != nil {
return items, false, err
}
return items, cursor == 0, nil
}
data.Value, data.End, err = loadListHandle()
case "hash":
loadHashHandle := func() (map[string]string, bool, error) {
items := map[string]string{}
scanSize := int64(Preferences().GetScanSize())
var loadedVal []string
var cursor uint64
if param.Full {
// load all
cursor = 0
for {
loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", scanSize).Result()
if err != nil {
return nil, false, err
}
for i := 0; i < len(loadedVal); i += 2 {
items[loadedVal[i]] = loadedVal[i+1]
}
if cursor == 0 {
break
}
}
} else {
cursor, _ = getEntryCursor()
loadedVal, cursor, err = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result()
if err != nil {
return nil, false, err
}
for i := 0; i < len(loadedVal); i += 2 {
items[loadedVal[i]] = loadedVal[i+1]
}
}
setEntryCursor(cursor)
return items, cursor == 0, nil
}
data.Value, data.End, err = loadHashHandle()
if err != nil {
resp.Msg = err.Error()
return
}
case "set":
loadSetHandle := func() ([]string, bool, error) {
var items []string
var cursor uint64
scanSize := int64(Preferences().GetScanSize())
var loadedKey []string
if param.Full {
// load all
cursor = 0
for {
loadedKey, cursor, err = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
if err != nil {
return items, false, err
}
items = append(items, loadedKey...)
if cursor == 0 {
break
}
}
} else {
cursor, _ = getEntryCursor()
loadedKey, cursor, err = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
items = append(items, loadedKey...)
}
setEntryCursor(cursor)
return items, cursor == 0, nil
}
data.Value, data.End, err = loadSetHandle()
if err != nil {
resp.Msg = err.Error()
return
}
case "zset":
loadZSetHandle := func() ([]types.ZSetItem, bool, error) {
var items []types.ZSetItem
var cursor uint64
scanSize := int64(Preferences().GetScanSize())
var loadedVal []string
if param.Full {
// load all
cursor = 0
for {
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
if err != nil {
return items, false, err
}
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
}
}
} else {
cursor, _ = getEntryCursor()
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
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,
})
}
}
}
setEntryCursor(cursor)
return items, cursor == 0, nil
}
data.Value, data.End, err = loadZSetHandle()
if err != nil {
resp.Msg = err.Error()
return
}
case "stream":
loadStreamHandle := func() ([]types.StreamItem, bool, error) {
var msgs []redis.XMessage
var items []types.StreamItem
var last string
if param.Full {
// load all
last = ""
msgs, err = client.XRevRange(ctx, key, "+", "-").Result()
} else {
scanSize := int64(Preferences().GetScanSize())
_, last = getEntryCursor()
if len(last) <= 0 {
last = "+"
}
if last != "+" {
// add 1 more item when continue scan
msgs, err = client.XRevRangeN(ctx, key, last, "-", scanSize+1).Result()
msgs = msgs[1:]
} else {
msgs, err = client.XRevRangeN(ctx, key, last, "-", scanSize).Result()
}
scanCount := len(msgs)
if scanCount <= 0 || scanCount < int(scanSize) {
last = ""
} else if scanCount > 0 {
last = msgs[scanCount-1].ID
}
}
setEntryXLast(last)
for _, msg := range msgs {
items = append(items, types.StreamItem{
ID: msg.ID,
Value: msg.Values,
})
}
if err != nil {
return items, false, err
}
return items, last == "", nil
}
data.Value, data.End, err = loadStreamHandle()
if err != nil {
resp.Msg = err.Error()
return
}
}
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true
resp.Data = data
return
}
// GetKeyValue get value by key // GetKeyValue get value by key
func (b *browserService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) { func (b *browserService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(connName, db)
@ -774,6 +1126,7 @@ func (b *browserService) SetHashValue(connName string, db int, k any, field, new
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var removedField []string var removedField []string
updatedField := map[string]string{} updatedField := map[string]string{}
replacedField := map[string]string{}
if len(field) <= 0 { if len(field) <= 0 {
// old filed is empty, add new field // old filed is empty, add new field
_, err = client.HSet(ctx, key, newField, value).Result() _, err = client.HSet(ctx, key, newField, value).Result()
@ -795,6 +1148,7 @@ func (b *browserService) SetHashValue(connName string, db int, k any, field, new
_, err = client.HSet(ctx, key, newField, value).Result() _, err = client.HSet(ctx, key, newField, value).Result()
removedField = append(removedField, field) removedField = append(removedField, field)
updatedField[newField] = value updatedField[newField] = value
replacedField[field] = newField
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -805,6 +1159,7 @@ func (b *browserService) SetHashValue(connName string, db int, k any, field, new
resp.Data = map[string]any{ resp.Data = map[string]any{
"removed": removedField, "removed": removedField,
"updated": updatedField, "updated": updatedField,
"replaced": replacedField,
} }
return return
} }
@ -944,10 +1299,11 @@ func (b *browserService) SetSetItem(connName string, db int, k any, remove bool,
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var affected int64
if remove { if remove {
_, err = client.SRem(ctx, key, members...).Result() affected, err = client.SRem(ctx, key, members...).Result()
} else { } else {
_, err = client.SAdd(ctx, key, members...).Result() affected, err = client.SAdd(ctx, key, members...).Result()
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -955,6 +1311,9 @@ func (b *browserService) SetSetItem(connName string, db int, k any, remove bool,
} }
resp.Success = true resp.Success = true
resp.Data = map[string]any{
"affected": affected,
}
return return
} }
@ -1003,6 +1362,9 @@ func (b *browserService) UpdateZSetValue(connName string, db int, k any, value,
Score: score, Score: score,
Member: value, Member: value,
}).Result() }).Result()
if err == nil {
updated[value] = score
}
} else { } else {
// remove old value and add new one // remove old value and add new one
_, err = client.ZRem(ctx, key, value).Result() _, err = client.ZRem(ctx, key, value).Result()
@ -1075,7 +1437,8 @@ func (b *browserService) AddStreamValue(connName string, db int, k any, ID strin
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, err = client.XAdd(ctx, &redis.XAddArgs{ var updateID string
updateID, err = client.XAdd(ctx, &redis.XAddArgs{
Stream: key, Stream: key,
ID: ID, ID: ID,
Values: fieldItems, Values: fieldItems,
@ -1086,6 +1449,9 @@ func (b *browserService) AddStreamValue(connName string, db int, k any, ID strin
} }
resp.Success = true resp.Success = true
resp.Data = map[string]any{
"updateID": updateID,
}
return return
} }
@ -1099,8 +1465,18 @@ func (b *browserService) RemoveStreamValues(connName string, db int, k any, IDs
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, err = client.XDel(ctx, key, IDs...).Result()
var affected int64
affected, err = client.XDel(ctx, key, IDs...).Result()
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true resp.Success = true
resp.Data = map[string]any{
"affected": affected,
}
return return
} }
@ -1146,23 +1522,25 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
if strings.HasSuffix(key, "*") { if strings.HasSuffix(key, "*") {
// delete by prefix // delete by prefix
var mutex sync.Mutex var mutex sync.Mutex
supportUnlink := true
del := func(ctx context.Context, cli redis.UniversalClient) error { del := func(ctx context.Context, cli redis.UniversalClient) error {
handleDel := func(ks []string) error { handleDel := func(ks []string) error {
pipe := cli.Pipeline() var delErr error
for _, k2 := range ks { if async && supportUnlink {
if async { supportUnlink = false
cli.Unlink(ctx, k2) if delErr = cli.Unlink(ctx, ks...).Err(); delErr != nil {
// not support unlink? try del command
delErr = cli.Del(ctx, ks...).Err()
}
} else { } else {
cli.Del(ctx, k2) delErr = cli.Del(ctx, ks...).Err()
} }
}
pipe.Exec(ctx)
mutex.Lock() mutex.Lock()
deletedKeys = append(deletedKeys, ks...) deletedKeys = append(deletedKeys, ks...)
mutex.Unlock() mutex.Unlock()
return nil return delErr
} }
scanSize := int64(Preferences().GetScanSize()) scanSize := int64(Preferences().GetScanSize())
@ -1170,7 +1548,7 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
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())
if len(resultKeys) >= 3 { if len(resultKeys) >= 20 {
handleDel(resultKeys) handleDel(resultKeys)
resultKeys = resultKeys[:0:cap(resultKeys)] resultKeys = resultKeys[:0:cap(resultKeys)]
} }
@ -1198,12 +1576,14 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
} else { } else {
// delete key only // delete key only
if async { if async {
if _, err = client.Unlink(ctx, key).Result(); err != nil { if err = client.Unlink(ctx, key).Err(); err != nil {
if err = client.Del(ctx, key).Err(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
}
} else { } else {
if _, err = client.Del(ctx, key).Result(); err != nil { if err = client.Del(ctx, key).Err(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
@ -1227,8 +1607,8 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
return return
} }
flush := func(ctx context.Context, cli redis.UniversalClient) { flush := func(ctx context.Context, cli redis.UniversalClient, async bool) error {
cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error { _, e := cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Select(ctx, db) pipe.Select(ctx, db)
if async { if async {
pipe.FlushDBAsync(ctx) pipe.FlushDBAsync(ctx)
@ -1237,17 +1617,26 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
} }
return nil return nil
}) })
return e
} }
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
if cluster, ok := client.(*redis.ClusterClient); ok { if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode // cluster mode
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error { err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
flush(ctx, cli) return flush(ctx, cli, async)
return nil
}) })
// try sync mode if error cause
if err != nil && async {
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
return flush(ctx, cli, false)
})
}
} else { } else {
flush(ctx, client) if err = flush(ctx, client, async); err != nil && async {
// try sync mode if error cause
err = flush(ctx, client, false)
}
} }
if err != nil { if err != nil {

View File

@ -5,3 +5,35 @@ type JSResp struct {
Msg string `json:"msg"` Msg string `json:"msg"`
Data any `json:"data,omitempty"` Data any `json:"data,omitempty"`
} }
type KeySummaryParam struct {
Server string `json:"server"`
DB int `json:"db"`
Key any `json:"key"`
}
type KeySummary struct {
Type string `json:"type"`
TTL int64 `json:"ttl"`
Size int64 `json:"size"`
Length int64 `json:"length"`
}
type KeyDetailParam struct {
Server string `json:"server"`
DB int `json:"db"`
Key any `json:"key"`
ViewAs string `json:"viewAs,omitempty"`
DecodeType string `json:"decodeType,omitempty"`
MatchPattern string `json:"matchPattern,omitempty"`
Reset bool `json:"reset"`
Full bool `json:"full"`
}
type KeyDetail struct {
Value any `json:"value"`
Length int64 `json:"length,omitempty"`
ViewAs string `json:"viewAs,omitempty"`
DecodeType string `json:"decodeType,omitempty"`
End bool `json:"end"`
}

View File

@ -14,17 +14,17 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"sass": "^1.69.5", "sass": "^1.69.5",
"vue": "^3.3.7", "vue": "^3.3.8",
"vue-i18n": "^9.6.2", "vue-i18n": "^9.6.5",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0" "xterm-addon-fit": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.1",
"naive-ui": "^2.35.0", "naive-ui": "^2.35.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"unplugin-auto-import": "^0.16.7", "unplugin-auto-import": "^0.16.7",
"unplugin-icons": "^0.17.1", "unplugin-icons": "^0.17.3",
"unplugin-vue-components": "^0.25.2", "unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0" "vite": "^4.5.0"
} }
@ -465,23 +465,23 @@
} }
}, },
"node_modules/@intlify/core-base": { "node_modules/@intlify/core-base": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.6.5.tgz",
"integrity": "sha512-ci0j2nbEL/pamvqgcCqyIVeQ3LS41F1IRqI5rCBNnpSp0FjNnH8bpha8R3OifkhqatzlP4wGOuN/UqfLYVDv7g==", "integrity": "sha512-LzbGXiZkMWPIHnHI0g6q554S87Cmh2mmCmjytK/3pDQfjI84l+dgGoeQuKj02q7EbULRuUUgYVZVqAwEUawXGg==",
"dependencies": { "dependencies": {
"@intlify/message-compiler": "9.6.2", "@intlify/message-compiler": "9.6.5",
"@intlify/shared": "9.6.2" "@intlify/shared": "9.6.5"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
} }
}, },
"node_modules/@intlify/message-compiler": { "node_modules/@intlify/message-compiler": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.6.5.tgz",
"integrity": "sha512-kgZQL9zeJDeEB5vvD93Y++HvFUELnT48PjnpfCcF3EJaLLVs9he8IzODiNK42Z40lWbFyja0SXJZjsalybQygA==", "integrity": "sha512-WeJ499thIj0p7JaIO1V3JaJbqdqfBykS5R8fElFs5hNeotHtPAMBs4IiA+8/KGFkAbjJusgFefCq6ajP7F7+4Q==",
"dependencies": { "dependencies": {
"@intlify/shared": "9.6.2", "@intlify/shared": "9.6.5",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"engines": { "engines": {
@ -489,9 +489,9 @@
} }
}, },
"node_modules/@intlify/shared": { "node_modules/@intlify/shared": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.6.5.tgz",
"integrity": "sha512-9KBcXmJNxElp7QMnU8V0/tScTOitDqyFi4HceEZqJyyDkMi8K5DBPMTIuXIAMmtMlXpe/nj5pke7tRw97VeQRA==", "integrity": "sha512-gD7Ey47Xi4h/t6P+S04ymMSoA3wVRxGqjxuIMglwRO8POki9h164Epu2N8wk/GHXM/hR6ZGcsx2HArCCENjqSQ==",
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
} }
@ -592,9 +592,9 @@
} }
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.1.tgz",
"integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==", "integrity": "sha512-HCQG8VDFDM7YDAdcj5QI5DvUi+r6xvo9LgvYdk7LSkUNwdpempdB5horkMSZsbdey9Ywsf5aaU8kEPw9M5kREA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
@ -605,36 +605,36 @@
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.8.tgz",
"integrity": "sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==", "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.0",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz",
"integrity": "sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==", "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.3.7", "@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz",
"integrity": "sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==", "integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.7", "@vue/compiler-core": "3.3.8",
"@vue/compiler-dom": "3.3.7", "@vue/compiler-dom": "3.3.8",
"@vue/compiler-ssr": "3.3.7", "@vue/compiler-ssr": "3.3.8",
"@vue/reactivity-transform": "3.3.7", "@vue/reactivity-transform": "3.3.8",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
"postcss": "^8.4.31", "postcss": "^8.4.31",
@ -642,12 +642,12 @@
} }
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz",
"integrity": "sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==", "integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.3.7", "@vue/compiler-dom": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/devtools-api": { "node_modules/@vue/devtools-api": {
@ -656,41 +656,41 @@
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.8.tgz",
"integrity": "sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==", "integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==",
"dependencies": { "dependencies": {
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/reactivity-transform": { "node_modules/@vue/reactivity-transform": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz",
"integrity": "sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==", "integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.7", "@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5" "magic-string": "^0.30.5"
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.8.tgz",
"integrity": "sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==", "integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.3.7", "@vue/reactivity": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz",
"integrity": "sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==", "integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==",
"dependencies": { "dependencies": {
"@vue/runtime-core": "3.3.7", "@vue/runtime-core": "3.3.8",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"csstype": "^3.1.2" "csstype": "^3.1.2"
} }
}, },
@ -700,21 +700,21 @@
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.8.tgz",
"integrity": "sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==", "integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.3.7", "@vue/compiler-ssr": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.3.7" "vue": "3.3.8"
} }
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==" "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw=="
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.10.0", "version": "8.10.0",
@ -1744,9 +1744,9 @@
} }
}, },
"node_modules/unplugin-icons": { "node_modules/unplugin-icons": {
"version": "0.17.1", "version": "0.17.3",
"resolved": "https://registry.npmmirror.com/unplugin-icons/-/unplugin-icons-0.17.1.tgz", "resolved": "https://registry.npmmirror.com/unplugin-icons/-/unplugin-icons-0.17.3.tgz",
"integrity": "sha512-KsWejBPCHokYCNDQUzGu6R3E3XDYH/YpewgQwrVBXgpl1iR0RdW1NEGNdjlbuapwVnZXVgA5eiDTfNaQCawSdg==", "integrity": "sha512-uvopA4I6QYWYpwAyRvG/4NNvR1Cd4GnF/mtOkybgP2YPkHjRX02l/ALm20VUla31Jt9eZHxHyODtNf/Upx5uaw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@antfu/install-pkg": "^0.1.1", "@antfu/install-pkg": "^0.1.1",
@ -1906,15 +1906,15 @@
} }
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.8.tgz",
"integrity": "sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==", "integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.3.7", "@vue/compiler-dom": "3.3.8",
"@vue/compiler-sfc": "3.3.7", "@vue/compiler-sfc": "3.3.8",
"@vue/runtime-dom": "3.3.7", "@vue/runtime-dom": "3.3.8",
"@vue/server-renderer": "3.3.7", "@vue/server-renderer": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@ -1926,12 +1926,12 @@
} }
}, },
"node_modules/vue-i18n": { "node_modules/vue-i18n": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.6.5.tgz",
"integrity": "sha512-J43grTQjPR8LCUxvx3mkoM+11xhTnej1Al4lvJCEeKmQqf8eqbuYPQb54HXnEg/UzZyaxLBAwPAUTbrZ8V7hcg==", "integrity": "sha512-dpUEjKHg7pEsaS7ZPPxp1CflaR7bGmsvZJEhnszHPKl9OTNyno5j/DvMtMSo41kpddq4felLA7GK2prjpnXVlw==",
"dependencies": { "dependencies": {
"@intlify/core-base": "9.6.2", "@intlify/core-base": "9.6.5",
"@intlify/shared": "9.6.2", "@intlify/shared": "9.6.5",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
}, },
"engines": { "engines": {
@ -2238,27 +2238,27 @@
} }
}, },
"@intlify/core-base": { "@intlify/core-base": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.6.5.tgz",
"integrity": "sha512-ci0j2nbEL/pamvqgcCqyIVeQ3LS41F1IRqI5rCBNnpSp0FjNnH8bpha8R3OifkhqatzlP4wGOuN/UqfLYVDv7g==", "integrity": "sha512-LzbGXiZkMWPIHnHI0g6q554S87Cmh2mmCmjytK/3pDQfjI84l+dgGoeQuKj02q7EbULRuUUgYVZVqAwEUawXGg==",
"requires": { "requires": {
"@intlify/message-compiler": "9.6.2", "@intlify/message-compiler": "9.6.5",
"@intlify/shared": "9.6.2" "@intlify/shared": "9.6.5"
} }
}, },
"@intlify/message-compiler": { "@intlify/message-compiler": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.6.5.tgz",
"integrity": "sha512-kgZQL9zeJDeEB5vvD93Y++HvFUELnT48PjnpfCcF3EJaLLVs9he8IzODiNK42Z40lWbFyja0SXJZjsalybQygA==", "integrity": "sha512-WeJ499thIj0p7JaIO1V3JaJbqdqfBykS5R8fElFs5hNeotHtPAMBs4IiA+8/KGFkAbjJusgFefCq6ajP7F7+4Q==",
"requires": { "requires": {
"@intlify/shared": "9.6.2", "@intlify/shared": "9.6.5",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"@intlify/shared": { "@intlify/shared": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.6.5.tgz",
"integrity": "sha512-9KBcXmJNxElp7QMnU8V0/tScTOitDqyFi4HceEZqJyyDkMi8K5DBPMTIuXIAMmtMlXpe/nj5pke7tRw97VeQRA==" "integrity": "sha512-gD7Ey47Xi4h/t6P+S04ymMSoA3wVRxGqjxuIMglwRO8POki9h164Epu2N8wk/GHXM/hR6ZGcsx2HArCCENjqSQ=="
}, },
"@jridgewell/sourcemap-codec": { "@jridgewell/sourcemap-codec": {
"version": "1.4.15", "version": "1.4.15",
@ -2336,43 +2336,43 @@
} }
}, },
"@vitejs/plugin-vue": { "@vitejs/plugin-vue": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.1.tgz",
"integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==", "integrity": "sha512-HCQG8VDFDM7YDAdcj5QI5DvUi+r6xvo9LgvYdk7LSkUNwdpempdB5horkMSZsbdey9Ywsf5aaU8kEPw9M5kREA==",
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"@vue/compiler-core": { "@vue/compiler-core": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.8.tgz",
"integrity": "sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==", "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==",
"requires": { "requires": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.0",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"@vue/compiler-dom": { "@vue/compiler-dom": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz",
"integrity": "sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==", "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==",
"requires": { "requires": {
"@vue/compiler-core": "3.3.7", "@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"@vue/compiler-sfc": { "@vue/compiler-sfc": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz",
"integrity": "sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==", "integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==",
"requires": { "requires": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.7", "@vue/compiler-core": "3.3.8",
"@vue/compiler-dom": "3.3.7", "@vue/compiler-dom": "3.3.8",
"@vue/compiler-ssr": "3.3.7", "@vue/compiler-ssr": "3.3.8",
"@vue/reactivity-transform": "3.3.7", "@vue/reactivity-transform": "3.3.8",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
"postcss": "^8.4.31", "postcss": "^8.4.31",
@ -2380,12 +2380,12 @@
} }
}, },
"@vue/compiler-ssr": { "@vue/compiler-ssr": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz",
"integrity": "sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==", "integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==",
"requires": { "requires": {
"@vue/compiler-dom": "3.3.7", "@vue/compiler-dom": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"@vue/devtools-api": { "@vue/devtools-api": {
@ -2394,41 +2394,41 @@
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
}, },
"@vue/reactivity": { "@vue/reactivity": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.8.tgz",
"integrity": "sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==", "integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==",
"requires": { "requires": {
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"@vue/reactivity-transform": { "@vue/reactivity-transform": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz",
"integrity": "sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==", "integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==",
"requires": { "requires": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.7", "@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5" "magic-string": "^0.30.5"
} }
}, },
"@vue/runtime-core": { "@vue/runtime-core": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.8.tgz",
"integrity": "sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==", "integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==",
"requires": { "requires": {
"@vue/reactivity": "3.3.7", "@vue/reactivity": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"@vue/runtime-dom": { "@vue/runtime-dom": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz",
"integrity": "sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==", "integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==",
"requires": { "requires": {
"@vue/runtime-core": "3.3.7", "@vue/runtime-core": "3.3.8",
"@vue/shared": "3.3.7", "@vue/shared": "3.3.8",
"csstype": "^3.1.2" "csstype": "^3.1.2"
}, },
"dependencies": { "dependencies": {
@ -2440,18 +2440,18 @@
} }
}, },
"@vue/server-renderer": { "@vue/server-renderer": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.8.tgz",
"integrity": "sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==", "integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==",
"requires": { "requires": {
"@vue/compiler-ssr": "3.3.7", "@vue/compiler-ssr": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"@vue/shared": { "@vue/shared": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==" "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw=="
}, },
"acorn": { "acorn": {
"version": "8.10.0", "version": "8.10.0",
@ -3236,9 +3236,9 @@
} }
}, },
"unplugin-icons": { "unplugin-icons": {
"version": "0.17.1", "version": "0.17.3",
"resolved": "https://registry.npmmirror.com/unplugin-icons/-/unplugin-icons-0.17.1.tgz", "resolved": "https://registry.npmmirror.com/unplugin-icons/-/unplugin-icons-0.17.3.tgz",
"integrity": "sha512-KsWejBPCHokYCNDQUzGu6R3E3XDYH/YpewgQwrVBXgpl1iR0RdW1NEGNdjlbuapwVnZXVgA5eiDTfNaQCawSdg==", "integrity": "sha512-uvopA4I6QYWYpwAyRvG/4NNvR1Cd4GnF/mtOkybgP2YPkHjRX02l/ALm20VUla31Jt9eZHxHyODtNf/Upx5uaw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@antfu/install-pkg": "^0.1.1", "@antfu/install-pkg": "^0.1.1",
@ -3311,24 +3311,24 @@
} }
}, },
"vue": { "vue": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.7.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.8.tgz",
"integrity": "sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==", "integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==",
"requires": { "requires": {
"@vue/compiler-dom": "3.3.7", "@vue/compiler-dom": "3.3.8",
"@vue/compiler-sfc": "3.3.7", "@vue/compiler-sfc": "3.3.8",
"@vue/runtime-dom": "3.3.7", "@vue/runtime-dom": "3.3.8",
"@vue/server-renderer": "3.3.7", "@vue/server-renderer": "3.3.8",
"@vue/shared": "3.3.7" "@vue/shared": "3.3.8"
} }
}, },
"vue-i18n": { "vue-i18n": {
"version": "9.6.2", "version": "9.6.5",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.6.2.tgz", "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.6.5.tgz",
"integrity": "sha512-J43grTQjPR8LCUxvx3mkoM+11xhTnej1Al4lvJCEeKmQqf8eqbuYPQb54HXnEg/UzZyaxLBAwPAUTbrZ8V7hcg==", "integrity": "sha512-dpUEjKHg7pEsaS7ZPPxp1CflaR7bGmsvZJEhnszHPKl9OTNyno5j/DvMtMSo41kpddq4felLA7GK2prjpnXVlw==",
"requires": { "requires": {
"@intlify/core-base": "9.6.2", "@intlify/core-base": "9.6.5",
"@intlify/shared": "9.6.2", "@intlify/shared": "9.6.5",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
} }
}, },

View File

@ -15,17 +15,17 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"sass": "^1.69.5", "sass": "^1.69.5",
"vue": "^3.3.7", "vue": "^3.3.8",
"vue-i18n": "^9.6.2", "vue-i18n": "^9.6.5",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0" "xterm-addon-fit": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.1",
"naive-ui": "^2.35.0", "naive-ui": "^2.35.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"unplugin-auto-import": "^0.16.7", "unplugin-auto-import": "^0.16.7",
"unplugin-icons": "^0.17.1", "unplugin-icons": "^0.17.3",
"unplugin-vue-components": "^0.25.2", "unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0" "vite": "^4.5.0"
} }

View File

@ -1 +1 @@
24a0be173ef3e3939a15eb9c7fff5e1a 3a2923aff6e49c16c4ca2e7fc8811eeb

View File

@ -26,7 +26,7 @@ const props = defineProps({
const data = reactive({ const data = reactive({
navMenuWidth: 60, navMenuWidth: 60,
toolbarHeight: 45, toolbarHeight: 38,
}) })
const tabStore = useTabStore() const tabStore = useTabStore()
@ -135,7 +135,7 @@ onMounted(async () => {
paddingLeft: `${logoPaddingLeft}px`, paddingLeft: `${logoPaddingLeft}px`,
}"> }">
<n-space :size="3" :wrap="false" :wrap-item="false" align="center"> <n-space :size="3" :wrap="false" :wrap-item="false" align="center">
<n-avatar :size="35" :src="iconUrl" color="#0000" style="min-width: 35px" /> <n-avatar :size="32" :src="iconUrl" color="#0000" style="min-width: 32px" />
<div style="min-width: 68px; font-weight: 800">Tiny RDM</div> <div style="min-width: 68px; font-weight: 800">Tiny RDM</div>
<transition name="fade"> <transition name="fade">
<n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px"> <n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px">

View File

@ -56,11 +56,13 @@ const tabContent = computed(() => {
length: tab.length || 0, length: tab.length || 0,
viewAs: tab.viewAs, viewAs: tab.viewAs,
decode: tab.decode, decode: tab.decode,
end: tab.end,
loading: tab.loading === true,
} }
}) })
const isBlankValue = computed(() => { const isBlankValue = computed(() => {
return tabContent.value.value == null return tabContent.value?.keyPath == null
}) })
const selectedSubTab = computed(() => { const selectedSubTab = computed(() => {
@ -133,19 +135,7 @@ watch(
<span>{{ $t('interface.sub_tab.key_detail') }}</span> <span>{{ $t('interface.sub_tab.key_detail') }}</span>
</n-space> </n-space>
</template> </template>
<content-value-wrapper <content-value-wrapper :blank="isBlankValue" :content="tabContent" />
:blank="isBlankValue"
:db="tabContent.db"
:decode="tabContent.decode"
:key-code="tabContent.keyCode"
:key-path="tabContent.keyPath"
:length="tabContent.length"
:name="tabContent.name"
:size="tabContent.size"
:ttl="tabContent.ttl"
:type="tabContent.type"
:value="tabContent.value"
:view-as="tabContent.viewAs" />
</n-tab-pane> </n-tab-pane>
<!-- cli pane --> <!-- cli pane -->

View File

@ -1,9 +1,8 @@
<script setup> <script setup>
import { h, onMounted, onUnmounted, reactive, ref } from 'vue' import { h, onMounted, onUnmounted, reactive, ref } from 'vue'
import Refresh from '@/components/icons/Refresh.vue' import Refresh from '@/components/icons/Refresh.vue'
import { debounce, isEmpty, map, size, split } from 'lodash' import { debounce, isEmpty, map, size } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import dayjs from 'dayjs'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'

View File

@ -11,9 +11,7 @@ import IconButton from '@/components/common/IconButton.vue'
import Copy from '@/components/icons/Copy.vue' import Copy from '@/components/icons/Copy.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js' import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { computed } from 'vue' import { computed } from 'vue'
import { isEmpty, padStart } from 'lodash' import { padStart } from 'lodash'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -34,32 +32,18 @@ const props = defineProps({
type: Number, type: Number,
default: -1, default: -1,
}, },
viewAs: { loading: Boolean,
type: String,
default: formatTypes.PLAIN_TEXT,
},
decode: {
type: String,
default: decodeTypes.NONE,
},
}) })
const emit = defineEmits(['reload', 'rename', 'delete'])
const dialogStore = useDialog() const dialogStore = useDialog()
const browserStore = useBrowserStore()
const i18n = useI18n() const i18n = useI18n()
const binaryKey = computed(() => { const binaryKey = computed(() => {
return !!props.keyCode return !!props.keyCode
}) })
/**
*
* @type {ComputedRef<string|number[]>}
*/
const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
})
const ttlString = computed(() => { const ttlString = computed(() => {
let s = '' let s = ''
if (props.ttl > 0) { if (props.ttl > 0) {
@ -77,10 +61,6 @@ const ttlString = computed(() => {
return s return s
}) })
const onReloadKey = () => {
browserStore.loadKeyValue(props.server, props.db, keyName.value, props.viewAs, props.decode)
}
const onCopyKey = () => { const onCopyKey = () => {
ClipboardSetText(props.keyPath) ClipboardSetText(props.keyPath)
.then((succ) => { .then((succ) => {
@ -92,33 +72,20 @@ const onCopyKey = () => {
$message.error(e.message) $message.error(e.message)
}) })
} }
const onRenameKey = () => {
if (binaryKey.value) {
$message.error(i18n.t('dialogue.rename_binary_key_fail'))
} else {
dialogStore.openRenameKeyDialog(props.server, props.db, props.keyPath)
}
}
const onDeleteKey = () => {
$dialog.warning(i18n.t('dialogue.remove_tip', { name: props.keyPath }), () => {
browserStore.deleteKey(props.server, props.db, keyName.value).then((success) => {
if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: props.keyPath }))
}
})
})
}
</script> </script>
<template> <template>
<div class="content-toolbar flex-box-h"> <div class="content-toolbar flex-box-h">
<n-input-group> <n-input-group>
<redis-type-tag :binary-key="binaryKey" :type="props.keyType" size="large" /> <redis-type-tag :binary-key="binaryKey" :type="props.keyType" size="large" />
<n-input v-model:value="props.keyPath"> <n-input v-model:value="props.keyPath" readonly>
<template #suffix> <template #suffix>
<icon-button :icon="Refresh" size="18" t-tooltip="interface.reload" @click="onReloadKey" /> <icon-button
:icon="Refresh"
:loading="props.loading"
size="18"
t-tooltip="interface.reload"
@click="emit('reload')" />
</template> </template>
</n-input> </n-input>
<icon-button :icon="Copy" border size="18" t-tooltip="interface.copy_key" @click="onCopyKey" /> <icon-button :icon="Copy" border size="18" t-tooltip="interface.copy_key" @click="onCopyKey" />
@ -135,11 +102,11 @@ const onDeleteKey = () => {
</template> </template>
TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }} TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }}
</n-tooltip> </n-tooltip>
<icon-button :icon="Edit" border size="18" t-tooltip="interface.rename_key" @click="onRenameKey" /> <icon-button :icon="Edit" border size="18" t-tooltip="interface.rename_key" @click="emit('rename')" />
</n-button-group> </n-button-group>
<n-tooltip :show-arrow="false"> <n-tooltip :show-arrow="false">
<template #trigger> <template #trigger>
<n-button :focusable="false" @click="onDeleteKey"> <n-button :focusable="false" @click="emit('delete')">
<template #icon> <template #icon>
<n-icon :component="Delete" size="18" /> <n-icon :component="Delete" size="18" />
</template> </template>

View File

@ -7,10 +7,13 @@ import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js' import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue' import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import useDialogStore from 'stores/dialog.js' import useDialogStore from 'stores/dialog.js'
import { isEmpty } from 'lodash' import { isEmpty, size } from 'lodash'
import bytes from 'bytes' import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String, type: String,
default: decodeTypes.NONE, default: decodeTypes.NONE,
}, },
end: Boolean,
loading: Boolean,
}) })
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/** /**
* *
* @type {ComputedRef<string|number[]>} * @type {ComputedRef<string|number[]>}
@ -149,14 +156,7 @@ const actionColumn = {
row.key, row.key,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.key })) $message.success(i18n.t('dialogue.delete_key_succ', { key: row.key }))
// update display value
// if (!isEmpty(removed)) {
// for (const elem of removed) {
// delete props.value[elem]
// }
// }
} else { } else {
$message.error(msg) $message.error(msg)
} }
@ -175,14 +175,7 @@ const actionColumn = {
currentEditRow.value.value, currentEditRow.value.value,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
// update display value
// if (!isEmpty(updated)) {
// for (const key in updated) {
// props.value[key] = updated[key]
// }
// }
} else { } else {
$message.error(msg) $message.error(msg)
} }
@ -223,6 +216,12 @@ const tableData = computed(() => {
} }
return data return data
}) })
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => { const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.HASH) dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.HASH)
} }
@ -262,20 +261,28 @@ const onUpdateFilter = (filters, sourceColumn) => {
break break
} }
} }
defineExpose({
reset: () => {
clearFilter()
},
})
</script> </script>
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
:db="props.db" :db="props.db"
:decode="props.decode"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
:key-type="keyType" :key-type="keyType"
:loading="props.loading"
:server="props.name" :server="props.name"
:ttl="ttl" :ttl="ttl"
:view-as="props.viewAs" class="value-item-part"
class="value-item-part" /> @delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input-group> <n-input-group>
@ -294,6 +301,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group> </n-input-group>
</div> </div>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddRow"> <n-button :focusable="false" plain @click="onAddRow">
<template #icon> <template #icon>
<n-icon :component="AddLink" size="18" /> <n-icon :component="AddLink" size="18" />
@ -301,24 +324,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }} {{ $t('interface.add_row') }}
</n-button> </n-button>
</div> </div>
<div class="value-wrapper value-item-part fill-height flex-box-h"> <div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table <n-data-table
:key="(row) => row.no" :key="(row) => row.no"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:loading="props.loading"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
class="flex-item-expand"
flex-height flex-height
max-height="100%"
size="small" size="small"
striped striped
virtual-scroll virtual-scroll
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
</div> </div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text> <n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>

View File

@ -11,6 +11,9 @@ import useDialogStore from 'stores/dialog.js'
import bytes from 'bytes' import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String, type: String,
default: decodeTypes.NONE, default: decodeTypes.NONE,
}, },
end: Boolean,
loading: Boolean,
}) })
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/** /**
* *
* @type {ComputedRef<string|number[]>} * @type {ComputedRef<string|number[]>}
@ -106,12 +113,7 @@ const actionColumn = {
row.no - 1, row.no - 1,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: '#' + row.no })) $message.success(i18n.t('dialogue.delete_key_succ', { key: '#' + row.no }))
// update display value
// if (!isEmpty(removed)) {
// props.value.splice(removed[0], 1)
// }
} else { } else {
$message.error(msg) $message.error(msg)
} }
@ -129,14 +131,7 @@ const actionColumn = {
currentEditRow.value.value, currentEditRow.value.value,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
// update display value
// if (!isEmpty(updated)) {
// for (const key in updated) {
// props.value[key] = updated[key]
// }
// }
} else { } else {
$message.error(msg) $message.error(msg)
} }
@ -178,6 +173,11 @@ const tableData = computed(() => {
return data return data
}) })
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddValue = (value) => { const onAddValue = (value) => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.LIST) dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.LIST)
} }
@ -194,20 +194,28 @@ const clearFilter = () => {
const onUpdateFilter = (filters, sourceColumn) => { const onUpdateFilter = (filters, sourceColumn) => {
valueColumn.filterOptionValue = filters[sourceColumn.key] valueColumn.filterOptionValue = filters[sourceColumn.key]
} }
defineExpose({
reset: () => {
clearFilter()
},
})
</script> </script>
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
:db="props.db" :db="props.db"
:decode="props.decode"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
:key-type="keyType" :key-type="keyType"
:loading="props.loading"
:server="props.name" :server="props.name"
:ttl="ttl" :ttl="ttl"
:view-as="props.viewAs" class="value-item-part"
class="value-item-part" /> @delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input <n-input
@ -218,6 +226,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
@update:value="onFilterInput" /> @update:value="onFilterInput" />
</div> </div>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddValue"> <n-button :focusable="false" plain @click="onAddValue">
<template #icon> <template #icon>
<n-icon :component="AddLink" size="18" /> <n-icon :component="AddLink" size="18" />
@ -225,24 +249,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }} {{ $t('interface.add_row') }}
</n-button> </n-button>
</div> </div>
<div class="value-wrapper value-item-part fill-height flex-box-h"> <div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table <n-data-table
:key="(row) => row.no" :key="(row) => row.no"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:loading="props.loading"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
class="flex-item-expand"
flex-height flex-height
max-height="100%"
size="small" size="small"
striped striped
virtual-scroll virtual-scroll
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
</div> </div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text> <n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>

View File

@ -11,6 +11,9 @@ import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import bytes from 'bytes' import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String, type: String,
default: decodeTypes.NONE, default: decodeTypes.NONE,
}, },
end: Boolean,
loading: Boolean,
}) })
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/** /**
* *
* @type {ComputedRef<string|number[]>} * @type {ComputedRef<string|number[]>}
@ -107,10 +114,7 @@ const actionColumn = {
row.value, row.value,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.value })) $message.success(i18n.t('dialogue.delete_key_succ', { key: row.value }))
// update display value
// props.value.splice(row.no - 1, 1)
} else { } else {
$message.error(msg) $message.error(msg)
} }
@ -128,10 +132,7 @@ const actionColumn = {
currentEditRow.value.value, currentEditRow.value.value,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
// update display value
// props.value[row.no - 1] = currentEditRow.value.value
} else { } else {
$message.error(msg) $message.error(msg)
} }
@ -173,6 +174,11 @@ const tableData = computed(() => {
return data return data
}) })
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddValue = (value) => { const onAddValue = (value) => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET) dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET)
} }
@ -189,20 +195,28 @@ const clearFilter = () => {
const onUpdateFilter = (filters, sourceColumn) => { const onUpdateFilter = (filters, sourceColumn) => {
valueColumn.filterOptionValue = filters[sourceColumn.key] valueColumn.filterOptionValue = filters[sourceColumn.key]
} }
defineExpose({
reset: () => {
clearFilter()
},
})
</script> </script>
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
:db="props.db" :db="props.db"
:decode="props.decode"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
:key-type="keyType" :key-type="keyType"
:loading="props.loading"
:server="props.name" :server="props.name"
:ttl="ttl" :ttl="ttl"
:view-as="props.viewAs" class="value-item-part"
class="value-item-part" /> @delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input <n-input
@ -213,6 +227,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
@update:value="onFilterInput" /> @update:value="onFilterInput" />
</div> </div>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddValue"> <n-button :focusable="false" plain @click="onAddValue">
<template #icon> <template #icon>
<n-icon :component="AddLink" size="18" /> <n-icon :component="AddLink" size="18" />
@ -220,24 +250,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }} {{ $t('interface.add_row') }}
</n-button> </n-button>
</div> </div>
<div class="value-wrapper value-item-part fill-height flex-box-h"> <div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table <n-data-table
:key="(row) => row.no" :key="(row) => row.no"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:loading="props.loading"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
class="flex-item-expand"
flex-height flex-height
max-height="100%"
size="small" size="small"
striped striped
virtual-scroll virtual-scroll
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
</div> </div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text> <n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>

View File

@ -7,10 +7,13 @@ import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js' import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue' import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import useDialogStore from 'stores/dialog.js' import useDialogStore from 'stores/dialog.js'
import { includes, isEmpty, keys, some, values } from 'lodash' import { includes, isEmpty, keys, size, some, values } from 'lodash'
import bytes from 'bytes' import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -27,7 +30,10 @@ const props = defineProps({
type: Number, type: Number,
default: -1, default: -1,
}, },
value: Object, value: {
type: Array,
default: () => [],
},
size: Number, size: Number,
length: Number, length: Number,
viewAs: { viewAs: {
@ -38,8 +44,12 @@ const props = defineProps({
type: String, type: String,
default: decodeTypes.NONE, default: decodeTypes.NONE,
}, },
end: Boolean,
loading: Boolean,
}) })
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/** /**
* *
* @type {ComputedRef<string|number[]>} * @type {ComputedRef<string|number[]>}
@ -115,14 +125,7 @@ const actionColumn = {
row.id, row.id,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.id })) $message.success(i18n.t('dialogue.delete_key_succ', { key: row.id }))
// update display value
// if (!isEmpty(removed)) {
// for (const elem of removed) {
// delete props.value[elem]
// }
// }
} else { } else {
$message.error(msg) $message.error(msg)
} }
@ -137,15 +140,22 @@ const columns = reactive([idColumn, valueColumn, actionColumn])
const tableData = computed(() => { const tableData = computed(() => {
const data = [] const data = []
if (!isEmpty(props.value)) {
for (const elem of props.value) { for (const elem of props.value) {
data.push({ data.push({
id: elem.id, id: elem.id,
value: elem.value, value: elem.value,
}) })
} }
}
return data return data
}) })
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => { const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.STREAM) dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.STREAM)
} }
@ -174,20 +184,28 @@ const onUpdateFilter = (filters, sourceColumn) => {
break break
} }
} }
defineExpose({
reset: () => {
clearFilter()
},
})
</script> </script>
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
:db="props.db" :db="props.db"
:decode="props.decode"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
:key-type="keyType" :key-type="keyType"
:loading="props.loading"
:server="props.name" :server="props.name"
:ttl="ttl" :ttl="ttl"
:view-as="props.viewAs" class="value-item-part"
class="value-item-part" /> @delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input-group> <n-input-group>
@ -206,6 +224,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group> </n-input-group>
</div> </div>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddRow"> <n-button :focusable="false" plain @click="onAddRow">
<template #icon> <template #icon>
<n-icon :component="AddLink" size="18" /> <n-icon :component="AddLink" size="18" />
@ -213,17 +247,18 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }} {{ $t('interface.add_row') }}
</n-button> </n-button>
</div> </div>
<div class="value-wrapper value-item-part fill-height flex-box-h"> <div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table <n-data-table
:key="(row) => row.id" :key="(row) => row.id"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:loading="props.loading"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
class="flex-item-expand"
flex-height flex-height
max-height="100%"
size="small" size="small"
striped striped
virtual-scroll virtual-scroll
@ -231,7 +266,7 @@ const onUpdateFilter = (filters, sourceColumn) => {
</div> </div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text> <n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>

View File

@ -43,8 +43,11 @@ const props = defineProps({
type: String, type: String,
default: decodeTypes.NONE, default: decodeTypes.NONE,
}, },
loading: Boolean,
}) })
const emit = defineEmits(['reload', 'rename', 'delete'])
/** /**
* *
* @type {ComputedRef<string|number[]>} * @type {ComputedRef<string|number[]>}
@ -53,16 +56,6 @@ const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
}) })
// const viewOption = computed(() =>
// map(types, (t) => {
// return {
// value: t,
// label: t,
// key: t,
// }
// }),
// )
const keyType = redisTypes.STRING const keyType = redisTypes.STRING
const viewLanguage = computed(() => { const viewLanguage = computed(() => {
switch (props.viewAs) { switch (props.viewAs) {
@ -74,11 +67,23 @@ const viewLanguage = computed(() => {
}) })
const onViewTypeUpdate = (viewType) => { const onViewTypeUpdate = (viewType) => {
browserStore.loadKeyValue(props.name, props.db, keyName.value, viewType, props.decode) browserStore.loadKeyDetail({
server: props.name,
db: props.db,
key: keyName.value,
viewType,
decodeType: props.decode,
})
} }
const onDecodeTypeUpdate = (decodeType) => { const onDecodeTypeUpdate = (decodeType) => {
browserStore.loadKeyValue(props.name, props.db, keyName.value, props.viewAs, decodeType) browserStore.loadKeyDetail({
server: props.name,
db: props.db,
key: keyName.value,
viewType: props.viewAs,
decodeType,
})
} }
/** /**
@ -126,7 +131,7 @@ const onSaveValue = async () => {
props.decode, props.decode,
) )
if (success) { if (success) {
await browserStore.loadKeyValue(props.name, props.db, keyName.value) await browserStore.loadKeyDetail({ server: props.name, db: props.db, key: keyName.value })
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -144,14 +149,16 @@ const onSaveValue = async () => {
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
:db="props.db" :db="props.db"
:decode="props.decode"
:key-code="keyCode" :key-code="keyCode"
:key-path="keyPath" :key-path="keyPath"
:key-type="keyType" :key-type="keyType"
:loading="loading"
:server="props.name" :server="props.name"
:ttl="ttl" :ttl="ttl"
:view-as="props.viewAs" class="value-item-part"
class="value-item-part" /> @delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group v-if="!inEdit"> <n-button-group v-if="!inEdit">

View File

@ -1,5 +1,4 @@
<script setup> <script setup>
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { types as redisTypes } from '@/consts/support_redis_type.js' import { types as redisTypes } from '@/consts/support_redis_type.js'
import ContentValueString from '@/components/content_value/ContentValueString.vue' import ContentValueString from '@/components/content_value/ContentValueString.vue'
import ContentValueHash from '@/components/content_value/ContentValueHash.vue' import ContentValueHash from '@/components/content_value/ContentValueHash.vue'
@ -9,37 +8,48 @@ import ContentValueZset from '@/components/content_value/ContentValueZSet.vue'
import ContentValueStream from '@/components/content_value/ContentValueStream.vue' import ContentValueStream from '@/components/content_value/ContentValueStream.vue'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { computed, onMounted, ref, watch } from 'vue'
import { isEmpty } from 'lodash'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useDialogStore from 'stores/dialog.js'
const themeVars = useThemeVars() const themeVars = useThemeVars()
const browserStore = useBrowserStore() const browserStore = useBrowserStore()
const dialogStore = useDialogStore()
const props = defineProps({ const props = defineProps({
blank: Boolean, blank: Boolean,
type: String, content: {
name: String, type: Object,
db: Number, default: {},
keyPath: String,
keyCode: {
type: Array,
default: null,
},
ttl: {
type: Number,
default: -1,
},
value: [String, Object],
size: Number,
length: Number,
viewAs: {
type: String,
default: formatTypes.PLAIN_TEXT,
},
decode: {
type: String,
default: decodeTypes.NONE,
}, },
}) })
/**
*
* @type {ComputedRef<{
* type:
* String, name: String,
* db: Number,
* keyPath: String,
* keyCode: Array,
* ttl: Number,
* value: [String, Object],
* size: Number,
* length: Number,
* viewAs: String,
* decode: String,
* end: Boolean
* }>}
*/
const data = computed(() => {
return props.content
})
const binaryKey = computed(() => {
return !!data.value.keyCode
})
const valueComponents = { const valueComponents = {
[redisTypes.STRING]: ContentValueString, [redisTypes.STRING]: ContentValueString,
[redisTypes.HASH]: ContentValueHash, [redisTypes.HASH]: ContentValueHash,
@ -49,34 +59,108 @@ const valueComponents = {
[redisTypes.STREAM]: ContentValueStream, [redisTypes.STREAM]: ContentValueStream,
} }
/** const keyName = computed(() => {
* reload current selection key return !isEmpty(data.value.keyCode) ? data.value.keyCode : data.value.keyPath
* @returns {Promise<null>} })
*/
const onReloadKey = async () => { const loadData = async (reset, full) => {
await browserStore.loadKeyValue(props.name, props.db, props.key, props.viewAs) try {
const { name, db, view, decodeType, matchPattern } = data.value
await browserStore.loadKeyDetail({
server: name,
db: db,
key: keyName.value,
viewType: view,
decodeType: decodeType,
matchPattern: matchPattern,
reset: reset === true,
full: full === true,
})
} finally {
}
} }
const onReload = async () => {
try {
const { name, db, keyCode, keyPath } = data.value
await browserStore.reloadKey({ server: name, db, key: keyCode || keyPath })
} finally {
}
}
const onRename = () => {
const { name, db, keyPath } = data.value
if (binaryKey.value) {
$message.error(i18n.t('dialogue.rename_binary_key_fail'))
} else {
dialogStore.openRenameKeyDialog(name, db, keyPath)
}
}
const onDelete = () => {
$dialog.warning(i18n.t('dialogue.remove_tip', { name: props.keyPath }), () => {
const { name, db } = data.value
browserStore.deleteKey(name, db, keyName.value).then((success) => {
if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: props.keyPath }))
}
})
})
}
const onLoadMore = () => {
loadData(false, false)
}
const onLoadAll = () => {
loadData(false, true)
}
onMounted(() => {
// onReload()
loadData(false, false)
})
const contentRef = ref(null)
watch(
() => data.value?.keyPath,
() => {
// onReload()
if (contentRef.value?.reset != null) {
contentRef.value?.reset()
}
loadData(false, false)
},
)
</script> </script>
<template> <template>
<n-empty v-if="props.blank" :description="$t('interface.nonexist_tab_content')" class="empty-content"> <n-empty v-if="props.blank" :description="$t('interface.nonexist_tab_content')" class="empty-content">
<template #extra> <template #extra>
<n-button :focusable="false" @click="onReloadKey">{{ $t('interface.reload') }}</n-button> <n-button :focusable="false" @click="onReload">{{ $t('interface.reload') }}</n-button>
</template> </template>
</n-empty> </n-empty>
<keep-alive v-else> <keep-alive v-else>
<component <component
:is="valueComponents[props.type]" :is="valueComponents[data.type]"
:db="props.db" ref="contentRef"
:decode="props.decode" :db="data.db"
:key-code="props.keyCode" :decode="data.decode || decodeTypes.NONE"
:key-path="props.keyPath" :end="data.end"
:length="props.length" :key-code="data.keyCode"
:name="props.name" :key-path="data.keyPath"
:size="props.size" :length="data.length"
:ttl="props.ttl" :loading="data.loading === true"
:value="props.value" :name="data.name"
:view-as="props.viewAs" /> :size="data.size"
:ttl="data.ttl"
:value="data.value"
:view-as="data.viewAs || formatTypes.PLAIN_TEXT"
@delete="onDelete"
@loadall="onLoadAll"
@loadmore="onLoadMore"
@reload="onReload"
@rename="onRename" />
</keep-alive> </keep-alive>
</template> </template>

View File

@ -6,11 +6,14 @@ import AddLink from '@/components/icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, NInputNumber, useThemeVars } from 'naive-ui' import { NButton, NCode, NIcon, NInput, NInputNumber, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js' import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue' import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import { isEmpty } from 'lodash' import { isEmpty, size } from 'lodash'
import useDialogStore from 'stores/dialog.js' import useDialogStore from 'stores/dialog.js'
import bytes from 'bytes' import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String, type: String,
default: decodeTypes.NONE, default: decodeTypes.NONE,
}, },
end: Boolean,
loading: Boolean,
}) })
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/** /**
* *
* @type {ComputedRef<string|number[]>} * @type {ComputedRef<string|number[]>}
@ -178,7 +185,6 @@ const actionColumn = {
row.value, row.value,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.value })) $message.success(i18n.t('dialogue.delete_key_succ', { key: row.value }))
} else { } else {
$message.error(msg) $message.error(msg)
@ -203,7 +209,6 @@ const actionColumn = {
currentEditRow.value.score, currentEditRow.value.score,
) )
if (success) { if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -235,6 +240,7 @@ const columns = reactive([
const tableData = computed(() => { const tableData = computed(() => {
const data = [] const data = []
if (!isEmpty(props.value)) {
let index = 0 let index = 0
for (const elem of props.value) { for (const elem of props.value) {
data.push({ data.push({
@ -243,9 +249,15 @@ const tableData = computed(() => {
score: elem.score, score: elem.score,
}) })
} }
}
return data return data
}) })
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => { const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET) dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET)
} }
@ -287,20 +299,28 @@ const onUpdateFilter = (filters, sourceColumn) => {
break break
} }
} }
defineExpose({
reset: () => {
clearFilter()
},
})
</script> </script>
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
:db="props.db" :db="props.db"
:decode="props.decode"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
:key-type="keyType" :key-type="keyType"
:loading="props.loading"
:server="props.name" :server="props.name"
:ttl="ttl" :ttl="ttl"
:view-as="props.viewAs" class="value-item-part"
class="value-item-part" /> @delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input-group> <n-input-group>
@ -324,6 +344,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group> </n-input-group>
</div> </div>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddRow"> <n-button :focusable="false" plain @click="onAddRow">
<template #icon> <template #icon>
<n-icon :component="AddLink" size="18" /> <n-icon :component="AddLink" size="18" />
@ -331,24 +367,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }} {{ $t('interface.add_row') }}
</n-button> </n-button>
</div> </div>
<div class="value-wrapper value-item-part fill-height flex-box-h"> <div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table <n-data-table
:key="(row) => row.no" :key="(row) => row.no"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:loading="props.loading"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
class="flex-item-expand"
flex-height flex-height
max-height="100%"
size="small" size="small"
striped striped
virtual-scroll virtual-scroll
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
</div> </div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text> <n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>

View File

@ -11,6 +11,7 @@ import AddZSetValue from '@/components/new_value/AddZSetValue.vue'
import NewStreamValue from '@/components/new_value/NewStreamValue.vue' import NewStreamValue from '@/components/new_value/NewStreamValue.vue'
import { isEmpty, size, slice } from 'lodash' import { isEmpty, size, slice } from 'lodash'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import useTabStore from 'stores/tab.js'
const i18n = useI18n() const i18n = useI18n()
const newForm = reactive({ const newForm = reactive({
@ -79,6 +80,7 @@ watch(
) )
const browserStore = useBrowserStore() const browserStore = useBrowserStore()
const tab = useTabStore()
const onAdd = async () => { const onAdd = async () => {
try { try {
const { server, db, key, keyCode, type } = newForm const { server, db, key, keyCode, type } = newForm
@ -87,6 +89,7 @@ const onAdd = async () => {
value = defaultValue[type] value = defaultValue[type]
} }
const keyName = isEmpty(keyCode) ? key : keyCode const keyName = isEmpty(keyCode) ? key : keyCode
let updated = false
switch (type) { switch (type) {
case types.LIST: case types.LIST:
{ {
@ -98,9 +101,7 @@ const onAdd = async () => {
} }
const { success, msg } = data const { success, msg } = data
if (success) { if (success) {
if (newForm.reload) { updated = true
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ')) $message.success(i18n.t('dialogue.handle_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -112,9 +113,7 @@ const onAdd = async () => {
{ {
const { success, msg } = await browserStore.addHashField(server, db, keyName, newForm.opType, value) const { success, msg } = await browserStore.addHashField(server, db, keyName, newForm.opType, value)
if (success) { if (success) {
if (newForm.reload) { updated = true
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ')) $message.success(i18n.t('dialogue.handle_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -126,9 +125,7 @@ const onAdd = async () => {
{ {
const { success, msg } = await browserStore.addSetItem(server, db, keyName, value) const { success, msg } = await browserStore.addSetItem(server, db, keyName, value)
if (success) { if (success) {
if (newForm.reload) { updated = true
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ')) $message.success(i18n.t('dialogue.handle_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -140,9 +137,7 @@ const onAdd = async () => {
{ {
const { success, msg } = await browserStore.addZSetItem(server, db, keyName, newForm.opType, value) const { success, msg } = await browserStore.addZSetItem(server, db, keyName, newForm.opType, value)
if (success) { if (success) {
if (newForm.reload) { updated = true
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ')) $message.success(i18n.t('dialogue.handle_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -161,9 +156,7 @@ const onAdd = async () => {
slice(value, 1), slice(value, 1),
) )
if (success) { if (success) {
if (newForm.reload) { updated = true
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
$message.success(i18n.t('dialogue.handle_succ')) $message.success(i18n.t('dialogue.handle_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -172,6 +165,12 @@ const onAdd = async () => {
} }
break break
} }
if (updated) {
if (newForm.reload) {
browserStore.reloadKey({ server, db, key: keyName })
}
}
dialogStore.closeAddFieldsDialog() dialogStore.closeAddFieldsDialog()
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import useDialog from 'stores/dialog' import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { isEmpty, size } from 'lodash' import { isEmpty, size } from 'lodash'
@ -29,18 +29,22 @@ watch(
deleteForm.loadingAffected = false deleteForm.loadingAffected = false
deleteForm.affectedKeys = [] deleteForm.affectedKeys = []
deleteForm.async = true deleteForm.async = true
loading.value = false
} }
}, },
) )
const loading = ref(false)
const scanAffectedKey = async () => { const scanAffectedKey = async () => {
try { try {
loading.value = true
deleteForm.loadingAffected = true deleteForm.loadingAffected = true
const { keys = [] } = await browserStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key) const { keys = [] } = await browserStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
deleteForm.affectedKeys = keys || [] deleteForm.affectedKeys = keys || []
deleteForm.showAffected = true deleteForm.showAffected = true
} finally { } finally {
deleteForm.loadingAffected = false deleteForm.loadingAffected = false
loading.value = false
} }
} }
@ -52,6 +56,7 @@ const resetAffected = () => {
const i18n = useI18n() const i18n = useI18n()
const onConfirmDelete = async () => { const onConfirmDelete = async () => {
try { try {
loading.value = true
const { server, db, key, async } = deleteForm const { server, db, key, async } = deleteForm
const success = await browserStore.deleteKeyPrefix(server, db, key, async) const success = await browserStore.deleteKeyPrefix(server, db, key, async)
if (success) { if (success) {
@ -59,6 +64,9 @@ const onConfirmDelete = async () => {
} }
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
return
} finally {
loading.value = false
} }
dialogStore.closeDeleteKeyDialog() dialogStore.closeDeleteKeyDialog()
} }
@ -78,6 +86,7 @@ const onClose = () => {
:title="$t('interface.batch_delete_key')" :title="$t('interface.batch_delete_key')"
preset="dialog" preset="dialog"
transform-origin="center"> transform-origin="center">
<n-spin :show="loading">
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top"> <n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('dialogue.key.server')"> <n-form-item :label="$t('dialogue.key.server')">
<n-input :value="deleteForm.server" readonly /> <n-input :value="deleteForm.server" readonly />
@ -89,9 +98,14 @@ const onClose = () => {
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" /> <n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.key.async_delete')" required> <n-form-item :label="$t('dialogue.key.async_delete')" required>
<n-checkbox v-model:checked="deleteForm.async">{{ $t('dialogue.key.async_delete_title') }}</n-checkbox> <n-checkbox v-model:checked="deleteForm.async">
{{ $t('dialogue.key.async_delete_title') }}
</n-checkbox>
</n-form-item> </n-form-item>
<n-card v-if="deleteForm.showAffected" :title="$t('dialogue.key.affected_key')" size="small"> <n-card
v-if="deleteForm.showAffected"
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
size="small">
<n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text /> <n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text />
<n-log <n-log
v-else v-else
@ -101,17 +115,24 @@ const onClose = () => {
style="user-select: text; cursor: text" /> style="user-select: text; cursor: text" />
</n-card> </n-card>
</n-form> </n-form>
</n-spin>
<template #action> <template #action>
<div class="flex-item n-dialog__action"> <div class="flex-item n-dialog__action">
<n-button :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button> <n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button v-if="!deleteForm.showAffected" :focusable="false" type="primary" @click="scanAffectedKey"> <n-button
v-if="!deleteForm.showAffected"
:focusable="false"
:loading="loading"
type="primary"
@click="scanAffectedKey">
{{ $t('dialogue.key.show_affected_key') }} {{ $t('dialogue.key.show_affected_key') }}
</n-button> </n-button>
<n-button <n-button
v-else v-else
:disabled="isEmpty(deleteForm.affectedKeys)" :disabled="isEmpty(deleteForm.affectedKeys)"
:focusable="false" :focusable="false"
:loading="loading"
type="error" type="error"
@click="onConfirmDelete"> @click="onConfirmDelete">
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }} {{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import useDialog from 'stores/dialog' import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
@ -23,13 +23,16 @@ watch(
flushForm.db = db flushForm.db = db
flushForm.async = true flushForm.async = true
flushForm.confirm = false flushForm.confirm = false
loading.value = false
} }
}, },
) )
const loading = ref(false)
const i18n = useI18n() const i18n = useI18n()
const onConfirmFlush = async () => { const onConfirmFlush = async () => {
try { try {
loading.value = true
const { server, db, async } = flushForm const { server, db, async } = flushForm
const success = await browserStore.flushDatabase(server, db, async) const success = await browserStore.flushDatabase(server, db, async)
if (success) { if (success) {
@ -37,6 +40,9 @@ const onConfirmFlush = async () => {
} }
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
return
} finally {
loading.value = false
} }
dialogStore.closeFlushDBDialog() dialogStore.closeFlushDBDialog()
} }
@ -56,6 +62,7 @@ const onClose = () => {
:title="$t('interface.flush_db')" :title="$t('interface.flush_db')"
preset="dialog" preset="dialog"
transform-origin="center"> transform-origin="center">
<n-spin :show="loading">
<n-form :model="flushForm" :show-require-mark="false" label-placement="top"> <n-form :model="flushForm" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('dialogue.key.server')"> <n-form-item :label="$t('dialogue.key.server')">
<n-input :value="flushForm.server" readonly /> <n-input :value="flushForm.server" readonly />
@ -64,7 +71,9 @@ const onClose = () => {
<n-input :value="flushForm.db.toString()" readonly /> <n-input :value="flushForm.db.toString()" readonly />
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.key.async_delete')" required> <n-form-item :label="$t('dialogue.key.async_delete')" required>
<n-checkbox v-model:checked="flushForm.async">{{ $t('dialogue.key.async_delete_title') }}</n-checkbox> <n-checkbox v-model:checked="flushForm.async">
{{ $t('dialogue.key.async_delete_title') }}
</n-checkbox>
</n-form-item> </n-form-item>
<n-form-item :label="$t('common.warning')" required> <n-form-item :label="$t('common.warning')" required>
<n-checkbox v-model:checked="flushForm.confirm"> <n-checkbox v-model:checked="flushForm.confirm">
@ -72,10 +81,16 @@ const onClose = () => {
</n-checkbox> </n-checkbox>
</n-form-item> </n-form-item>
</n-form> </n-form>
</n-spin>
<template #action> <template #action>
<n-button :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button> <n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button :disabled="!!!flushForm.confirm" :focusable="false" type="primary" @click="onConfirmFlush"> <n-button
:disabled="!!!flushForm.confirm"
:focusable="false"
:loading="loading"
type="primary"
@click="onConfirmFlush">
{{ $t('dialogue.key.confirm_flush_db') }} {{ $t('dialogue.key.confirm_flush_db') }}
</n-button> </n-button>
</template> </template>

View File

@ -130,7 +130,7 @@ const onAdd = async () => {
if (success) { if (success) {
// select current key // select current key
tabStore.setSelectedKeys(server, nodeKey) tabStore.setSelectedKeys(server, nodeKey)
browserStore.loadKeyValue(server, db, key).then(() => {}) browserStore.loadKeySummary({ server, db, key })
} else if (!isEmpty(msg)) { } else if (!isEmpty(msg)) {
$message.error(msg) $message.error(msg)
} }

View File

@ -3,6 +3,7 @@ import { reactive, watch } from 'vue'
import useDialog from 'stores/dialog' import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import useTabStore from 'stores/tab.js'
const renameForm = reactive({ const renameForm = reactive({
server: '', server: '',
@ -13,6 +14,7 @@ const renameForm = reactive({
const dialogStore = useDialog() const dialogStore = useDialog()
const browserStore = useBrowserStore() const browserStore = useBrowserStore()
const tab = useTabStore()
watch( watch(
() => dialogStore.renameDialogVisible, () => dialogStore.renameDialogVisible,
(visible) => { (visible) => {
@ -30,9 +32,10 @@ const i18n = useI18n()
const onRename = async () => { const onRename = async () => {
try { try {
const { server, db, key, newKey } = renameForm const { server, db, key, newKey } = renameForm
const { success, msg } = await browserStore.renameKey(server, db, key, newKey) const { success, msg, nodeKey } = await browserStore.renameKey(server, db, key, newKey)
if (success) { if (success) {
await browserStore.loadKeyValue(server, db, newKey) tab.setSelectedKeys(server, nodeKey)
browserStore.loadKeySummary({ server, db, key: newKey })
$message.success(i18n.t('dialogue.handle_succ')) $message.success(i18n.t('dialogue.handle_succ'))
} else { } else {
$message.error(msg) $message.error(msg)

View File

@ -272,11 +272,20 @@ const handleSelectContextMenu = (key) => {
const { match: pattern, type } = browserStore.getKeyFilter(props.server, db) const { match: pattern, type } = browserStore.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':
// browserStore.loadKeys(props.server, db, redisKey) if (node != null && !!!node.loading) {
// break node.loading = true
browserStore.reloadLayer(props.server, db, redisKey).finally(() => {
delete node.loading
})
}
break
case 'value_reload': case 'value_reload':
browserStore.loadKeyValue(props.server, db, redisKey) browserStore.reloadKey({
server: props.server,
db,
key: redisKey,
})
break break
case 'key_remove': case 'key_remove':
dialogStore.openDeleteKeyDialog(props.server, db, isEmpty(redisKey) ? '*' : redisKey + ':*') dialogStore.openDeleteKeyDialog(props.server, db, isEmpty(redisKey) ? '*' : redisKey + ':*')
@ -378,14 +387,18 @@ const onUpdateSelectedKeys = (keys, options) => {
const { key, db } = node const { key, db } = node
const redisKey = node.redisKeyCode || node.redisKey const redisKey = node.redisKeyCode || node.redisKey
if (!includes(selectedKeys.value, key)) { if (!includes(selectedKeys.value, key)) {
browserStore.loadKeyValue(props.server, db, redisKey) browserStore.loadKeySummary({
server: props.server,
db,
key: redisKey,
})
} }
return return
} }
} }
} }
// default is load blank key to display server status // default is load blank key to display server status
browserStore.loadKeyValue(props.server, 0) tabStore.openBlank(props.server)
} finally { } finally {
tabStore.setSelectedKeys(props.server, keys) tabStore.setSelectedKeys(props.server, keys)
} }
@ -511,7 +524,7 @@ const renderIconMenu = (items) => {
) )
} }
const getDatabaseMenu = (opened, loading, end) => { const calcDBMenu = (opened, loading, end) => {
const btns = [] const btns = []
if (opened) { if (opened) {
btns.push( btns.push(
@ -573,14 +586,16 @@ const getDatabaseMenu = (opened, loading, end) => {
return btns return btns
} }
const getLayerMenu = () => { const calcLayerMenu = (loading, end) => {
return [ return [
// disable reload by layer, due to conflict with partial loading keys // reload layer enable only full loaded
// h(IconButton, { h(IconButton, {
// tTooltip: 'interface.reload', tTooltip: end === true ? 'interface.reload' : 'interface.reload',
// icon: Refresh, icon: Refresh,
// onClick: () => handleSelectContextMenu('key_reload'), loading: loading === true,
// }), disabled: end !== true,
onClick: () => handleSelectContextMenu('key_reload'),
}),
h(IconButton, { h(IconButton, {
tTooltip: 'interface.new_key', tTooltip: 'interface.new_key',
icon: Add, icon: Add,
@ -594,7 +609,7 @@ const getLayerMenu = () => {
] ]
} }
const getValueMenu = () => { const calcValueMenu = () => {
return [ return [
h(IconButton, { h(IconButton, {
tTooltip: 'interface.remove_key', tTooltip: 'interface.remove_key',
@ -609,11 +624,12 @@ 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, option.loading, option.fullLoaded)) return renderIconMenu(calcDBMenu(option.opened, option.loading, option.fullLoaded))
case ConnectionType.RedisKey: case ConnectionType.RedisKey:
return renderIconMenu(getLayerMenu()) const fullLoaded = browserStore.isFullLoaded(props.server, option.db)
return renderIconMenu(calcLayerMenu(option.loading, fullLoaded))
case ConnectionType.RedisValue: case ConnectionType.RedisValue:
return renderIconMenu(getValueMenu()) return renderIconMenu(calcValueMenu())
} }
} }
return null return null
@ -640,7 +656,7 @@ const nodeProps = ({ option }) => {
contextMenuParam.x = e.clientX contextMenuParam.x = e.clientX
contextMenuParam.y = e.clientY contextMenuParam.y = e.clientY
contextMenuParam.show = true contextMenuParam.show = true
tabStore.setSelectedKeys(props.server, option.key) onUpdateSelectedKeys([option.key], [option])
}) })
}, },
// onMouseover() { // onMouseover() {

View File

@ -90,6 +90,8 @@
"new_key": "Add Key", "new_key": "Add Key",
"load_more": "Load More Keys", "load_more": "Load More Keys",
"load_all": "Load All Left Keys", "load_all": "Load All Left Keys",
"load_more_entries": "Load More",
"load_all_entries": "Load All",
"more_action": "More Action", "more_action": "More Action",
"nonexist_tab_content": "Selected key does not exist or no key is selected. Please retry", "nonexist_tab_content": "Selected key does not exist or no key is selected. Please retry",
"empty_server_content": "Select and open a connection from the left", "empty_server_content": "Select and open a connection from the left",

View File

@ -1,7 +1,9 @@
import en from './en-us' import en from './en-us'
import pt from './pt-br'
import zh from './zh-cn' import zh from './zh-cn'
export const lang = { export const lang = {
en, en,
pt,
zh, zh,
} }

View File

@ -0,0 +1,308 @@
{
"name": "Português",
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",
"success": "Sucesso",
"warning": "Aviso",
"error": "Erro",
"save": "Salvar",
"none": "Nenhum",
"second": "Segundo(s)",
"unit_day": "D",
"unit_hour": "H",
"unit_minute": "M",
"all": "Tudo",
"key": "Chave",
"value": "Valor",
"field": "Campo"
},
"preferences": {
"name": "Preferências",
"restore_defaults": "Restaurar Padrões",
"general": {
"name": "Geral",
"theme": "Tema",
"theme_light": "Claro",
"theme_dark": "Escuro",
"theme_auto": "Automático",
"language": "Idioma",
"system_lang": "Usar Idioma do Sistema",
"default": "Padrão",
"font": "Fonte",
"font_size": "Tamanho da Fonte",
"scan_size": "Tamanho Padrão para Comando SCAN",
"proxy": "Proxy",
"use_system_proxy": "Usar proxy do sistema",
"use_system_proxy_http": "Usar proxy do sistema apenas para solicitações HTTP(S)",
"update": "Atualizar",
"auto_check_update": "Verificar atualizações automaticamente"
},
"editor": {
"name": "Editor"
}
},
"interface": {
"new_conn": "Adicionar Nova Conexão",
"new_group": "Adicionar Novo Grupo",
"disconnect_all": "Desconectar tudo",
"status": "Status",
"filter": "Filtro",
"sort_conn": "Ordenar Conexões",
"new_conn_title": "Nova Conexão",
"open_db": "Abrir Banco de Dados",
"close_db": "Fechar Banco de Dados",
"filter_key": "Filtrar Chave",
"disconnect": "Desconectar",
"dup_conn": "Duplicar Conexão",
"remove_conn": "Excluir Conexão",
"edit_conn": "Editar Configuração da Conexão",
"edit_conn_group": "Editar Grupo de Conexão",
"rename_conn_group": "Renomear Grupo de Conexão",
"remove_conn_group": "Excluir Grupo de Conexão",
"ttl": "TTL",
"forever": "Para Sempre",
"rename_key": "Renomear Chave",
"delete_key": "Excluir Chave",
"batch_delete_key": "Excluir Lotes de Chaves",
"flush_db": "Limpar Banco de Dados",
"copy_value": "Copiar Valor",
"edit_value": "Editar Valor",
"save_update": "Salvar Atualização",
"score_filter_tip": "Lista de operadores suportados abaixo:\n= igual\n!= diferente\n> maior que\n>= maior ou igual a\n< menor que\n<= menor ou igual a\nPor exemplo, se você deseja filtrar resultados maiores que 3, insira: >3",
"add_row": "Adicionar Linha",
"edit_row": "Editar Linha",
"delete_row": "Excluir Linha",
"search": "Buscar",
"filter_field": "Filtrar Campo",
"filter_value": "Filtrar Valor",
"length": "Tamanho",
"entries": "Entradas",
"memory_usage": "Uso de Memória",
"view_as": "Visualizar Como",
"decode_with": "Decodificar / Descompressão",
"reload": "Recarregar",
"open_connection": "Abrir Conexão",
"copy_path": "Copiar Caminho",
"copy_key": "Copiar Chave",
"binary_key": "Nome da Chave Binária",
"remove_key": "Remover Chave",
"new_key": "Adicionar Chave",
"load_more": "Carregar Mais Chaves",
"load_all": "Carregar Todas as Chaves Restantes",
"more_action": "Mais Ação",
"nonexist_tab_content": "A chave selecionada não existe ou nenhuma chave está selecionada. Por favor, tente novamente",
"empty_server_content": "Selecione e abra uma conexão à esquerda",
"empty_server_list": "Nenhum servidor Redis",
"action": "Ação",
"type": "Tipo",
"score": "Pontuação",
"cli_welcome": "Bem-vindo ao Console Redis Tiny RDM",
"sub_tab": {
"status": "Status",
"key_detail": "Detalhes da Chave",
"cli": "Console",
"slow_log": "Log Lento",
"cmd_monitor": "Monitorar Comandos",
"pub_message": "Pub/Sub"
}
},
"ribbon": {
"server": "Servidor",
"browser": "Navegador de Dados",
"log": "Log"
},
"dialogue": {
"close_confirm": "Confirmar o fechamento desta guia e conexão ({name})",
"edit_close_confirm": "Por favor, feche as conexões relevantes antes de editar. Deseja continuar?",
"opening_connection": "Abrindo Conexão...",
"interrupt_connection": "Cancelar",
"remove_tip": "{type} \"{name}\" será excluído",
"remove_group_tip": "O grupo \"{name}\" e todas as conexões nele serão excluídos",
"delete_key_succ": "\"{key}\" foi excluída",
"save_value_succ": "Valor Salvo!",
"copy_succ": "Valor Copiado!",
"rename_binary_key_fail": "Renomear nome de chave binária não é suportado",
"handle_succ": "Sucesso!",
"reload_succ": "Recarregado!",
"field_required": "Este item não deve ficar em branco",
"spec_field_required": "\"{key}\" não deve ficar em branco",
"illegal_characters": "Inclui caracteres ilegais",
"connection": {
"new_title": "Nova Conexão",
"edit_title": "Editar Conexão",
"general": "Geral",
"no_group": "Sem Grupo",
"group": "Grupo",
"conn_name": "Nome",
"addr": "Endereço",
"usr": "Nome de Usuário",
"pwd": "Senha",
"name_tip": "Nome da Conexão",
"addr_tip": "Host do servidor Redis",
"usr_tip": "(Opcional) Nome de usuário para autenticação",
"pwd_tip": "(Opcional) Senha de autenticação (Redis > 6.0)",
"test": "Testar Conexão",
"test_succ": "Conexão bem-sucedida ao servidor Redis",
"test_fail": "Falha na Conexão",
"advn": {
"title": "Avançado",
"filter": "Padrão de Filtro de Chave Padrão",
"filter_tip": "Padrão que define as chaves carregadas do servidor Redis",
"separator": "Separador de Chave",
"separator_tip": "Separador para item do caminho da chave",
"conn_timeout": "Tempo Limite de Conexão",
"exec_timeout": "Tempo Limite de Execução",
"dbfilter_type": "Filtro de Banco de Dados",
"dbfilter_all": "Mostrar Todos",
"dbfilter_show": "Mostrar Selecionados",
"dbfilter_hide": "Ocultar Selecionados",
"dbfilter_show_title": "Selecione os Bancos de Dados para Mostrar",
"dbfilter_hide_title": "Selecione os Bancos de Dados para Ocultar",
"dbfilter_input": "Índice do Banco de Dados de Entrada",
"dbfilter_input_tip": "Pressione Enter para confirmar",
"key_view": "Visualização Padrão de Chave",
"key_view_tree": "Visualização em Árvore",
"key_view_list": "Visualização em Lista",
"load_size": "Tamanho das Chaves por Carga",
"mark_color": "Cor de Marcação"
},
"ssl": {
"title": "SSL/TLS",
"enable": "Habilitar SSL/TLS",
"cert_file": "Chave Pública",
"key_file": "Chave Privada",
"ca_file": "Autoridade",
"cert_file_tip": "Arquivo de Chave Pública no formato PEM (Cert)",
"key_file_tip": "Arquivo de Chave Privada no formato PEM (Chave)",
"ca_file_tip": "Arquivo de Autoridade de Certificação no formato PEM (CA)"
},
"ssh": {
"title": "Túnel SSH",
"enable": "Habilitar Túnel SSH",
"login_type": "Tipo de Login",
"pkfile": "Arquivo de Chave Privada",
"passphrase": "Frase de Senha",
"addr_tip": "Host do Servidor SSH",
"usr_tip": "Nome de Usuário SSH",
"pwd_tip": "Senha SSH",
"pkfile_tip": "Caminho do Arquivo de Chave Privada SSH",
"passphrase_tip": "(Opcional) Frase de Senha para Chave Privada"
},
"sentinel": {
"title": "Sentinela",
"enable": "Atuar como Nó Sentinela",
"master": "Nome do Grupo Master",
"auto_discover": "Auto Descoberta",
"password": "Senha para Nó Master",
"username": "Nome de Usuário para Nó Master",
"pwd_tip": "(Opcional) Nome de usuário para autenticação no nó master",
"usr_tip": "(Opcional) Senha de autenticação no nó master (Redis > 6.0)"
},
"cluster": {
"title": "Cluster",
"enable": "Atuar como Nó Cluster",
"readonly": "Habilitar comandos somente leitura em nós escravos"
}
},
"group": {
"name": "Nome do Grupo",
"rename": "Renomear Grupo",
"new": "Novo Grupo"
},
"key": {
"new": "Nova Chave",
"new_name": "Novo Nome da Chave",
"persist_key": "Persistir Chave",
"server": "Conexão",
"db_index": "Índice do Banco de Dados",
"key_expression": "Expressão da Chave",
"affected_key": "Chaves Afetadas",
"show_affected_key": "Mostrar Chaves Afetadas",
"confirm_delete_key": "Confirmar Exclusão de {num} Chave(s)",
"async_delete": "Executar de Forma Assíncrona",
"async_delete_title": "Não esperar pelo resultado da operação",
"confirm_flush": "Eu sei o que estou fazendo!",
"confirm_flush_db": "Confirmar Limpar Banco de Dados"
},
"field": {
"new": "Adicionar Novo Campo",
"new_item": "Adicionar Novo Item",
"overwrite_field": "Sobrescrever Campo Existente",
"ignore_field": "Ignorar Campo Existente",
"insert_type": "Inserir",
"append_item": "Anexar",
"prepend_item": "Inserir no Início",
"enter_key": "Digite a Chave",
"enter_value": "Digite o Valor",
"enter_field": "Digite o Nome do Campo",
"enter_elem": "Digite o Elemento",
"enter_member": "Digite o Membro",
"enter_score": "Digite a Pontuação",
"element": "Elemento",
"reload_when_succ": "Recarregar imediatamente após o sucesso"
},
"filter": {
"set_key_filter": "Definir Filtro de Chave",
"filter_pattern": "Padrão",
"filter_pattern_tip": "prefixo_*: Corresponde a nomes de chaves que começam com \"prefixo_\".\n*_sufixo: Corresponde a nomes de chaves que terminam com \"_sufixo\".\n*padrão*: Corresponde a nomes de chaves que contêm \"padrão\".\nprefixo_??: Corresponde a nomes de chaves que começam com \"prefixo_\" seguido por dois caracteres.\n*abc*: Corresponde a nomes de chaves que contêm \"abc\" em qualquer posição."
},
"ttl": {
"title": "Definir TTL da Chave"
},
"upgrade": {
"title": "Nova Versão Disponível",
"new_version_tip": "Uma nova versão ({ver}) está disponível. Baixar agora?",
"no_update": "Você está atualizado",
"download_now": "Baixar",
"later": "Mais Tarde",
"skip": "Pular Esta Atualização"
},
"about": {
"source": "Código Fonte",
"website": "Site Oficial"
}
},
"menu": {
"minimise": "Minimizar",
"maximise": "Maximizar",
"restore": "Restaurar",
"close": "Fechar",
"preferences": "Preferências",
"help": "Ajuda",
"check_update": "Verificar Atualizações...",
"about": "Sobre"
},
"log": {
"title": "Registro de Inicialização",
"filter_server": "Filtrar Servidor",
"filter_keyword": "Filtrar Palavra-chave",
"clean_log": "Limpar Registro de Inicialização",
"confirm_clean_log": "Confirmar limpar registro de inicialização",
"exec_time": "Tempo de Execução",
"server": "Servidor",
"cmd": "Comando",
"cost_time": "Custo",
"refresh": "Atualizar"
},
"status": {
"uptime": "Tempo de Atividade",
"connected_clients": "Clientes Conectados",
"total_keys": "Chaves Totais",
"memory_used": "Memória Usada",
"all_info": "Informações",
"refresh": "Atualizar",
"auto_refresh": "Atualização Automática"
},
"slog": {
"title": "Registro de Operações Lentas",
"limit": "Limite",
"filter": "Filtrar",
"exec_time": "Tempo",
"client": "Cliente",
"cmd": "Comando",
"cost_time": "Custo",
"refresh": "Atualizar Agora",
"auto_refresh": "Atualização Automática"
}
}

View File

@ -90,6 +90,8 @@
"new_key": "添加新键", "new_key": "添加新键",
"load_more": "加载更多键", "load_more": "加载更多键",
"load_all": "加载剩余所有键", "load_all": "加载剩余所有键",
"load_more_entries": "加载更多",
"load_all_entries": "加载全部",
"more_action": "更多操作", "more_action": "更多操作",
"nonexist_tab_content": "所选键不存在或未选中任何键,请尝试刷新重试", "nonexist_tab_content": "所选键不存在或未选中任何键,请尝试刷新重试",
"empty_server_content": "可以从左边选择并打开连接", "empty_server_content": "可以从左边选择并打开连接",

View File

@ -5,6 +5,7 @@ import {
get, get,
isEmpty, isEmpty,
join, join,
last,
remove, remove,
set, set,
size, size,
@ -24,7 +25,8 @@ import {
DeleteKey, DeleteKey,
FlushDB, FlushDB,
GetCmdHistory, GetCmdHistory,
GetKeyValue, GetKeyDetail,
GetKeySummary,
GetSlowLogs, GetSlowLogs,
LoadAllKeys, LoadAllKeys,
LoadNextKeys, LoadNextKeys,
@ -58,7 +60,7 @@ const useBrowserStore = defineStore('browser', {
* @property {number} type * @property {number} type
* @property {number} [db] - database index, type == ConnectionType.RedisDB only * @property {number} [db] - database index, type == ConnectionType.RedisDB only
* @property {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue 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[]} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
* @property {number} [keys] - children key count * @property {number} [keys] - children key count
* @property {number} [maxKeys] - max key count for database * @property {number} [maxKeys] - max key count for database
* @property {boolean} [isLeaf] * @property {boolean} [isLeaf]
@ -137,7 +139,7 @@ const useBrowserStore = defineStore('browser', {
* get database by server name and index * get database by server name and index
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @return {{}|null} * @return {DatabaseItem|null}
*/ */
getDatabase(connName, db) { getDatabase(connName, db) {
const dbs = this.databases[connName] const dbs = this.databases[connName]
@ -150,6 +152,20 @@ const useBrowserStore = defineStore('browser', {
return null return null
}, },
/**
* get full loaded status of database
* @param connName
* @param db
* @return {boolean}
*/
isFullLoaded(connName, db) {
const selDB = this.getDatabase(connName, db)
if (selDB != null) {
return selDB.fullLoaded === true
}
return false
},
/** /**
* switch key view * switch key view
* @param {string} connName * @param {string} connName
@ -344,20 +360,23 @@ const useBrowserStore = defineStore('browser', {
}, },
/** /**
* load redis key * load key summary info
* @param {string} server * @param {string} server
* @param {number} db * @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|number[]} [key] null or blank indicate that update tab to display normal content (blank content or server status)
* @param {string} [viewType] * @return {Promise<void>}
* @param {string} [decodeType]
*/ */
async loadKeyValue(server, db, key, viewType, decodeType) { async loadKeySummary({ server, db, key }) {
try { try {
const tab = useTabStore() const tab = useTabStore()
if (!isEmpty(key)) { if (!isEmpty(key)) {
const { data, success, msg } = await GetKeyValue(server, db, key, viewType, decodeType) const { data, success, msg } = await GetKeySummary({
server,
db,
key,
})
if (success) { if (success) {
const { type, ttl, value, size, length, viewAs, decode } = data const { type, ttl, size, length } = data
const k = decodeRedisKey(key) const k = decodeRedisKey(key)
const binaryKey = k !== key const binaryKey = k !== key
tab.upsertTab({ tab.upsertTab({
@ -368,16 +387,13 @@ const useBrowserStore = defineStore('browser', {
ttl, ttl,
keyCode: binaryKey ? key : undefined, keyCode: binaryKey ? key : undefined,
key: k, key: k,
value,
size, size,
length, length,
viewAs,
decode,
}) })
return return
} else { } else {
if (!isEmpty(msg)) { if (!isEmpty(msg)) {
$message.error('load key fail: ' + msg) $message.error('load key summary fail: ' + msg)
} }
// its danger to delete "non-exists" key, just remove from tree view // its danger to delete "non-exists" key, just remove from tree view
await this.deleteKey(server, db, key, true) await this.deleteKey(server, db, key, true)
@ -397,18 +413,83 @@ const useBrowserStore = defineStore('browser', {
size: 0, size: 0,
length: 0, length: 0,
}) })
} catch (e) {
$message.error('')
} finally { } finally {
} }
}, },
/**
* reload key
* @param server
* @param db
* @param key
* @return {Promise<void>}
*/
async reloadKey({ server, db, key }) {
const tab = useTabStore()
try {
tab.updateLoading({ server, db, loading: true })
await this.loadKeySummary({ server, db, key })
await this.loadKeyDetail({ server, db, key, reset: true })
} finally {
tab.updateLoading({ server, db, loading: false })
}
},
/**
* load key content
* @param {string} server
* @param {number} db
* @param {string|number[]} key
* @param {string} [viewType]
* @param {string} [decodeType]
* @param {string} [matchPattern]
* @param {boolean} [reset]
* @param {boolean} [full]
* @return {Promise<void>}
*/
async loadKeyDetail({ server, db, key, viewType, decodeType, matchPattern, reset, full }) {
const tab = useTabStore()
try {
tab.updateLoading({ server, db, loading: true })
const { data, success, msg } = await GetKeyDetail({
server,
db,
key,
viewAs: viewType,
decodeType,
matchPattern,
full: full === true,
reset,
lite: true,
})
if (success) {
const { value, viewAs, decodeType: decode, end } = data
tab.updateValue({
server,
db,
key: decodeRedisKey(key),
value,
viewAs,
decode,
reset: reset || full === true,
end,
})
}
} finally {
tab.updateLoading({ server, db, loading: false })
}
},
/** /**
* scan keys with prefix * scan keys with prefix
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {string} match * @param {string} match
* @param {string} matchType * @param {string} [matchType]
* @param {boolean} [full] * @param {boolean} [full]
* @returns {Promise<{keys: string[], end: boolean}>} * @returns {Promise<{keys: string[], maxKeys: number, end: boolean}>}
*/ */
async scanKeys(connName, db, match, matchType, full) { async scanKeys(connName, db, match, matchType, full) {
let resp let resp
@ -421,8 +502,8 @@ const useBrowserStore = defineStore('browser', {
if (!success) { if (!success) {
throw new Error(msg) throw new Error(msg)
} }
const { keys = [], end } = data const { keys = [], maxKeys, end } = data
return { keys, end, success } return { keys, end, maxKeys, success }
}, },
/** /**
@ -432,7 +513,7 @@ const useBrowserStore = defineStore('browser', {
* @param {string|null} prefix * @param {string|null} prefix
* @param {string|null} matchType * @param {string|null} matchType
* @param {boolean} [all] * @param {boolean} [all]
* @return {Promise<{keys: Array<string|number[]>, end: boolean}>} * @return {Promise<{keys: Array<string|number[]>, maxKeys: number, end: boolean}>}
* @private * @private
*/ */
async _loadKeys(connName, db, prefix, matchType, all) { async _loadKeys(connName, db, prefix, matchType, all) {
@ -456,7 +537,8 @@ const useBrowserStore = defineStore('browser', {
*/ */
async loadMoreKeys(connName, db) { async loadMoreKeys(connName, db) {
const { match, type: keyType } = this.getKeyFilter(connName, db) const { match, type: keyType } = this.getKeyFilter(connName, db)
const { keys, end } = await this._loadKeys(connName, db, match, keyType, false) const { keys, maxKeys, end } = await this._loadKeys(connName, db, match, keyType, false)
this._setDBMaxKeys(connName, db, maxKeys)
// remove current keys below prefix // remove current keys below prefix
this._addKeyNodes(connName, db, keys) this._addKeyNodes(connName, db, keys)
this._tidyNode(connName, db, '') this._tidyNode(connName, db, '')
@ -471,11 +553,44 @@ const useBrowserStore = defineStore('browser', {
*/ */
async loadAllKeys(connName, db) { async loadAllKeys(connName, db) {
const { match, type: keyType } = this.getKeyFilter(connName, db) const { match, type: keyType } = this.getKeyFilter(connName, db)
const { keys } = await this._loadKeys(connName, db, match, keyType, true) const { keys, maxKeys } = await this._loadKeys(connName, db, match, keyType, true)
this._setDBMaxKeys(connName, db, maxKeys)
this._addKeyNodes(connName, db, keys) this._addKeyNodes(connName, db, keys)
this._tidyNode(connName, db, '') this._tidyNode(connName, db, '')
}, },
/**
* reload keys under layer
* @param {string} connName
* @param {number} db
* @param {string} prefix
* @return {Promise<void>}
*/
async reloadLayer(connName, db, prefix) {
if (isEmpty(prefix)) {
return
}
let match = prefix
const separator = this._getSeparator(connName)
if (!endsWith(match, separator)) {
match += separator + '*'
} else {
match += '*'
}
// FIXME: ignore original match pattern due to redis not support combination matching
const { match: originMatch, type: keyType } = this.getKeyFilter(connName, db)
const { keys, maxKeys, success } = await this._loadKeys(connName, db, match, keyType, true)
if (!success) {
return
}
this._setDBMaxKeys(connName, db, maxKeys)
// remove current keys below prefix
this._deleteKeyNode(connName, db, prefix, true)
this._addKeyNodes(connName, db, keys)
this._tidyNode(connName, db, prefix)
},
/** /**
* get custom separator of connection * get custom separator of connection
* @param server * @param server
@ -749,13 +864,16 @@ const useBrowserStore = defineStore('browser', {
}, },
/** /**
* update max key by value * update max key by increase/decrease value
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {number} updateValue * @param {number} [updateValue]
* @private * @private
*/ */
_updateDBMaxKeys(connName, db, updateValue) { _updateDBMaxKeys(connName, db, updateValue) {
if (updateValue === undefined) {
return
}
const database = this.getDatabase(connName, db) const database = this.getDatabase(connName, db)
if (database != null) { if (database != null) {
const maxKeys = get(database, 'maxKeys', 0) const maxKeys = get(database, 'maxKeys', 0)
@ -764,15 +882,16 @@ const useBrowserStore = defineStore('browser', {
}, },
/** /**
* set db max keys to 0 * set db max keys value
* @param connName * @param {string} connName
* @param db * @param {number} db
* @param {number} maxKeys
* @private * @private
*/ */
_emptyDBMaxKeys(connName, db) { _setDBMaxKeys(connName, db, maxKeys) {
const database = this.getDatabase(connName, db) const database = this.getDatabase(connName, db)
if (database != null) { if (database != null) {
set(database, 'maxKeys', 0) set(database, 'maxKeys', maxKeys)
} }
}, },
@ -857,7 +976,14 @@ const useBrowserStore = defineStore('browser', {
try { try {
const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '') const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '')
if (success) { if (success) {
const { updated = {} } = data const { updated = {}, removed = [], replaced = {} } = data
const tab = useTabStore()
if (!isEmpty(removed)) {
tab.removeValueEntries({ server: connName, db, key, type: 'hash', entries: removed })
}
if (!isEmpty(updated)) {
tab.upsertValueEntries({ server: connName, db, key, type: 'hash', entries: updated })
}
return { success, updated } return { success, updated }
} else { } else {
return { success, msg } return { success, msg }
@ -881,6 +1007,8 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems) const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
if (success) { if (success) {
const { updated = {} } = data const { updated = {} } = data
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'hash', entries: updated })
return { success, updated } return { success, updated }
} else { } else {
return { success: false, msg } return { success: false, msg }
@ -903,6 +1031,10 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '') const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '')
if (success) { if (success) {
const { removed = [] } = data const { removed = [] } = data
if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'hash', entries: removed })
}
return { success, removed } return { success, removed }
} else { } else {
return { success, msg } return { success, msg }
@ -935,13 +1067,24 @@ const useBrowserStore = defineStore('browser', {
* @param db * @param db
* @param key * @param key
* @param values * @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: []>} * @returns {Promise<{[msg]: string, success: boolean, [item]: []}>}
*/ */
async prependListItem(connName, db, key, values) { async prependListItem(connName, db, key, values) {
try { try {
const { data, success, msg } = await AddListItem(connName, db, key, 0, values) const { data, success, msg } = await AddListItem(connName, db, key, 0, values)
if (success) { if (success) {
const { left = [] } = data const { left = [] } = data
if (!isEmpty(left)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: right,
prepend: true,
})
}
return { success, item: left } return { success, item: left }
} else { } else {
return { success: false, msg } return { success: false, msg }
@ -957,13 +1100,24 @@ const useBrowserStore = defineStore('browser', {
* @param db * @param db
* @param key * @param key
* @param values * @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: any[]>} * @returns {Promise<{[msg]: string, success: boolean, [item]: any[]}>}
*/ */
async appendListItem(connName, db, key, values) { async appendListItem(connName, db, key, values) {
try { try {
const { data, success, msg } = await AddListItem(connName, db, key, 1, values) const { data, success, msg } = await AddListItem(connName, db, key, 1, values)
if (success) { if (success) {
const { right = [] } = data const { right = [] } = data
if (!isEmpty(right)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: right,
prepend: false,
})
}
return { success, item: right } return { success, item: right }
} else { } else {
return { success: false, msg } return { success: false, msg }
@ -987,6 +1141,16 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetListItem(connName, db, key, index, value) const { data, success, msg } = await SetListItem(connName, db, key, index, value)
if (success) { if (success) {
const { updated = {} } = data const { updated = {} } = data
if (!isEmpty(updated)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: updated,
})
}
return { success, updated } return { success, updated }
} else { } else {
return { success, msg } return { success, msg }
@ -1009,6 +1173,16 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetListItem(connName, db, key, index, '') const { data, success, msg } = await SetListItem(connName, db, key, index, '')
if (success) { if (success) {
const { removed = [] } = data const { removed = [] } = data
if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({
server: connName,
db,
key,
type: 'list',
entries: removed,
})
}
return { success, removed } return { success, removed }
} else { } else {
return { success, msg } return { success, msg }
@ -1023,13 +1197,18 @@ const useBrowserStore = defineStore('browser', {
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {string|number} key * @param {string|number} key
* @param {string} value * @param {string|string[]} value
* @returns {Promise<{[msg]: string, success: boolean}>} * @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async addSetItem(connName, db, key, value) { async addSetItem(connName, db, key, value) {
try { try {
const { success, msg } = await SetSetItem(connName, db, key, false, [value]) if (!value instanceof Array) {
value = [value]
}
const { data, success, msg } = await SetSetItem(connName, db, key, false, value)
if (success) { if (success) {
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: value })
return { success } return { success }
} else { } else {
return { success, msg } return { success, msg }
@ -1052,6 +1231,8 @@ const useBrowserStore = defineStore('browser', {
try { try {
const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue) const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue)
if (success) { if (success) {
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: { [value]: newValue } })
return { success: true } return { success: true }
} else { } else {
return { success, msg } return { success, msg }
@ -1073,6 +1254,8 @@ const useBrowserStore = defineStore('browser', {
try { try {
const { success, msg } = await SetSetItem(connName, db, key, true, [value]) const { success, msg } = await SetSetItem(connName, db, key, true, [value])
if (success) { if (success) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'set', entries: [value] })
return { success } return { success }
} else { } else {
return { success, msg } return { success, msg }
@ -1094,6 +1277,8 @@ const useBrowserStore = defineStore('browser', {
async addZSetItem(connName, db, key, action, vs) { async addZSetItem(connName, db, key, action, vs) {
try { try {
const { success, msg } = await AddZSetValue(connName, db, key, action, vs) const { success, msg } = await AddZSetValue(connName, db, key, action, vs)
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'zset', entries: vs })
if (success) { if (success) {
return { success } return { success }
} else { } else {
@ -1119,6 +1304,13 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score) const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score)
if (success) { if (success) {
const { updated, removed } = data const { updated, removed } = data
const tab = useTabStore()
if (!isEmpty(updated)) {
tab.upsertValueEntries({ server: connName, db, key, type: 'zset', entries: updated })
}
if (!isEmpty(removed)) {
tab.removeValueEntries({ server: connName, db, key, type: 'zset', entries: removed })
}
return { success, updated, removed } return { success, updated, removed }
} else { } else {
return { success, msg } return { success, msg }
@ -1141,7 +1333,11 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0) const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0)
if (success) { if (success) {
const { removed } = data const { removed } = data
return { success, removed } if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'zset', entries: removed })
}
return { success }
} else { } else {
return { success, msg } return { success, msg }
} }
@ -1157,14 +1353,22 @@ const useBrowserStore = defineStore('browser', {
* @param {string|number[]} key * @param {string|number[]} key
* @param {string} id * @param {string} id
* @param {string[]} values field1, value1, filed2, value2... * @param {string[]} values field1, value1, filed2, value2...
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} * @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async addStreamValue(connName, db, key, id, values) { async addStreamValue(connName, db, key, id, values) {
try { try {
const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values) const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values)
if (success) { if (success) {
const { updated = {} } = data const { updateID } = data
return { success, updated } const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'stream',
entries: [{ id: updateID, value: values }],
})
return { success }
} else { } else {
return { success: false, msg } return { success: false, msg }
} }
@ -1179,7 +1383,7 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string|number[]} key
* @param {string[]|string} ids * @param {string[]|string} ids
* @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>} * @returns {Promise<{[msg]: {}, success: boolean}>}
*/ */
async removeStreamValues(connName, db, key, ids) { async removeStreamValues(connName, db, key, ids) {
if (typeof ids === 'string') { if (typeof ids === 'string') {
@ -1188,8 +1392,9 @@ const useBrowserStore = defineStore('browser', {
try { try {
const { data = {}, success, msg } = await RemoveStreamValues(connName, db, key, ids) const { data = {}, success, msg } = await RemoveStreamValues(connName, db, key, ids)
if (success) { if (success) {
const { removed = [] } = data const tab = useTabStore()
return { success, removed } tab.removeValueEntries({ server: connName, db, key, type: 'stream', entries: ids })
return { success }
} else { } else {
return { success, msg } return { success, msg }
} }
@ -1215,6 +1420,34 @@ const useBrowserStore = defineStore('browser', {
} }
}, },
/**
*
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {string} newKey
* @private
*/
_renameKeyNode(connName, db, key, newKey) {
const nodeMap = this._getNodeMap(connName, db)
const nodeKey = `${ConnectionType.RedisValue}/${key}`
const newNodeKey = `${ConnectionType.RedisValue}/${newKey}`
const node = nodeMap.get(nodeKey)
if (node != null) {
// replace node map item
const separator = this._getSeparator(connName)
node.label = last(split(newKey, separator))
node.key = `${connName}/db${db}#${newNodeKey}`
node.redisKey = newKey
nodeMap[newNodeKey] = node
nodeMap.delete(nodeKey)
// replace key set item
const keySet = this._getKeySet(connName, db)
keySet.delete(key)
keySet.add(newKey)
}
},
/** /**
* *
* @param {string} connName * @param {string} connName
@ -1407,7 +1640,7 @@ const useBrowserStore = defineStore('browser', {
if (success === true) { if (success === true) {
// update tree view data // update tree view data
this._deleteKeyNode(connName, db) this._deleteKeyNode(connName, db)
this._emptyDBMaxKeys(connName, db) this._setDBMaxKeys(connName, db, 0)
// set tab content empty // set tab content empty
const tab = useTabStore() const tab = useTabStore()
tab.emptyTab(connName) tab.emptyTab(connName)
@ -1424,15 +1657,14 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db * @param {number} db
* @param {string} key * @param {string} key
* @param {string} newKey * @param {string} newKey
* @returns {Promise<{[msg]: string, success: boolean}>} * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: string}>}
*/ */
async renameKey(connName, db, key, newKey) { async renameKey(connName, db, key, newKey) {
const { success = false, msg } = await RenameKey(connName, db, key, newKey) const { success = false, msg } = await RenameKey(connName, db, key, newKey)
if (success) { if (success) {
// delete old key and add new key struct // delete old key and add new key struct
this._deleteKeyNode(connName, db, key) this._renameKeyNode(connName, db, key, newKey)
this._addKeyNodes(connName, db, [newKey]) return { success: true, nodeKey: `${connName}/db${db}#${ConnectionType.RedisValue}/${newKey}` }
return { success: true }
} else { } else {
return { success: false, msg } return { success: false, msg }
} }

View File

@ -1,4 +1,4 @@
import { find, findIndex, get, isEmpty, set, size } from 'lodash' import { assign, find, findIndex, get, indexOf, isEmpty, pullAt, remove, set, size } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
const useTabStore = defineStore('tab', { const useTabStore = defineStore('tab', {
@ -11,12 +11,18 @@ const useTabStore = defineStore('tab', {
* @property {string} [icon] tab icon * @property {string} [icon] tab icon
* @property {string[]} selectedKeys * @property {string[]} selectedKeys
* @property {string} [type] key type * @property {string} [type] key type
* @property {Object|Array} [value] key value * @property {*} [value] key value
* @property {string} [server] server name * @property {string} [server] server name
* @property {int} [db] database index * @property {int} [db] database index
* @property {string} [key] current key name * @property {string} [key] current key name
* @property {number[]|null|undefined} [keyCode] current key name as char array * @property {number[]|null|undefined} [keyCode] current key name as char array
* @param {number} [size] memory usage
* @param {number} [length] length of content or entries
* @property {int} [ttl] ttl of current key * @property {int} [ttl] ttl of current key
* @param {string} [viewAs]
* @param {string} [decode]
* @param {boolean} [end]
* @param {boolean} [loading]
*/ */
/** /**
@ -82,6 +88,10 @@ const useTabStore = defineStore('tab', {
} }
}, },
openBlank(server) {
this.upsertTab({ server, db: 0 })
},
/** /**
* update or insert a new tab if not exists with the same name * update or insert a new tab if not exists with the same name
* @param {string} subTab * @param {string} subTab
@ -94,10 +104,8 @@ const useTabStore = defineStore('tab', {
* @param {number} [size] * @param {number} [size]
* @param {number} [length] * @param {number} [length]
* @param {*} [value] * @param {*} [value]
* @param {string} [viewAs]
* @param {string} [decode]
*/ */
upsertTab({ subTab, server, db, type, ttl, key, keyCode, size, length, value, viewAs, decode }) { upsertTab({ subTab, server, db, type, ttl, key, keyCode, size, length }) {
let tabIndex = findIndex(this.tabList, { name: server }) let tabIndex = findIndex(this.tabList, { name: server })
if (tabIndex === -1) { if (tabIndex === -1) {
this.tabList.push({ this.tabList.push({
@ -112,9 +120,7 @@ const useTabStore = defineStore('tab', {
keyCode, keyCode,
size, size,
length, length,
value, value: undefined,
viewAs,
decode,
}) })
tabIndex = this.tabList.length - 1 tabIndex = this.tabList.length - 1
} else { } else {
@ -131,16 +137,239 @@ const useTabStore = defineStore('tab', {
tab.keyCode = keyCode tab.keyCode = keyCode
tab.size = size tab.size = size
tab.length = length tab.length = length
tab.value = value tab.value = undefined
tab.viewAs = viewAs
tab.decode = decode
} }
this._setActivatedIndex(tabIndex, true, subTab) this._setActivatedIndex(tabIndex, true, subTab)
// this.activatedTab = tab.name // this.activatedTab = tab.name
}, },
/** /**
* update ttl by tag * keep update value in tab
* @param {string} server
* @param {number} db
* @param {string} key
* @param {*} value
* @param {string} [viewAs]
* @param {string] [decode]
* @param {boolean} reset
* @param {boolean} [end] keep end status if not set
*/
updateValue({ server, db, key, value, viewAs, decode, reset, end }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
tab.viewAs = viewAs || tab.viewAs
tab.decode = decode || tab.decode
if (typeof end === 'boolean') {
tab.end = end
}
if (!reset && typeof value === 'object') {
if (value instanceof Array) {
tab.value = tab.value || []
tab.value.push(...value)
} else {
tab.value = assign(value, tab.value || {})
}
} else {
tab.value = value
}
},
/**
* update or insert value entries
* @param {string} server
* @param {number} db
* @param {string} key
* @param {string} type
* @param {string[]|Object.<string, number>|Object.<number, string>} entries
* @param {boolean} [prepend] for list only
* @param {boolean} [reset]
* @param {boolean} [nocheck] ignore conflict checking for hash/set/zset
*/
upsertValueEntries({ server, db, key, type, entries, prepend, reset, nocheck }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'list': // string[] | Object.<number, string>
if (entries instanceof Array) {
// append or prepend items
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
if (prepend === true) {
tab.value = [...entries, ...tab.value]
} else {
tab.value.push(...entries)
}
tab.length += size(entries)
}
} else {
// replace by index
tab.value = tab.value || []
for (const idx in entries) {
set(tab.value, idx, entries[idx])
}
}
break
case 'hash': // Object.<string, string>
if (reset === true) {
tab.value = {}
tab.length = 0
} else {
tab.value = tab.value || {}
}
for (const k in entries) {
if (nocheck !== true && !tab.value.hasOwnProperty(k)) {
tab.length += 1
}
tab.value[k] = entries[k]
}
break
case 'set': // string[] | Object.{string, string}
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
if (entries instanceof Array) {
// add items
for (const elem of entries) {
if (nocheck !== true && indexOf(tab.value, elem) === -1) {
tab.value.push(elem)
tab.length += 1
}
}
} else {
// replace items
for (const k in entries) {
const idx = indexOf(tab.value, k)
if (idx !== -1) {
tab.value[idx] = entries[k]
} else {
tab.value.push(entries[k])
tab.length += 1
}
}
}
}
break
case 'zset': // {value: string, score: number}
if (reset === true) {
tab.value = Object.entries(entries).map(([value, score]) => ({ value, score }))
} else {
tab.value = tab.value || []
for (const val in entries) {
if (nocheck !== true) {
const ent = find(tab.value, (e) => e.value === val)
if (ent != null) {
ent.score = entries[val]
} else {
tab.value.push({ value: val, score: entries[val] })
tab.length += 1
}
} else {
tab.value.push({ value: val, score: entries[val] })
tab.length += 1
}
}
}
break
case 'stream': // [{id: string, value: []any}]
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
tab.value = [...entries, ...tab.value]
}
break
}
},
/**
* remove value entries
* @param {string} server
* @param {number} db
* @param {string} key
* @param {string} type
* @param {string[] | number[]} entries
*/
removeValueEntries({ server, db, key, type, entries }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'list': // string[] | number[]
tab.value = tab.value || []
if (typeof entries[0] === 'number') {
// remove by index、
entries.sort((a, b) => b - a)
const removed = pullAt(tab.value, ...entries)
tab.length -= size(removed)
} else {
// append or prepend items
for (const elem of entries) {
if (!isEmpty(remove(tab.value, elem))) {
tab.length -= 1
}
}
}
break
case 'hash': // string[]
tab.value = tab.value || {}
for (const k of entries) {
if (tab.value.hasOwnProperty(k)) {
delete tab.value[k]
tab.length -= 1
}
}
break
case 'set': // []string
tab.value = tab.value || []
tab.length -= size(remove(tab.value, (v) => entries.indexOf(v) >= 0))
break
case 'zset': // string[]
tab.value = tab.value || []
tab.length -= size(remove(tab.value, (v) => entries.indexOf(v.value) >= 0))
break
case 'stream': // string[]
tab.value = tab.value || []
tab.length -= size(remove(tab.value, (v) => entries.indexOf(v.id) >= 0))
break
}
},
/**
* update loading status of content in tab
* @param {string} server
* @param {number} db
* @param {boolean} loading
*/
updateLoading({ server, db, loading }) {
const tab = find(this.tabList, { name: server, db })
if (tab == null) {
return
}
tab.loading = loading
},
/**
* update ttl in tab
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string|number[]} key

View File

@ -92,7 +92,8 @@ body {
.value-wrapper { .value-wrapper {
//border-top: v-bind('themeVars.borderColor') 1px solid; //border-top: v-bind('themeVars.borderColor') 1px solid;
user-select: text; user-select: text;
height: 100%; //height: 100%;
box-sizing: border-box;
} }
.value-item-part { .value-item-part {
@ -136,3 +137,7 @@ body {
.n-modal-mask { .n-modal-mask {
--wails-draggable: drag; --wails-draggable: drag;
} }
.n-tabs .n-tabs-nav {
line-height: 1.3;
}

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.21
require ( require (
github.com/adrg/sysfont v0.1.2 github.com/adrg/sysfont v0.1.2
github.com/andybalholm/brotli v1.0.6 github.com/andybalholm/brotli v1.0.6
github.com/google/uuid v1.3.1 github.com/google/uuid v1.4.0
github.com/klauspost/compress v1.17.2 github.com/klauspost/compress v1.17.2
github.com/redis/go-redis/v9 v9.2.1 github.com/redis/go-redis/v9 v9.3.0
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
github.com/wailsapp/wails/v2 v2.6.0 github.com/wailsapp/wails/v2 v2.6.0
golang.org/x/crypto v0.14.0 golang.org/x/crypto v0.14.0

8
go.sum
View File

@ -23,8 +23,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
@ -62,8 +62,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=