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
### Multi-language Contributions {#language}
### Multi-language Contributions
#### 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)
### Code Submission`(To be completed)` {#pull_request}
### Code Submission`(To be completed)`
#### Pull Request Title
The format of PR's title like "<type>: <description>"

View File

@ -1,6 +1,6 @@
## 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)文件进行改名。
@ -19,7 +19,7 @@
```
4. 检查应用中对应翻译语境无问题后,可提交审核([查看如何提交](#pull_request)
### 代码提交`(待完善)` {#pull_request}
### 代码提交`(待完善)`
#### PR提交规范
PR提交格式为“<type>: <description>

View File

@ -29,12 +29,22 @@ type slowLogItem struct {
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 {
client redis.UniversalClient
ctx context.Context
cancelFunc context.CancelFunc
cursor map[int]uint64 // current cursor of databases
stepSize int64
client redis.UniversalClient
ctx context.Context
cancelFunc context.CancelFunc
cursor map[int]uint64 // current cursor of databases
entryCursor map[int]entryCursor // current entry cursor of databases
stepSize int64
}
type browserService struct {
@ -261,11 +271,12 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
}
ctx, cancelFunc := context.WithCancel(b.ctx)
item = connectionItem{
client: client,
ctx: ctx,
cancelFunc: cancelFunc,
cursor: map[int]uint64{},
stepSize: int64(selConn.LoadSize),
client: client,
ctx: ctx,
cancelFunc: cancelFunc,
cursor: map[int]uint64{},
entryCursor: map[int]entryCursor{},
stepSize: int64(selConn.LoadSize),
}
if item.stepSize <= 0 {
item.stepSize = consts.DEFAULT_LOAD_SIZE
@ -479,14 +490,355 @@ func (b *browserService) LoadAllKeys(connName string, db int, match, keyType str
return
}
b.setClientCursor(connName, db, 0)
maxKeys := b.loadDBSize(ctx, client)
resp.Success = true
resp.Data = map[string]any{
"keys": keys,
"keys": keys,
"maxKeys": maxKeys,
}
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
func (b *browserService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) {
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)
var removedField []string
updatedField := map[string]string{}
replacedField := map[string]string{}
if len(field) <= 0 {
// old filed is empty, add new field
_, 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()
removedField = append(removedField, field)
updatedField[newField] = value
replacedField[field] = newField
}
if err != nil {
resp.Msg = err.Error()
@ -803,8 +1157,9 @@ func (b *browserService) SetHashValue(connName string, db int, k any, field, new
resp.Success = true
resp.Data = map[string]any{
"removed": removedField,
"updated": updatedField,
"removed": removedField,
"updated": updatedField,
"replaced": replacedField,
}
return
}
@ -944,10 +1299,11 @@ func (b *browserService) SetSetItem(connName string, db int, k any, remove bool,
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k)
var affected int64
if remove {
_, err = client.SRem(ctx, key, members...).Result()
affected, err = client.SRem(ctx, key, members...).Result()
} else {
_, err = client.SAdd(ctx, key, members...).Result()
affected, err = client.SAdd(ctx, key, members...).Result()
}
if err != nil {
resp.Msg = err.Error()
@ -955,6 +1311,9 @@ func (b *browserService) SetSetItem(connName string, db int, k any, remove bool,
}
resp.Success = true
resp.Data = map[string]any{
"affected": affected,
}
return
}
@ -1003,6 +1362,9 @@ func (b *browserService) UpdateZSetValue(connName string, db int, k any, value,
Score: score,
Member: value,
}).Result()
if err == nil {
updated[value] = score
}
} else {
// remove old value and add new one
_, 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
key := strutil.DecodeRedisKey(k)
_, err = client.XAdd(ctx, &redis.XAddArgs{
var updateID string
updateID, err = client.XAdd(ctx, &redis.XAddArgs{
Stream: key,
ID: ID,
Values: fieldItems,
@ -1086,6 +1449,9 @@ func (b *browserService) AddStreamValue(connName string, db int, k any, ID strin
}
resp.Success = true
resp.Data = map[string]any{
"updateID": updateID,
}
return
}
@ -1099,8 +1465,18 @@ func (b *browserService) RemoveStreamValues(connName string, db int, k any, IDs
client, ctx := item.client, item.ctx
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.Data = map[string]any{
"affected": affected,
}
return
}
@ -1146,23 +1522,25 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
if strings.HasSuffix(key, "*") {
// delete by prefix
var mutex sync.Mutex
supportUnlink := true
del := func(ctx context.Context, cli redis.UniversalClient) error {
handleDel := func(ks []string) error {
pipe := cli.Pipeline()
for _, k2 := range ks {
if async {
cli.Unlink(ctx, k2)
} else {
cli.Del(ctx, k2)
var delErr error
if async && supportUnlink {
supportUnlink = false
if delErr = cli.Unlink(ctx, ks...).Err(); delErr != nil {
// not support unlink? try del command
delErr = cli.Del(ctx, ks...).Err()
}
} else {
delErr = cli.Del(ctx, ks...).Err()
}
pipe.Exec(ctx)
mutex.Lock()
deletedKeys = append(deletedKeys, ks...)
mutex.Unlock()
return nil
return delErr
}
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)
for iter.Next(ctx) {
resultKeys = append(resultKeys, iter.Val())
if len(resultKeys) >= 3 {
if len(resultKeys) >= 20 {
handleDel(resultKeys)
resultKeys = resultKeys[:0:cap(resultKeys)]
}
@ -1198,12 +1576,14 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
} else {
// delete key only
if async {
if _, err = client.Unlink(ctx, key).Result(); err != nil {
resp.Msg = err.Error()
return
if err = client.Unlink(ctx, key).Err(); err != nil {
if err = client.Del(ctx, key).Err(); err != nil {
resp.Msg = err.Error()
return
}
}
} else {
if _, err = client.Del(ctx, key).Result(); err != nil {
if err = client.Del(ctx, key).Err(); err != nil {
resp.Msg = err.Error()
return
}
@ -1227,8 +1607,8 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
return
}
flush := func(ctx context.Context, cli redis.UniversalClient) {
cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
flush := func(ctx context.Context, cli redis.UniversalClient, async bool) error {
_, e := cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Select(ctx, db)
if async {
pipe.FlushDBAsync(ctx)
@ -1237,17 +1617,26 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
}
return nil
})
return e
}
client, ctx := item.client, item.ctx
if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
flush(ctx, cli)
return nil
return flush(ctx, cli, async)
})
// 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 {
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 {

View File

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

View File

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

View File

@ -1 +1 @@
24a0be173ef3e3939a15eb9c7fff5e1a
3a2923aff6e49c16c4ca2e7fc8811eeb

View File

@ -26,7 +26,7 @@ const props = defineProps({
const data = reactive({
navMenuWidth: 60,
toolbarHeight: 45,
toolbarHeight: 38,
})
const tabStore = useTabStore()
@ -135,7 +135,7 @@ onMounted(async () => {
paddingLeft: `${logoPaddingLeft}px`,
}">
<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>
<transition name="fade">
<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,
viewAs: tab.viewAs,
decode: tab.decode,
end: tab.end,
loading: tab.loading === true,
}
})
const isBlankValue = computed(() => {
return tabContent.value.value == null
return tabContent.value?.keyPath == null
})
const selectedSubTab = computed(() => {
@ -133,19 +135,7 @@ watch(
<span>{{ $t('interface.sub_tab.key_detail') }}</span>
</n-space>
</template>
<content-value-wrapper
: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" />
<content-value-wrapper :blank="isBlankValue" :content="tabContent" />
</n-tab-pane>
<!-- cli pane -->

View File

@ -1,9 +1,8 @@
<script setup>
import { h, onMounted, onUnmounted, reactive, ref } from '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 dayjs from 'dayjs'
import { useThemeVars } from 'naive-ui'
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 { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { computed } from 'vue'
import { isEmpty, padStart } from 'lodash'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
import { padStart } from 'lodash'
const props = defineProps({
server: String,
@ -34,32 +32,18 @@ const props = defineProps({
type: Number,
default: -1,
},
viewAs: {
type: String,
default: formatTypes.PLAIN_TEXT,
},
decode: {
type: String,
default: decodeTypes.NONE,
},
loading: Boolean,
})
const emit = defineEmits(['reload', 'rename', 'delete'])
const dialogStore = useDialog()
const browserStore = useBrowserStore()
const i18n = useI18n()
const binaryKey = computed(() => {
return !!props.keyCode
})
/**
*
* @type {ComputedRef<string|number[]>}
*/
const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
})
const ttlString = computed(() => {
let s = ''
if (props.ttl > 0) {
@ -77,10 +61,6 @@ const ttlString = computed(() => {
return s
})
const onReloadKey = () => {
browserStore.loadKeyValue(props.server, props.db, keyName.value, props.viewAs, props.decode)
}
const onCopyKey = () => {
ClipboardSetText(props.keyPath)
.then((succ) => {
@ -92,33 +72,20 @@ const onCopyKey = () => {
$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>
<template>
<div class="content-toolbar flex-box-h">
<n-input-group>
<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>
<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>
</n-input>
<icon-button :icon="Copy" border size="18" t-tooltip="interface.copy_key" @click="onCopyKey" />
@ -135,11 +102,11 @@ const onDeleteKey = () => {
</template>
TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }}
</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-tooltip :show-arrow="false">
<template #trigger>
<n-button :focusable="false" @click="onDeleteKey">
<n-button :focusable="false" @click="emit('delete')">
<template #icon>
<n-icon :component="Delete" size="18" />
</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 EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import useDialogStore from 'stores/dialog.js'
import { isEmpty } from 'lodash'
import { isEmpty, size } from 'lodash'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.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 themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -149,14 +156,7 @@ const actionColumn = {
row.key,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$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 {
$message.error(msg)
}
@ -175,14 +175,7 @@ const actionColumn = {
currentEditRow.value.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$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 {
$message.error(msg)
}
@ -223,6 +216,12 @@ const tableData = computed(() => {
}
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.HASH)
}
@ -262,20 +261,28 @@ const onUpdateFilter = (filters, sourceColumn) => {
break
}
}
defineExpose({
reset: () => {
clearFilter()
},
})
</script>
<template>
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
: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="flex-box-h">
<n-input-group>
@ -294,6 +301,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group>
</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">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -301,24 +324,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</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
:key="(row) => row.no"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@update:filters="onUpdateFilter" />
</div>
<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-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div>

View File

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

View File

@ -11,6 +11,9 @@ import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.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 themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -107,10 +114,7 @@ const actionColumn = {
row.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.value }))
// update display value
// props.value.splice(row.no - 1, 1)
} else {
$message.error(msg)
}
@ -128,10 +132,7 @@ const actionColumn = {
currentEditRow.value.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ'))
// update display value
// props.value[row.no - 1] = currentEditRow.value.value
} else {
$message.error(msg)
}
@ -173,6 +174,11 @@ const tableData = computed(() => {
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddValue = (value) => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET)
}
@ -189,20 +195,28 @@ const clearFilter = () => {
const onUpdateFilter = (filters, sourceColumn) => {
valueColumn.filterOptionValue = filters[sourceColumn.key]
}
defineExpose({
reset: () => {
clearFilter()
},
})
</script>
<template>
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
: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="flex-box-h">
<n-input
@ -213,6 +227,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
@update:value="onFilterInput" />
</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">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -220,24 +250,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</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
:key="(row) => row.no"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@update:filters="onUpdateFilter" />
</div>
<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-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<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 EditableTableColumn from '@/components/common/EditableTableColumn.vue'
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 { decodeTypes, formatTypes } from '@/consts/value_view_type.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 themeVars = useThemeVars()
@ -27,7 +30,10 @@ const props = defineProps({
type: Number,
default: -1,
},
value: Object,
value: {
type: Array,
default: () => [],
},
size: Number,
length: Number,
viewAs: {
@ -38,8 +44,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -115,14 +125,7 @@ const actionColumn = {
row.id,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$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 {
$message.error(msg)
}
@ -137,15 +140,22 @@ const columns = reactive([idColumn, valueColumn, actionColumn])
const tableData = computed(() => {
const data = []
for (const elem of props.value) {
data.push({
id: elem.id,
value: elem.value,
})
if (!isEmpty(props.value)) {
for (const elem of props.value) {
data.push({
id: elem.id,
value: elem.value,
})
}
}
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.STREAM)
}
@ -174,20 +184,28 @@ const onUpdateFilter = (filters, sourceColumn) => {
break
}
}
defineExpose({
reset: () => {
clearFilter()
},
})
</script>
<template>
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
: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="flex-box-h">
<n-input-group>
@ -206,6 +224,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group>
</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">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -213,17 +247,18 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</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
:key="(row) => row.id"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@ -231,7 +266,7 @@ const onUpdateFilter = (filters, sourceColumn) => {
</div>
<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-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div>

View File

@ -43,8 +43,11 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
loading: Boolean,
})
const emit = defineEmits(['reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -53,16 +56,6 @@ const keyName = computed(() => {
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 viewLanguage = computed(() => {
switch (props.viewAs) {
@ -74,11 +67,23 @@ const viewLanguage = computed(() => {
})
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) => {
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,
)
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'))
} else {
$message.error(msg)
@ -144,14 +149,16 @@ const onSaveValue = async () => {
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="keyCode"
:key-path="keyPath"
:key-type="keyType"
:loading="loading"
:server="props.name"
: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="flex-item-expand"></div>
<n-button-group v-if="!inEdit">

View File

@ -1,5 +1,4 @@
<script setup>
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { types as redisTypes } from '@/consts/support_redis_type.js'
import ContentValueString from '@/components/content_value/ContentValueString.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 { useThemeVars } from 'naive-ui'
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 browserStore = useBrowserStore()
const dialogStore = useDialogStore()
const props = defineProps({
blank: Boolean,
type: String,
name: String,
db: Number,
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,
content: {
type: Object,
default: {},
},
})
/**
*
* @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 = {
[redisTypes.STRING]: ContentValueString,
[redisTypes.HASH]: ContentValueHash,
@ -49,34 +59,108 @@ const valueComponents = {
[redisTypes.STREAM]: ContentValueStream,
}
/**
* reload current selection key
* @returns {Promise<null>}
*/
const onReloadKey = async () => {
await browserStore.loadKeyValue(props.name, props.db, props.key, props.viewAs)
const keyName = computed(() => {
return !isEmpty(data.value.keyCode) ? data.value.keyCode : data.value.keyPath
})
const loadData = async (reset, full) => {
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>
<template>
<n-empty v-if="props.blank" :description="$t('interface.nonexist_tab_content')" class="empty-content">
<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>
</n-empty>
<keep-alive v-else>
<component
:is="valueComponents[props.type]"
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:length="props.length"
:name="props.name"
:size="props.size"
:ttl="props.ttl"
:value="props.value"
:view-as="props.viewAs" />
:is="valueComponents[data.type]"
ref="contentRef"
:db="data.db"
:decode="data.decode || decodeTypes.NONE"
:end="data.end"
:key-code="data.keyCode"
:key-path="data.keyPath"
:length="data.length"
:loading="data.loading === true"
:name="data.name"
: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>
</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 { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import { isEmpty } from 'lodash'
import { isEmpty, size } from 'lodash'
import useDialogStore from 'stores/dialog.js'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.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 themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -178,7 +185,6 @@ const actionColumn = {
row.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.value }))
} else {
$message.error(msg)
@ -203,7 +209,6 @@ const actionColumn = {
currentEditRow.value.score,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ'))
} else {
$message.error(msg)
@ -235,17 +240,24 @@ const columns = reactive([
const tableData = computed(() => {
const data = []
let index = 0
for (const elem of props.value) {
data.push({
no: ++index,
value: elem.value,
score: elem.score,
})
if (!isEmpty(props.value)) {
let index = 0
for (const elem of props.value) {
data.push({
no: ++index,
value: elem.value,
score: elem.score,
})
}
}
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET)
}
@ -287,20 +299,28 @@ const onUpdateFilter = (filters, sourceColumn) => {
break
}
}
defineExpose({
reset: () => {
clearFilter()
},
})
</script>
<template>
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
: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="flex-box-h">
<n-input-group>
@ -324,6 +344,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group>
</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">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -331,24 +367,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</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
:key="(row) => row.no"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@update:filters="onUpdateFilter" />
</div>
<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-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<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 { isEmpty, size, slice } from 'lodash'
import useBrowserStore from 'stores/browser.js'
import useTabStore from 'stores/tab.js'
const i18n = useI18n()
const newForm = reactive({
@ -79,6 +80,7 @@ watch(
)
const browserStore = useBrowserStore()
const tab = useTabStore()
const onAdd = async () => {
try {
const { server, db, key, keyCode, type } = newForm
@ -87,6 +89,7 @@ const onAdd = async () => {
value = defaultValue[type]
}
const keyName = isEmpty(keyCode) ? key : keyCode
let updated = false
switch (type) {
case types.LIST:
{
@ -98,9 +101,7 @@ const onAdd = async () => {
}
const { success, msg } = data
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -112,9 +113,7 @@ const onAdd = async () => {
{
const { success, msg } = await browserStore.addHashField(server, db, keyName, newForm.opType, value)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -126,9 +125,7 @@ const onAdd = async () => {
{
const { success, msg } = await browserStore.addSetItem(server, db, keyName, value)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -140,9 +137,7 @@ const onAdd = async () => {
{
const { success, msg } = await browserStore.addZSetItem(server, db, keyName, newForm.opType, value)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -161,9 +156,7 @@ const onAdd = async () => {
slice(value, 1),
)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -172,6 +165,12 @@ const onAdd = async () => {
}
break
}
if (updated) {
if (newForm.reload) {
browserStore.reloadKey({ server, db, key: keyName })
}
}
dialogStore.closeAddFieldsDialog()
} catch (e) {
$message.error(e.message)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,6 +90,8 @@
"new_key": "Add Key",
"load_more": "Load More Keys",
"load_all": "Load All Left Keys",
"load_more_entries": "Load More",
"load_all_entries": "Load All",
"more_action": "More Action",
"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",

View File

@ -1,7 +1,9 @@
import en from './en-us'
import pt from './pt-br'
import zh from './zh-cn'
export const lang = {
en,
pt,
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": "添加新键",
"load_more": "加载更多键",
"load_all": "加载剩余所有键",
"load_more_entries": "加载更多",
"load_all_entries": "加载全部",
"more_action": "更多操作",
"nonexist_tab_content": "所选键不存在或未选中任何键,请尝试刷新重试",
"empty_server_content": "可以从左边选择并打开连接",

View File

@ -5,6 +5,7 @@ import {
get,
isEmpty,
join,
last,
remove,
set,
size,
@ -24,7 +25,8 @@ import {
DeleteKey,
FlushDB,
GetCmdHistory,
GetKeyValue,
GetKeyDetail,
GetKeySummary,
GetSlowLogs,
LoadAllKeys,
LoadNextKeys,
@ -58,7 +60,7 @@ const useBrowserStore = defineStore('browser', {
* @property {number} type
* @property {number} [db] - database index, type == ConnectionType.RedisDB only
* @property {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue only
* @property {string} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
* @property {number[]} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
* @property {number} [keys] - children key count
* @property {number} [maxKeys] - max key count for database
* @property {boolean} [isLeaf]
@ -137,7 +139,7 @@ const useBrowserStore = defineStore('browser', {
* get database by server name and index
* @param {string} connName
* @param {number} db
* @return {{}|null}
* @return {DatabaseItem|null}
*/
getDatabase(connName, db) {
const dbs = this.databases[connName]
@ -150,6 +152,20 @@ const useBrowserStore = defineStore('browser', {
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
* @param {string} connName
@ -344,20 +360,23 @@ const useBrowserStore = defineStore('browser', {
},
/**
* load redis key
* load key summary info
* @param {string} server
* @param {number} db
* @param {string|number[]} [key] when key is null or blank, update tab to display normal content (blank content or server status)
* @param {string} [viewType]
* @param {string} [decodeType]
* @param {string|number[]} [key] null or blank indicate that update tab to display normal content (blank content or server status)
* @return {Promise<void>}
*/
async loadKeyValue(server, db, key, viewType, decodeType) {
async loadKeySummary({ server, db, key }) {
try {
const tab = useTabStore()
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) {
const { type, ttl, value, size, length, viewAs, decode } = data
const { type, ttl, size, length } = data
const k = decodeRedisKey(key)
const binaryKey = k !== key
tab.upsertTab({
@ -368,16 +387,13 @@ const useBrowserStore = defineStore('browser', {
ttl,
keyCode: binaryKey ? key : undefined,
key: k,
value,
size,
length,
viewAs,
decode,
})
return
} else {
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
await this.deleteKey(server, db, key, true)
@ -397,18 +413,83 @@ const useBrowserStore = defineStore('browser', {
size: 0,
length: 0,
})
} catch (e) {
$message.error('')
} 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
* @param {string} connName
* @param {number} db
* @param {string} match
* @param {string} matchType
* @param {string} [matchType]
* @param {boolean} [full]
* @returns {Promise<{keys: string[], end: boolean}>}
* @returns {Promise<{keys: string[], maxKeys: number, end: boolean}>}
*/
async scanKeys(connName, db, match, matchType, full) {
let resp
@ -421,8 +502,8 @@ const useBrowserStore = defineStore('browser', {
if (!success) {
throw new Error(msg)
}
const { keys = [], end } = data
return { keys, end, success }
const { keys = [], maxKeys, end } = data
return { keys, end, maxKeys, success }
},
/**
@ -432,7 +513,7 @@ const useBrowserStore = defineStore('browser', {
* @param {string|null} prefix
* @param {string|null} matchType
* @param {boolean} [all]
* @return {Promise<{keys: Array<string|number[]>, end: boolean}>}
* @return {Promise<{keys: Array<string|number[]>, maxKeys: number, end: boolean}>}
* @private
*/
async _loadKeys(connName, db, prefix, matchType, all) {
@ -456,7 +537,8 @@ const useBrowserStore = defineStore('browser', {
*/
async loadMoreKeys(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
this._addKeyNodes(connName, db, keys)
this._tidyNode(connName, db, '')
@ -471,11 +553,44 @@ const useBrowserStore = defineStore('browser', {
*/
async loadAllKeys(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._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
* @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 {number} db
* @param {number} updateValue
* @param {number} [updateValue]
* @private
*/
_updateDBMaxKeys(connName, db, updateValue) {
if (updateValue === undefined) {
return
}
const database = this.getDatabase(connName, db)
if (database != null) {
const maxKeys = get(database, 'maxKeys', 0)
@ -764,15 +882,16 @@ const useBrowserStore = defineStore('browser', {
},
/**
* set db max keys to 0
* @param connName
* @param db
* set db max keys value
* @param {string} connName
* @param {number} db
* @param {number} maxKeys
* @private
*/
_emptyDBMaxKeys(connName, db) {
_setDBMaxKeys(connName, db, maxKeys) {
const database = this.getDatabase(connName, db)
if (database != null) {
set(database, 'maxKeys', 0)
set(database, 'maxKeys', maxKeys)
}
},
@ -857,7 +976,14 @@ const useBrowserStore = defineStore('browser', {
try {
const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '')
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 }
} else {
return { success, msg }
@ -881,6 +1007,8 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
if (success) {
const { updated = {} } = data
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'hash', entries: updated })
return { success, updated }
} else {
return { success: false, msg }
@ -903,6 +1031,10 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '')
if (success) {
const { removed = [] } = data
if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'hash', entries: removed })
}
return { success, removed }
} else {
return { success, msg }
@ -935,13 +1067,24 @@ const useBrowserStore = defineStore('browser', {
* @param db
* @param key
* @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: []>}
* @returns {Promise<{[msg]: string, success: boolean, [item]: []}>}
*/
async prependListItem(connName, db, key, values) {
try {
const { data, success, msg } = await AddListItem(connName, db, key, 0, values)
if (success) {
const { left = [] } = data
if (!isEmpty(left)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: right,
prepend: true,
})
}
return { success, item: left }
} else {
return { success: false, msg }
@ -957,13 +1100,24 @@ const useBrowserStore = defineStore('browser', {
* @param db
* @param key
* @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: any[]>}
* @returns {Promise<{[msg]: string, success: boolean, [item]: any[]}>}
*/
async appendListItem(connName, db, key, values) {
try {
const { data, success, msg } = await AddListItem(connName, db, key, 1, values)
if (success) {
const { right = [] } = data
if (!isEmpty(right)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: right,
prepend: false,
})
}
return { success, item: right }
} else {
return { success: false, msg }
@ -987,6 +1141,16 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetListItem(connName, db, key, index, value)
if (success) {
const { updated = {} } = data
if (!isEmpty(updated)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: updated,
})
}
return { success, updated }
} else {
return { success, msg }
@ -1009,6 +1173,16 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetListItem(connName, db, key, index, '')
if (success) {
const { removed = [] } = data
if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({
server: connName,
db,
key,
type: 'list',
entries: removed,
})
}
return { success, removed }
} else {
return { success, msg }
@ -1023,13 +1197,18 @@ const useBrowserStore = defineStore('browser', {
* @param {string} connName
* @param {number} db
* @param {string|number} key
* @param {string} value
* @param {string|string[]} value
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async addSetItem(connName, db, key, value) {
try {
const { success, msg } = await SetSetItem(connName, db, key, false, [value])
if (!value instanceof Array) {
value = [value]
}
const { data, success, msg } = await SetSetItem(connName, db, key, false, value)
if (success) {
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: value })
return { success }
} else {
return { success, msg }
@ -1052,6 +1231,8 @@ const useBrowserStore = defineStore('browser', {
try {
const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue)
if (success) {
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: { [value]: newValue } })
return { success: true }
} else {
return { success, msg }
@ -1073,6 +1254,8 @@ const useBrowserStore = defineStore('browser', {
try {
const { success, msg } = await SetSetItem(connName, db, key, true, [value])
if (success) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'set', entries: [value] })
return { success }
} else {
return { success, msg }
@ -1094,6 +1277,8 @@ const useBrowserStore = defineStore('browser', {
async addZSetItem(connName, db, key, action, vs) {
try {
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) {
return { success }
} else {
@ -1119,6 +1304,13 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score)
if (success) {
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 }
} else {
return { success, msg }
@ -1141,7 +1333,11 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0)
if (success) {
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 {
return { success, msg }
}
@ -1157,14 +1353,22 @@ const useBrowserStore = defineStore('browser', {
* @param {string|number[]} key
* @param {string} id
* @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) {
try {
const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values)
if (success) {
const { updated = {} } = data
return { success, updated }
const { updateID } = data
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'stream',
entries: [{ id: updateID, value: values }],
})
return { success }
} else {
return { success: false, msg }
}
@ -1179,7 +1383,7 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db
* @param {string|number[]} key
* @param {string[]|string} ids
* @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
* @returns {Promise<{[msg]: {}, success: boolean}>}
*/
async removeStreamValues(connName, db, key, ids) {
if (typeof ids === 'string') {
@ -1188,8 +1392,9 @@ const useBrowserStore = defineStore('browser', {
try {
const { data = {}, success, msg } = await RemoveStreamValues(connName, db, key, ids)
if (success) {
const { removed = [] } = data
return { success, removed }
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'stream', entries: ids })
return { success }
} else {
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
@ -1407,7 +1640,7 @@ const useBrowserStore = defineStore('browser', {
if (success === true) {
// update tree view data
this._deleteKeyNode(connName, db)
this._emptyDBMaxKeys(connName, db)
this._setDBMaxKeys(connName, db, 0)
// set tab content empty
const tab = useTabStore()
tab.emptyTab(connName)
@ -1424,15 +1657,14 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db
* @param {string} key
* @param {string} newKey
* @returns {Promise<{[msg]: string, success: boolean}>}
* @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: string}>}
*/
async renameKey(connName, db, key, newKey) {
const { success = false, msg } = await RenameKey(connName, db, key, newKey)
if (success) {
// delete old key and add new key struct
this._deleteKeyNode(connName, db, key)
this._addKeyNodes(connName, db, [newKey])
return { success: true }
this._renameKeyNode(connName, db, key, newKey)
return { success: true, nodeKey: `${connName}/db${db}#${ConnectionType.RedisValue}/${newKey}` }
} else {
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'
const useTabStore = defineStore('tab', {
@ -11,12 +11,18 @@ const useTabStore = defineStore('tab', {
* @property {string} [icon] tab icon
* @property {string[]} selectedKeys
* @property {string} [type] key type
* @property {Object|Array} [value] key value
* @property {*} [value] key value
* @property {string} [server] server name
* @property {int} [db] database index
* @property {string} [key] current key name
* @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
* @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
* @param {string} subTab
@ -94,10 +104,8 @@ const useTabStore = defineStore('tab', {
* @param {number} [size]
* @param {number} [length]
* @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 })
if (tabIndex === -1) {
this.tabList.push({
@ -112,9 +120,7 @@ const useTabStore = defineStore('tab', {
keyCode,
size,
length,
value,
viewAs,
decode,
value: undefined,
})
tabIndex = this.tabList.length - 1
} else {
@ -131,16 +137,239 @@ const useTabStore = defineStore('tab', {
tab.keyCode = keyCode
tab.size = size
tab.length = length
tab.value = value
tab.viewAs = viewAs
tab.decode = decode
tab.value = undefined
}
this._setActivatedIndex(tabIndex, true, subTab)
// 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 {number} db
* @param {string|number[]} key

View File

@ -92,7 +92,8 @@ body {
.value-wrapper {
//border-top: v-bind('themeVars.borderColor') 1px solid;
user-select: text;
height: 100%;
//height: 100%;
box-sizing: border-box;
}
.value-item-part {
@ -136,3 +137,7 @@ body {
.n-modal-mask {
--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 (
github.com/adrg/sysfont v0.1.2
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/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/wailsapp/wails/v2 v2.6.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/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/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
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/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
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.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=