Compare commits

..

No commits in common. "aafa0c5432acfd5b12d55be683ebd6729b9c17f4" and "45e9c49f265261afdd43826c8057183701e836a6" have entirely different histories.

25 changed files with 619 additions and 1154 deletions

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"net/url" "net/url"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -17,6 +16,7 @@ import (
"tinyrdm/backend/consts" "tinyrdm/backend/consts"
"tinyrdm/backend/types" "tinyrdm/backend/types"
"tinyrdm/backend/utils/coll" "tinyrdm/backend/utils/coll"
maputil "tinyrdm/backend/utils/map"
redis2 "tinyrdm/backend/utils/redis" redis2 "tinyrdm/backend/utils/redis"
sliceutil "tinyrdm/backend/utils/slice" sliceutil "tinyrdm/backend/utils/slice"
strutil "tinyrdm/backend/utils/string" strutil "tinyrdm/backend/utils/string"
@ -1081,7 +1081,7 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
return return
} }
// SetHashValue update hash field // SetHashValue set hash field
func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSResp) { func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB) item, err := b.getRedisClient(param.Server, param.DB)
if err != nil { if err != nil {
@ -1092,67 +1092,36 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key) key := strutil.DecodeRedisKey(param.Key)
str := strutil.DecodeRedisKey(param.Value) str := strutil.DecodeRedisKey(param.Value)
var saveStr, displayStr string var saveStr string
if saveStr, err = strutil.SaveAs(str, param.Format, param.Decode); err != nil { if saveStr, err = strutil.SaveAs(str, param.Format, param.Decode); err != nil {
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 { var removedField []string
displayStr, _, _ = strutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat) updatedField := map[string]any{}
} replacedField := map[string]any{}
var updated, added, removed []types.HashEntryItem if len(param.Field) <= 0 {
var replaced []types.HashReplaceItem // old filed is empty, add new field
var affect int64 _, err = client.HSet(ctx, key, param.NewField, saveStr).Result()
if len(param.NewField) <= 0 { updatedField[param.NewField] = saveStr
} else if len(param.NewField) <= 0 {
// new field is empty, delete old field // new field is empty, delete old field
_, err = client.HDel(ctx, key, param.Field).Result() _, err = client.HDel(ctx, key, param.Field).Result()
removed = append(removed, types.HashEntryItem{ removedField = append(removedField, param.Field)
Key: param.Field, } else if param.Field == param.NewField {
})
} else if len(param.Field) <= 0 || param.Field == param.NewField {
affect, err = client.HSet(ctx, key, param.NewField, saveStr).Result()
if affect <= 0 {
// update field value // update field value
updated = append(updated, types.HashEntryItem{ _, err = client.HSet(ctx, key, param.Field, saveStr).Result()
Key: param.NewField, updatedField[param.NewField] = saveStr
Value: saveStr,
DisplayValue: displayStr,
})
} else {
// add new field
added = append(added, types.HashEntryItem{
Key: param.NewField,
Value: saveStr,
DisplayValue: displayStr,
})
}
} else { } else {
// remove old field and add new field // remove old field and add new field
if _, err = client.HDel(ctx, key, param.Field).Result(); err != nil { if _, err = client.HDel(ctx, key, param.Field).Result(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
_, err = client.HSet(ctx, key, param.NewField, saveStr).Result()
affect, err = client.HSet(ctx, key, param.NewField, saveStr).Result() removedField = append(removedField, param.Field)
if affect <= 0 { updatedField[param.NewField] = saveStr
// no new filed added, just update exists item replacedField[param.Field] = param.NewField
removed = append(removed, types.HashEntryItem{
Key: param.Field,
})
updated = append(updated, types.HashEntryItem{
Key: param.NewField,
Value: saveStr,
DisplayValue: displayStr,
})
} else {
// add new field
replaced = append(replaced, types.HashReplaceItem{
Key: param.Field,
NewKey: param.NewField,
Value: saveStr,
DisplayValue: displayStr,
})
}
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -1160,16 +1129,10 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Added []types.HashEntryItem `json:"added,omitempty"` "removed": removedField,
Removed []types.HashEntryItem `json:"removed,omitempty"` "updated": updatedField,
Updated []types.HashEntryItem `json:"updated,omitempty"` "replaced": replacedField,
Replaced []types.HashReplaceItem `json:"replaced,omitempty"`
}{
Added: added,
Removed: removed,
Updated: updated,
Replaced: replaced,
} }
return return
} }
@ -1184,40 +1147,28 @@ func (b *browserService) AddHashField(connName string, db int, k any, action int
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var updated []types.HashEntryItem updated := map[string]any{}
var added []types.HashEntryItem
switch action { switch action {
case 1: case 1:
// ignore duplicated fields // ignore duplicated fields
for i := 0; i < len(fieldItems); i += 2 { for i := 0; i < len(fieldItems); i += 2 {
field, value := strutil.DecodeRedisKey(fieldItems[i]), strutil.DecodeRedisKey(fieldItems[i+1]) _, err = client.HSetNX(ctx, key, fieldItems[i].(string), fieldItems[i+1]).Result()
if succ, _ := client.HSetNX(ctx, key, field, value).Result(); succ { if err == nil {
added = append(added, types.HashEntryItem{ updated[fieldItems[i].(string)] = fieldItems[i+1]
Key: field,
Value: value,
DisplayValue: "", // TODO: convert to display value
})
} }
} }
default: default:
// overwrite duplicated fields // overwrite duplicated fields
total := len(fieldItems) total := len(fieldItems)
if total > 1 { if total > 1 {
_, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < total; i += 2 { for i := 0; i < total; i += 2 {
field, value := strutil.DecodeRedisKey(fieldItems[i]), strutil.DecodeRedisKey(fieldItems[i+1]) client.HSet(ctx, key, fieldItems[i], fieldItems[i+1])
if affect, _ := client.HSet(ctx, key, field, value).Result(); affect > 0 {
added = append(added, types.HashEntryItem{
Key: field,
Value: value,
DisplayValue: "", // TODO: convert to display value
})
} else {
updated = append(updated, types.HashEntryItem{
Key: field,
Value: value,
DisplayValue: "", // TODO: convert to display value
})
} }
return nil
})
for i := 0; i < total; i += 2 {
updated[fieldItems[i].(string)] = fieldItems[i+1]
} }
} }
} }
@ -1227,12 +1178,8 @@ func (b *browserService) AddHashField(connName string, db int, k any, action int
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Added []types.HashEntryItem `json:"added,omitempty"` "updated": updated,
Updated []types.HashEntryItem `json:"updated,omitempty"`
}{
Added: added,
Updated: updated,
} }
return return
} }
@ -1247,27 +1194,16 @@ func (b *browserService) AddListItem(connName string, db int, k any, action int,
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var leftPush, rightPush []types.ListEntryItem var leftPush, rightPush []any
switch action { switch action {
case 0: case 0:
// push to head // push to head
slices.Reverse(items)
_, err = client.LPush(ctx, key, items...).Result() _, err = client.LPush(ctx, key, items...).Result()
for i := len(items) - 1; i >= 0; i-- { leftPush = append(leftPush, items...)
leftPush = append(leftPush, types.ListEntryItem{
Value: items[i],
DisplayValue: "", // TODO: convert to display value
})
}
default: default:
// append to tail // append to tail
_, err = client.RPush(ctx, key, items...).Result() _, err = client.RPush(ctx, key, items...).Result()
for _, it := range items { rightPush = append(rightPush, items...)
rightPush = append(rightPush, types.ListEntryItem{
Value: it,
DisplayValue: "", // TODO: convert to display value
})
}
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -1275,12 +1211,9 @@ func (b *browserService) AddListItem(connName string, db int, k any, action int,
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Left []types.ListEntryItem `json:"left,omitempty"` "left": leftPush,
Right []types.ListEntryItem `json:"right,omitempty"` "right": rightPush,
}{
Left: leftPush,
Right: rightPush,
} }
return return
} }
@ -1296,7 +1229,8 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key) key := strutil.DecodeRedisKey(param.Key)
str := strutil.DecodeRedisKey(param.Value) str := strutil.DecodeRedisKey(param.Value)
var replaced, removed []types.ListReplaceItem var removed []int64
updated := map[int64]string{}
if len(str) <= 0 { if len(str) <= 0 {
// remove from list // remove from list
err = client.LSet(ctx, key, param.Index, "---VALUE_REMOVED_BY_TINY_RDM---").Err() err = client.LSet(ctx, key, param.Index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
@ -1310,9 +1244,7 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
removed = append(removed, types.ListReplaceItem{ removed = append(removed, param.Index)
Index: param.Index,
})
} else { } else {
// replace index value // replace index value
var saveStr string var saveStr string
@ -1325,24 +1257,13 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
var displayStr string updated[param.Index] = saveStr
if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 {
displayStr, _, _ = strutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat)
}
replaced = append(replaced, types.ListReplaceItem{
Index: param.Index,
Value: saveStr,
DisplayValue: displayStr,
})
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Removed []types.ListReplaceItem `json:"removed,omitempty"` "removed": removed,
Replaced []types.ListReplaceItem `json:"replaced,omitempty"` "updated": updated,
}{
Removed: removed,
Replaced: replaced,
} }
return return
} }
@ -1357,25 +1278,11 @@ func (b *browserService) SetSetItem(server string, db int, k any, remove bool, m
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var added, removed []types.SetEntryItem
var affected int64 var affected int64
if remove { if remove {
for _, member := range members { affected, err = client.SRem(ctx, key, members...).Result()
if affected, _ = client.SRem(ctx, key, member).Result(); affected > 0 {
removed = append(removed, types.SetEntryItem{
Value: member,
})
}
}
} else { } else {
for _, member := range members { affected, err = client.SAdd(ctx, key, members...).Result()
if affected, _ = client.SAdd(ctx, key, member).Result(); affected > 0 {
added = append(added, types.SetEntryItem{
Value: member,
DisplayValue: "", // TODO: convert to display value
})
}
}
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -1383,14 +1290,8 @@ func (b *browserService) SetSetItem(server string, db int, k any, remove bool, m
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Added []types.SetEntryItem `json:"added,omitempty"` "affected": affected,
Removed []types.SetEntryItem `json:"removed,omitempty"`
Affected int64 `json:"affected"`
}{
Added: added,
Removed: removed,
Affected: affected,
} }
return return
} }
@ -1405,15 +1306,9 @@ func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp types.JSRe
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key) key := strutil.DecodeRedisKey(param.Key)
var added, removed []types.SetEntryItem
var affect int64
// remove old value // remove old value
str := strutil.DecodeRedisKey(param.Value) str := strutil.DecodeRedisKey(param.Value)
if affect, _ = client.SRem(ctx, key, str).Result(); affect > 0 { _, _ = client.SRem(ctx, key, str).Result()
removed = append(removed, types.SetEntryItem{
Value: str,
})
}
// insert new value // insert new value
str = strutil.DecodeRedisKey(param.NewValue) str = strutil.DecodeRedisKey(param.NewValue)
@ -1422,29 +1317,15 @@ func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp types.JSRe
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
if affect, _ = client.SAdd(ctx, key, saveStr).Result(); affect > 0 { _, err = client.SAdd(ctx, key, saveStr).Result()
// add new item
var displayStr string
if len(param.RetDecode) > 0 && len(param.RetFormat) > 0 {
displayStr, _, _ = strutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat)
}
added = append(added, types.SetEntryItem{
Value: saveStr,
DisplayValue: displayStr,
})
}
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Added []types.SetEntryItem `json:"added,omitempty"` "added": saveStr,
Removed []types.SetEntryItem `json:"removed,omitempty"`
}{
Added: added,
Removed: removed,
} }
return return
} }
@ -1460,16 +1341,13 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key) key := strutil.DecodeRedisKey(param.Key)
val, newVal := strutil.DecodeRedisKey(param.Value), strutil.DecodeRedisKey(param.NewValue) val, newVal := strutil.DecodeRedisKey(param.Value), strutil.DecodeRedisKey(param.NewValue)
var added, updated, removed []types.ZSetEntryItem updated := map[string]float64{}
var replaced []types.ZSetReplaceItem var removed []string
var affect int64
if len(newVal) <= 0 { if len(newVal) <= 0 {
// no new value, delete value // no new value, delete value
if affect, err = client.ZRem(ctx, key, val).Result(); affect > 0 { _, err = client.ZRem(ctx, key, val).Result()
//removed = append(removed, val) if err == nil {
removed = append(removed, types.ZSetEntryItem{ removed = append(removed, val)
Value: val,
})
} }
} else { } else {
var saveVal string var saveVal string
@ -1479,57 +1357,27 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
} }
if saveVal == val { if saveVal == val {
affect, err = client.ZAdd(ctx, key, redis.Z{ // update score only
_, err = client.ZAdd(ctx, key, redis.Z{
Score: param.Score, Score: param.Score,
Member: saveVal, Member: saveVal,
}).Result() }).Result()
displayValue, _, _ := strutil.ConvertTo(val, param.RetDecode, param.RetFormat) if err == nil {
if affect > 0 { updated[saveVal] = param.Score
// add new item
added = append(added, types.ZSetEntryItem{
Score: param.Score,
Value: val,
DisplayValue: displayValue,
})
} else {
// update score only
updated = append(updated, types.ZSetEntryItem{
Score: param.Score,
Value: val,
DisplayValue: displayValue,
})
} }
} else { } else {
// remove old value and add new one // remove old value and add new one
_, err = client.ZRem(ctx, key, val).Result() _, err = client.ZRem(ctx, key, val).Result()
if err != nil { if err == nil {
resp.Msg = err.Error() removed = append(removed, val)
return
} }
affect, err = client.ZAdd(ctx, key, redis.Z{ _, err = client.ZAdd(ctx, key, redis.Z{
Score: param.Score, Score: param.Score,
Member: saveVal, Member: saveVal,
}).Result() }).Result()
displayValue, _, _ := strutil.ConvertTo(saveVal, param.RetDecode, param.RetFormat) if err == nil {
if affect <= 0 { updated[saveVal] = param.Score
// no new value added, just update exists item
removed = append(removed, types.ZSetEntryItem{
Value: val,
})
updated = append(updated, types.ZSetEntryItem{
Score: param.Score,
Value: saveVal,
DisplayValue: displayValue,
})
} else {
// add new field
replaced = append(replaced, types.ZSetReplaceItem{
Score: param.Score,
Value: val,
NewValue: saveVal,
DisplayValue: displayValue,
})
} }
} }
} }
@ -1539,16 +1387,9 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Added []types.ZSetEntryItem `json:"added,omitempty"` "updated": updated,
Updated []types.ZSetEntryItem `json:"updated,omitempty"` "removed": removed,
Replaced []types.ZSetReplaceItem `json:"replaced,omitempty"`
Removed []types.ZSetEntryItem `json:"removed,omitempty"`
}{
Added: added,
Updated: updated,
Replaced: replaced,
Removed: removed,
} }
return return
} }
@ -1563,37 +1404,20 @@ func (b *browserService) AddZSetValue(connName string, db int, k any, action int
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
members := maputil.ToSlice(valueScore, func(k string) redis.Z {
return redis.Z{
Score: valueScore[k],
Member: k,
}
})
var added, updated []types.ZSetEntryItem
switch action { switch action {
case 1: case 1:
// ignore duplicated fields // ignore duplicated fields
for m, s := range valueScore { _, err = client.ZAddNX(ctx, key, members...).Result()
if affect, _ := client.ZAddNX(ctx, key, redis.Z{Score: s, Member: m}).Result(); affect > 0 {
added = append(added, types.ZSetEntryItem{
Score: s,
Value: m,
DisplayValue: "", // TODO: convert to display value
})
}
}
default: default:
// overwrite duplicated fields // overwrite duplicated fields
for m, s := range valueScore { _, err = client.ZAdd(ctx, key, members...).Result()
if affect, _ := client.ZAdd(ctx, key, redis.Z{Score: s, Member: m}).Result(); affect > 0 {
added = append(added, types.ZSetEntryItem{
Score: s,
Value: m,
DisplayValue: "", // TODO: convert to display value
})
} else {
updated = append(updated, types.ZSetEntryItem{
Score: s,
Value: m,
DisplayValue: "", // TODO: convert to display value
})
}
}
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -1601,13 +1425,6 @@ func (b *browserService) AddZSetValue(connName string, db int, k any, action int
} }
resp.Success = true resp.Success = true
resp.Data = struct {
Added []types.ZSetEntryItem `json:"added,omitempty"`
Updated []types.ZSetEntryItem `json:"updated,omitempty"`
}{
Added: added,
Updated: updated,
}
return return
} }
@ -1632,24 +1449,9 @@ func (b *browserService) AddStreamValue(connName string, db int, k any, ID strin
return return
} }
updateValues := make(map[string]any, len(fieldItems)/2)
for i := 0; i < len(fieldItems)/2; i += 2 {
updateValues[fieldItems[i].(string)] = fieldItems[i+1]
}
vb, _ := json.Marshal(updateValues)
displayValue, _, _ := strutil.ConvertTo(string(vb), types.DECODE_NONE, types.FORMAT_JSON)
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Added []types.StreamEntryItem `json:"added,omitempty"` "updateID": updateID,
}{
Added: []types.StreamEntryItem{
{
ID: updateID,
Value: updateValues,
DisplayValue: displayValue, // TODO: convert to display value
},
},
} }
return return
} }
@ -1673,10 +1475,8 @@ func (b *browserService) RemoveStreamValues(connName string, db int, k any, IDs
} }
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = map[string]any{
Affected int64 `json:"affected"` "affected": affected,
}{
Affected: affected,
} }
return return
} }

View File

@ -57,8 +57,6 @@ type SetListParam struct {
Value any `json:"value"` Value any `json:"value"`
Format string `json:"format,omitempty"` Format string `json:"format,omitempty"`
Decode string `json:"decode,omitempty"` Decode string `json:"decode,omitempty"`
RetFormat string `json:"retFormat,omitempty"`
RetDecode string `json:"retDecode,omitempty"`
} }
type SetHashParam struct { type SetHashParam struct {
@ -70,8 +68,6 @@ type SetHashParam struct {
Value any `json:"value"` Value any `json:"value"`
Format string `json:"format,omitempty"` Format string `json:"format,omitempty"`
Decode string `json:"decode,omitempty"` Decode string `json:"decode,omitempty"`
RetFormat string `json:"retFormat,omitempty"`
RetDecode string `json:"retDecode,omitempty"`
} }
type SetSetParam struct { type SetSetParam struct {
@ -82,8 +78,6 @@ type SetSetParam struct {
NewValue any `json:"newValue"` NewValue any `json:"newValue"`
Format string `json:"format,omitempty"` Format string `json:"format,omitempty"`
Decode string `json:"decode,omitempty"` Decode string `json:"decode,omitempty"`
RetFormat string `json:"retFormat,omitempty"`
RetDecode string `json:"retDecode,omitempty"`
} }
type SetZSetParam struct { type SetZSetParam struct {
@ -95,6 +89,4 @@ type SetZSetParam struct {
Score float64 `json:"score"` Score float64 `json:"score"`
Format string `json:"format,omitempty"` Format string `json:"format,omitempty"`
Decode string `json:"decode,omitempty"` Decode string `json:"decode,omitempty"`
RetFormat string `json:"retFormat,omitempty"`
RetDecode string `json:"retDecode,omitempty"`
} }

View File

@ -5,25 +5,12 @@ type ListEntryItem struct {
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
} }
type ListReplaceItem struct {
Index int64 `json:"index"`
Value any `json:"v,omitempty"`
DisplayValue string `json:"dv,omitempty"`
}
type HashEntryItem struct { type HashEntryItem struct {
Key string `json:"k"` Key string `json:"k"`
Value any `json:"v"` Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
} }
type HashReplaceItem struct {
Key any `json:"k"`
NewKey any `json:"nk"`
Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"`
}
type SetEntryItem struct { type SetEntryItem struct {
Value any `json:"v"` Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
@ -35,13 +22,6 @@ type ZSetEntryItem struct {
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
} }
type ZSetReplaceItem struct {
Score float64 `json:"s"`
Value string `json:"v"`
NewValue string `json:"nv"`
DisplayValue string `json:"dv,omitempty"`
}
type StreamEntryItem struct { type StreamEntryItem struct {
ID string `json:"id"` ID string `json:"id"`
Value map[string]any `json:"v"` Value map[string]any `json:"v"`

View File

@ -5,7 +5,6 @@ import (
) )
func containsBinary(str string) bool { func containsBinary(str string) bool {
//buf := []byte(str)
//size := 0 //size := 0
//for start := 0; start < len(buf); start += size { //for start := 0; start < len(buf); start += size {
// var r rune // var r rune
@ -15,25 +14,9 @@ func containsBinary(str string) bool {
//} //}
rs := []rune(str) rs := []rune(str)
for _, r := range rs { for _, r := range rs {
if !unicode.IsPrint(r) && !unicode.IsSpace(r) { if !unicode.IsPrint(r) && r != '\n' {
return true return true
} }
} }
return false return false
} }
func isSameChar(str string) bool {
if len(str) <= 0 {
return false
}
rs := []rune(str)
first := rs[0]
for _, r := range rs {
if r != first {
return false
}
}
return true
}

View File

@ -109,12 +109,10 @@ func autoDecode(str string) (value, resultDecode string) {
// pure digit content may incorrect regard as some encoded type, skip decode // pure digit content may incorrect regard as some encoded type, skip decode
if match, _ := regexp.MatchString(`^\d+$`, str); !match { if match, _ := regexp.MatchString(`^\d+$`, str); !match {
var ok bool var ok bool
if len(str)%4 == 0 && len(str) >= 12 && !isSameChar(str) {
if value, ok = decodeBase64(str); ok { if value, ok = decodeBase64(str); ok {
resultDecode = types.DECODE_BASE64 resultDecode = types.DECODE_BASE64
return return
} }
}
if value, ok = decodeGZip(str); ok { if value, ok = decodeGZip(str); ok {
resultDecode = types.DECODE_GZIP resultDecode = types.DECODE_GZIP

View File

@ -34,101 +34,39 @@ const filterServerOption = computed(() => {
const tableRef = ref(null) const tableRef = ref(null)
const columns = computed(() => [ const loadHistory = () => {
{
title: i18n.t('log.exec_time'),
key: 'timestamp',
defaultSortOrder: 'ascend',
sorter: 'default',
width: 180,
align: 'center',
titleAlign: 'center',
render: ({ timestamp }, index) => {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
},
},
{
title: i18n.t('log.server'),
key: 'server',
filterOptionValue: data.server,
filter: (value, row) => {
return value === '' || row.server === value.toString()
},
width: 150,
align: 'center',
titleAlign: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: i18n.t('log.cmd'),
key: 'cmd',
titleAlign: 'center',
filterOptionValue: data.keyword,
resizable: true,
filter: (value, row) => {
return value === '' || !!~row.cmd.indexOf(value.toString())
},
render: ({ cmd }, index) => {
const cmdList = split(cmd, '\n')
if (size(cmdList) > 1) {
return h(
'div',
null,
map(cmdList, (c) => h('div', null, c)),
)
}
return cmd
},
},
{
title: i18n.t('log.cost_time'),
key: 'cost',
width: 100,
align: 'center',
titleAlign: 'center',
render: ({ cost }, index) => {
const ms = dayjs.duration(cost).asMilliseconds()
if (ms < 1000) {
return `${ms} ms`
} else {
return `${Math.floor(ms / 1000)} s`
}
},
},
])
const loadHistory = async () => {
try {
await nextTick()
data.loading = true data.loading = true
const list = await browserStore.getCmdHistory() browserStore
.getCmdHistory()
.then((list) => {
data.history = list || [] data.history = list || []
} finally { })
.finally(() => {
data.loading = false data.loading = false
tableRef.value?.scrollTo({ top: 999999 }) tableRef.value?.scrollTo({ top: 999999 })
} })
} }
const cleanHistory = async () => { const cleanHistory = async () => {
$dialog.warning(i18n.t('log.confirm_clean_log'), async () => { $dialog.warning(i18n.t('log.confirm_clean_log'), () => {
try {
data.loading = true data.loading = true
const success = await browserStore.cleanCmdHistory() browserStore
.cleanCmdHistory()
.then((success) => {
if (success) { if (success) {
data.history = [] data.history = []
tableRef.value?.scrollTo({ top: 0 }) tableRef.value?.scrollTo({ top: 0 })
$message.success(i18n.t('common.success')) $message.success(i18n.t('common.success'))
} }
} finally { })
.finally(() => {
data.loading = false data.loading = false
} })
}) })
} }
defineExpose({ defineExpose({
refresh: loadHistory, refresh: () => nextTick().then(loadHistory),
}) })
</script> </script>
@ -160,12 +98,71 @@ defineExpose({
<div class="content-value fill-height flex-box-h"> <div class="content-value fill-height flex-box-h">
<n-data-table <n-data-table
ref="tableRef" ref="tableRef"
:columns="columns" :columns="[
{
title: $t('log.exec_time'),
key: 'timestamp',
defaultSortOrder: 'ascend',
sorter: 'default',
width: 180,
align: 'center',
titleAlign: 'center',
render: ({ timestamp }, index) => {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
},
},
{
title: $t('log.server'),
key: 'server',
filterOptionValue: data.server,
filter: (value, row) => {
return value === '' || row.server === value.toString()
},
width: 150,
align: 'center',
titleAlign: 'center',
ellipsis: true,
},
{
title: $t('log.cmd'),
key: 'cmd',
titleAlign: 'center',
filterOptionValue: data.keyword,
resizable: true,
filter: (value, row) => {
return value === '' || !!~row.cmd.indexOf(value.toString())
},
render: ({ cmd }, index) => {
const cmdList = split(cmd, '\n')
if (size(cmdList) > 1) {
return h(
'div',
null,
map(cmdList, (c) => h('div', null, c)),
)
}
return cmd
},
},
{
title: $t('log.cost_time'),
key: 'cost',
width: 100,
align: 'center',
titleAlign: 'center',
render: ({ cost }, index) => {
const ms = dayjs.duration(cost).asMilliseconds()
if (ms < 1000) {
return `${ms} ms`
} else {
return `${Math.floor(ms / 1000)} s`
}
},
},
]"
:data="data.history" :data="data.history"
:loading="data.loading"
class="flex-item-expand" class="flex-item-expand"
flex-height flex-height />
virtual-scroll />
</div> </div>
</n-card> </n-card>
</template> </template>

View File

@ -151,7 +151,6 @@ const onSave = () => {
<div class="editor-content-item-label">{{ props.fieldLabel }}</div> <div class="editor-content-item-label">{{ props.fieldLabel }}</div>
<n-input <n-input
v-model:value="viewAs.field" v-model:value="viewAs.field"
:placeholder="props.field + ''"
:readonly="props.fieldReadonly" :readonly="props.fieldReadonly"
class="editor-content-item-input" class="editor-content-item-input"
type="text" /> type="text" />
@ -161,12 +160,11 @@ const onSave = () => {
<div class="editor-content-item flex-box-v flex-item-expand"> <div class="editor-content-item flex-box-v flex-item-expand">
<div class="editor-content-item-label">{{ props.valueLabel }}</div> <div class="editor-content-item-label">{{ props.valueLabel }}</div>
<n-input <n-input
:placeholder="props.value"
:resizable="false"
:value="displayValue" :value="displayValue"
autofocus autofocus
class="flex-item-expand" class="flex-item-expand"
type="textarea" type="textarea"
:resizable="false"
@update:value="onUpdateValue" /> @update:value="onUpdateValue" />
<format-selector <format-selector
:decode="viewAs.decode" :decode="viewAs.decode"
@ -218,18 +216,11 @@ const onSave = () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.entry-editor { .entry-editor {
padding-left: 2px; padding-left: 2px;
box-sizing: border-box;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 100;
.editor-content { .editor-content {
&-item { &-item {
&:not(:last-child) { &:not(:last-child) {
margin-bottom: 16px; margin-bottom: 18px;
} }
&-label { &-label {

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue' import { h, onMounted, onUnmounted, reactive, ref } from 'vue'
import Refresh from '@/components/icons/Refresh.vue' import Refresh from '@/components/icons/Refresh.vue'
import { debounce, isEmpty, map, size, split } from 'lodash' import { debounce, isEmpty, map, size, split } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -33,85 +33,6 @@ const data = reactive({
const tableRef = ref(null) const tableRef = ref(null)
const columns = computed(() => [
{
title: i18n.t('slog.exec_time'),
key: 'timestamp',
sortOrder: data.sortOrder,
sorter: 'default',
width: 180,
align: 'center',
titleAlign: 'center',
render: ({ timestamp }, index) => {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
},
},
{
title: i18n.t('slog.client'),
key: 'client',
filterOptionValue: data.client,
resizable: true,
filter: (value, row) => {
return value === '' || row.client === value.toString() || row.addr === value.toString()
},
width: 200,
align: 'center',
titleAlign: 'center',
ellipsis: {
tooltip: true,
},
render: ({ client, addr }, index) => {
let content = ''
if (!isEmpty(client)) {
content += client
}
if (!isEmpty(addr)) {
if (!isEmpty(content)) {
content += ' - '
}
content += addr
}
return content
},
},
{
title: i18n.t('slog.cmd'),
key: 'cmd',
titleAlign: 'center',
filterOptionValue: data.keyword,
resizable: true,
filter: (value, row) => {
return value === '' || !!~row.cmd.indexOf(value.toString())
},
render: ({ cmd }, index) => {
const cmdList = split(cmd, '\n')
if (size(cmdList) > 1) {
return h(
'div',
null,
map(cmdList, (c) => h('div', null, c)),
)
}
return cmd
},
},
{
title: i18n.t('slog.cost_time'),
key: 'cost',
width: 100,
align: 'center',
titleAlign: 'center',
render: ({ cost }, index) => {
const ms = dayjs.duration(cost).asMilliseconds()
if (ms < 1000) {
return `${ms} ms`
} else {
return `${Math.floor(ms / 1000)} s`
}
},
},
])
const _loadSlowLog = () => { const _loadSlowLog = () => {
data.loading = true data.loading = true
browserStore browserStore
@ -182,12 +103,86 @@ const onListLimitChanged = (limit) => {
<div class="content-value fill-height flex-box-h"> <div class="content-value fill-height flex-box-h">
<n-data-table <n-data-table
ref="tableRef" ref="tableRef"
:columns="columns" :columns="[
{
title: $t('slog.exec_time'),
key: 'timestamp',
sortOrder: data.sortOrder,
sorter: 'default',
width: 180,
align: 'center',
titleAlign: 'center',
render({ timestamp }, index) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
},
},
{
title: $t('slog.client'),
key: 'client',
filterOptionValue: data.client,
resizable: true,
filter(value, row) {
return value === '' || row.client === value.toString() || row.addr === value.toString()
},
width: 200,
align: 'center',
titleAlign: 'center',
ellipsis: true,
render({ client, addr }, index) {
let content = ''
if (!isEmpty(client)) {
content += client
}
if (!isEmpty(addr)) {
if (!isEmpty(content)) {
content += ' - '
}
content += addr
}
return content
},
},
{
title: $t('slog.cmd'),
key: 'cmd',
titleAlign: 'center',
filterOptionValue: data.keyword,
resizable: true,
width: 100,
filter(value, row) {
return value === '' || !!~row.cmd.indexOf(value.toString())
},
render({ cmd }, index) {
const cmdList = split(cmd, '\n')
if (size(cmdList) > 1) {
return h(
'div',
null,
map(cmdList, (c) => h('div', null, c)),
)
}
return cmd
},
},
{
title: $t('slog.cost_time'),
key: 'cost',
width: 100,
align: 'center',
titleAlign: 'center',
render({ cost }, index) {
const ms = dayjs.duration(cost).asMilliseconds()
if (ms < 1000) {
return `${ms} ms`
} else {
return `${Math.floor(ms / 1000)} s`
}
},
},
]"
:data="data.list" :data="data.list"
:loading="data.loading"
class="flex-item-expand" class="flex-item-expand"
flex-height flex-height
virtual-scroll
@update:sorter="({ order }) => (data.sortOrder = order)" /> @update:sorter="({ order }) => (data.sortOrder = order)" />
</div> </div>
</n-card> </n-card>
@ -195,4 +190,9 @@ const onListLimitChanged = (limit) => {
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/content'; @import '@/styles/content';
.content-container {
padding: 5px;
box-sizing: border-box;
}
</style> </style>

View File

@ -89,6 +89,9 @@ const inEdit = computed(() => {
return currentEditRow.no > 0 return currentEditRow.no > 0
}) })
const fullEdit = ref(false) const fullEdit = ref(false)
const inFullEdit = computed(() => {
return inEdit.value && fullEdit.value
})
const tableRef = ref(null) const tableRef = ref(null)
const fieldFilterOption = ref(null) const fieldFilterOption = ref(null)
@ -155,11 +158,7 @@ const saveEdit = async (field, value, decode, format) => {
throw new Error('row not exists') throw new Error('row not exists')
} }
if (isEmpty(field)) { const { updated, success, msg } = await browserStore.setHash({
field = currentEditRow.key
}
const { success, msg } = await browserStore.setHash({
server: props.name, server: props.name,
db: props.db, db: props.db,
key: keyName.value, key: keyName.value,
@ -168,11 +167,16 @@ const saveEdit = async (field, value, decode, format) => {
value, value,
decode, decode,
format, format,
retDecode: props.decode,
retFormat: props.format,
index: [currentEditRow.no - 1],
}) })
if (success) { if (success) {
row.k = field
row.v = updated[row.k] || ''
const { value: displayVal } = await browserStore.convertValue({
value: row.v,
decode: props.decode,
format: props.format,
})
row.dv = displayVal
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -334,6 +338,7 @@ defineExpose({
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
v-show="!inFullEdit"
:db="props.db" :db="props.db"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
@ -345,7 +350,7 @@ defineExpose({
@delete="emit('delete')" @delete="emit('delete')"
@reload="emit('reload')" @reload="emit('reload')"
@rename="emit('rename')" /> @rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div v-show="!inFullEdit" class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input-group> <n-input-group>
<n-select <n-select
@ -379,6 +384,7 @@ defineExpose({
t-tooltip="interface.load_all_entries" t-tooltip="interface.load_all_entries"
@click="emit('loadall')" /> @click="emit('loadall')" />
</n-button-group> </n-button-group>
{{ valueColumn.align }}
<n-button :focusable="false" plain @click="onAddRow"> <n-button :focusable="false" plain @click="onAddRow">
<template #icon> <template #icon>
<n-icon :component="AddLink" size="18" /> <n-icon :component="AddLink" size="18" />
@ -390,12 +396,13 @@ defineExpose({
<!-- table --> <!-- table -->
<n-data-table <n-data-table
ref="tableRef" ref="tableRef"
v-show="!inFullEdit"
:row-key="(row) => row.k"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="props.value" :data="props.value"
:loading="props.loading" :loading="props.loading"
:row-key="(row) => row.k"
:row-props="rowProps" :row-props="rowProps"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
@ -407,12 +414,8 @@ defineExpose({
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
<!-- edit pane --> <!-- edit pane -->
<div
v-show="inEdit"
:style="{ position: fullEdit ? 'static' : 'relative' }"
class="entry-editor-container flex-item-expand"
style="width: 100%">
<content-entry-editor <content-entry-editor
v-show="inEdit"
v-model:fullscreen="fullEdit" v-model:fullscreen="fullEdit"
:decode="currentEditRow.decode" :decode="currentEditRow.decode"
:field="currentEditRow.key" :field="currentEditRow.key"
@ -425,7 +428,6 @@ defineExpose({
@close="resetEdit" @close="resetEdit"
@save="saveEdit" /> @save="saveEdit" />
</div> </div>
</div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />

View File

@ -75,6 +75,9 @@ const inEdit = computed(() => {
return currentEditRow.no > 0 return currentEditRow.no > 0
}) })
const fullEdit = ref(false) const fullEdit = ref(false)
const inFullEdit = computed(() => {
return inEdit.value && fullEdit.value
})
const displayCode = computed(() => { const displayCode = computed(() => {
return props.format === formatTypes.JSON return props.format === formatTypes.JSON
@ -127,11 +130,7 @@ const saveEdit = async (pos, value, decode, format) => {
throw new Error('row not exists') throw new Error('row not exists')
} }
if (isEmpty(value)) { const { updated, success, msg } = await browserStore.updateListItem({
value = currentEditRow.value
}
const { success, msg } = await browserStore.updateListItem({
server: props.name, server: props.name,
db: props.db, db: props.db,
key: keyName.value, key: keyName.value,
@ -141,6 +140,13 @@ const saveEdit = async (pos, value, decode, format) => {
format, format,
}) })
if (success) { if (success) {
row.v = updated[index] || ''
const { value: displayVal } = await browserStore.convertValue({
value: row.v,
decode: props.decode,
format: props.format,
})
row.dv = displayVal
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -277,6 +283,7 @@ defineExpose({
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
v-show="!inFullEdit"
:db="props.db" :db="props.db"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
@ -288,7 +295,7 @@ defineExpose({
@delete="emit('delete')" @delete="emit('delete')"
@reload="emit('reload')" @reload="emit('reload')"
@rename="emit('rename')" /> @rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div v-show="!inFullEdit" class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input <n-input
v-model:value="filterValue" v-model:value="filterValue"
@ -324,12 +331,13 @@ defineExpose({
<div class="value-wrapper value-item-part flex-box-h flex-item-expand"> <div class="value-wrapper value-item-part flex-box-h flex-item-expand">
<!-- table --> <!-- table -->
<n-data-table <n-data-table
v-show="!inFullEdit"
:row-key="(row) => row.no"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="props.value" :data="props.value"
:loading="props.loading" :loading="props.loading"
:row-key="(row) => row.no"
:row-props="rowProps" :row-props="rowProps"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
@ -341,12 +349,8 @@ defineExpose({
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
<!-- edit pane --> <!-- edit pane -->
<div
v-show="inEdit"
:style="{ position: fullEdit ? 'static' : 'relative' }"
class="entry-editor-container flex-item-expand"
style="width: 100%">
<content-entry-editor <content-entry-editor
v-show="inEdit"
v-model:fullscreen="fullEdit" v-model:fullscreen="fullEdit"
:decode="currentEditRow.decode" :decode="currentEditRow.decode"
:field="currentEditRow.no" :field="currentEditRow.no"
@ -360,7 +364,6 @@ defineExpose({
@close="resetEdit" @close="resetEdit"
@save="saveEdit" /> @save="saveEdit" />
</div> </div>
</div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />

View File

@ -74,6 +74,9 @@ const inEdit = computed(() => {
return currentEditRow.no > 0 return currentEditRow.no > 0
}) })
const fullEdit = ref(false) const fullEdit = ref(false)
const inFullEdit = computed(() => {
return inEdit.value && fullEdit.value
})
const displayCode = computed(() => { const displayCode = computed(() => {
return props.format === formatTypes.JSON return props.format === formatTypes.JSON
@ -126,7 +129,7 @@ const saveEdit = async (pos, value, decode, format) => {
throw new Error('row not exists') throw new Error('row not exists')
} }
const { success, msg } = await browserStore.updateSetItem({ const { added, success, msg } = await browserStore.updateSetItem({
server: props.name, server: props.name,
db: props.db, db: props.db,
key: keyName.value, key: keyName.value,
@ -134,10 +137,15 @@ const saveEdit = async (pos, value, decode, format) => {
newValue: value, newValue: value,
decode, decode,
format, format,
retDecode: props.decode,
retFormat: props.format,
}) })
if (success) { if (success) {
row.v = added
const { value: displayVal } = await browserStore.convertValue({
value: row.v,
decode: props.decode,
format: props.format,
})
row.dv = displayVal
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -175,7 +183,7 @@ const actionColumn = {
row.v, row.v,
) )
if (success) { if (success) {
// props.value.splice(index, 1) props.value.splice(index, 1)
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.v })) $message.success(i18n.t('dialogue.delete_key_succ', { key: row.v }))
} else { } else {
$message.error(msg) $message.error(msg)
@ -274,6 +282,7 @@ defineExpose({
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
v-show="!inFullEdit"
:db="props.db" :db="props.db"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
@ -285,7 +294,7 @@ defineExpose({
@delete="emit('delete')" @delete="emit('delete')"
@reload="emit('reload')" @reload="emit('reload')"
@rename="emit('rename')" /> @rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div v-show="!inFullEdit" class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input <n-input
v-model:value="filterValue" v-model:value="filterValue"
@ -321,12 +330,13 @@ defineExpose({
<div class="value-wrapper value-item-part flex-box-h flex-item-expand"> <div class="value-wrapper value-item-part flex-box-h flex-item-expand">
<!-- table --> <!-- table -->
<n-data-table <n-data-table
v-show="!inFullEdit"
:row-key="(row) => row.v"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="props.value" :data="props.value"
:loading="props.loading" :loading="props.loading"
:row-key="(row) => row.v"
:row-props="rowProps" :row-props="rowProps"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
@ -338,12 +348,8 @@ defineExpose({
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
<!-- edit pane --> <!-- edit pane -->
<div
v-show="inEdit"
:style="{ position: fullEdit ? 'static' : 'relative' }"
class="entry-editor-container flex-item-expand"
style="width: 100%">
<content-entry-editor <content-entry-editor
v-show="inEdit"
v-model:fullscreen="fullEdit" v-model:fullscreen="fullEdit"
:decode="currentEditRow.decode" :decode="currentEditRow.decode"
:field="currentEditRow.no" :field="currentEditRow.no"
@ -357,7 +363,6 @@ defineExpose({
@close="resetEdit" @close="resetEdit"
@save="saveEdit" /> @save="saveEdit" />
</div> </div>
</div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />

View File

@ -237,12 +237,12 @@ defineExpose({
</div> </div>
<div class="value-wrapper value-item-part flex-box-v flex-item-expand"> <div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table <n-data-table
:row-key="(row) => row.id"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="props.value" :data="props.value"
:loading="props.loading" :loading="props.loading"
:row-key="(row) => row.id"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
class="flex-item-expand" class="flex-item-expand"

View File

@ -6,7 +6,7 @@ import AddLink from '@/components/icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui' import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js' import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue' import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import { isEmpty, size } from 'lodash' import { head, isEmpty, keys, size } from 'lodash'
import useDialogStore from 'stores/dialog.js' import useDialogStore from 'stores/dialog.js'
import bytes from 'bytes' import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
@ -68,7 +68,7 @@ const filterOption = [
}, },
{ {
value: 2, value: 2,
label: i18n.t('common.score'), label: i18n.t('interface.score'),
}, },
] ]
const filterType = ref(1) const filterType = ref(1)
@ -88,11 +88,14 @@ const inEdit = computed(() => {
return currentEditRow.no > 0 return currentEditRow.no > 0
}) })
const fullEdit = ref(false) const fullEdit = ref(false)
const inFullEdit = computed(() => {
return inEdit.value && fullEdit.value
})
const scoreFilterOption = ref(null) const scoreFilterOption = ref(null)
const scoreColumn = computed(() => ({ const scoreColumn = computed(() => ({
key: 'score', key: 'score',
title: i18n.t('common.score'), title: i18n.t('interface.score'),
align: 'center', align: 'center',
titleAlign: 'center', titleAlign: 'center',
resizable: true, resizable: true,
@ -180,11 +183,7 @@ const saveEdit = async (field, value, decode, format) => {
throw new Error('row not exists') throw new Error('row not exists')
} }
if (isEmpty(value)) { const { updated, success, msg } = await browserStore.updateZSetItem({
value = currentEditRow.value
}
const { success, msg } = await browserStore.updateZSetItem({
server: props.name, server: props.name,
db: props.db, db: props.db,
key: keyName.value, key: keyName.value,
@ -195,6 +194,14 @@ const saveEdit = async (field, value, decode, format) => {
format, format,
}) })
if (success) { if (success) {
row.v = head(keys(updated))
row.s = updated[row.v]
const { value: displayVal } = await browserStore.convertValue({
value: row.v,
decode: props.decode,
format: props.format,
})
row.dv = displayVal
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
} else { } else {
$message.error(msg) $message.error(msg)
@ -233,7 +240,7 @@ const actionColumn = {
row.v, row.v,
) )
if (success) { if (success) {
// props.value.splice(index, 1) props.value.splice(index, 1)
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.v })) $message.success(i18n.t('dialogue.delete_key_succ', { key: row.v }))
} else { } else {
$message.error(msg) $message.error(msg)
@ -347,6 +354,7 @@ defineExpose({
<template> <template>
<div class="content-wrapper flex-box-v"> <div class="content-wrapper flex-box-v">
<content-toolbar <content-toolbar
v-show="!inFullEdit"
:db="props.db" :db="props.db"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
@ -358,7 +366,7 @@ defineExpose({
@delete="emit('delete')" @delete="emit('delete')"
@reload="emit('reload')" @reload="emit('reload')"
@rename="emit('rename')" /> @rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div v-show="!inFullEdit" class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input-group> <n-input-group>
<n-select <n-select
@ -407,12 +415,13 @@ defineExpose({
<div class="value-wrapper value-item-part flex-box-h flex-item-expand"> <div class="value-wrapper value-item-part flex-box-h flex-item-expand">
<!-- table --> <!-- table -->
<n-data-table <n-data-table
v-show="!inFullEdit"
:row-key="(row) => row.v"
:bordered="false" :bordered="false"
:bottom-bordered="false" :bottom-bordered="false"
:columns="columns" :columns="columns"
:data="props.value" :data="props.value"
:loading="props.loading" :loading="props.loading"
:row-key="(row) => row.v"
:single-column="true" :single-column="true"
:single-line="false" :single-line="false"
class="flex-item-expand" class="flex-item-expand"
@ -423,16 +432,12 @@ defineExpose({
@update:filters="onUpdateFilter" /> @update:filters="onUpdateFilter" />
<!-- edit pane --> <!-- edit pane -->
<div
v-show="inEdit"
:style="{ position: fullEdit ? 'static' : 'relative' }"
class="entry-editor-container flex-item-expand"
style="width: 100%">
<content-entry-editor <content-entry-editor
v-show="inEdit"
v-model:fullscreen="fullEdit" v-model:fullscreen="fullEdit"
:decode="currentEditRow.decode" :decode="currentEditRow.decode"
:field="currentEditRow.score" :field="currentEditRow.score"
:field-label="$t('common.score')" :field-label="$t('interface.score')"
:format="currentEditRow.format" :format="currentEditRow.format"
:value="currentEditRow.value" :value="currentEditRow.value"
:value-label="$t('common.value')" :value-label="$t('common.value')"
@ -441,7 +446,6 @@ defineExpose({
@close="resetEdit" @close="resetEdit"
@save="saveEdit" /> @save="saveEdit" />
</div> </div>
</div>
<div class="value-footer flex-box-h"> <div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text> <n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical /> <n-divider v-if="!isNaN(props.length)" vertical />

View File

@ -50,9 +50,7 @@ const onUpdate = (val) => {
<n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" /> <n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" />
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item <n-form-item :label="$t('dialogue.field.element')" required>
:label="$t('dialogue.field.element') + ' (' + $t('common.field') + ':' + $t('common.value') + ')'"
required>
<n-dynamic-input <n-dynamic-input
v-model:value="kvList" v-model:value="kvList"
:key-placeholder="$t('dialogue.field.enter_field')" :key-placeholder="$t('dialogue.field.enter_field')"

View File

@ -58,17 +58,14 @@ const onUpdate = () => {
<n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" /> <n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" />
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item <n-form-item :label="$t('dialogue.field.element')" required>
:label="$t('dialogue.field.element') + ' (' + $t('common.value') + ':' + $t('common.score') + ')'"
required>
<n-dynamic-input v-model:value="zset" @create="onCreate" @update:value="onUpdate"> <n-dynamic-input v-model:value="zset" @create="onCreate" @update:value="onUpdate">
<template #default="{ value }"> <template #default="{ value }">
<n-input <n-input
v-model:value="value.value" v-model:value="value.value"
:placeholder="$t('dialogue.field.enter_value')" :placeholder="$t('dialogue.field.enter_elem')"
type="text" type="text"
@update:value="onUpdate" /> @update:value="onUpdate" />
<n-text>:</n-text>
<n-input-number <n-input-number
v-model:value="value.score" v-model:value="value.score"
:placeholder="$t('dialogue.field.enter_score')" :placeholder="$t('dialogue.field.enter_score')"

View File

@ -43,7 +43,7 @@ defineExpose({
<n-form-item label="ID"> <n-form-item label="ID">
<n-input v-model:value="id" /> <n-input v-model:value="id" />
</n-form-item> </n-form-item>
<n-form-item :label="$t('common.field') + ':' + $t('common.value')" required> <n-form-item :label="$t('dialogue.field.element')" required>
<n-dynamic-input <n-dynamic-input
v-model:value="kvList" v-model:value="kvList"
:key-placeholder="$t('dialogue.field.enter_field')" :key-placeholder="$t('dialogue.field.enter_field')"

View File

@ -132,7 +132,7 @@ const exThemeVars = computed(() => {
<n-tooltip :delay="2" :show-arrow="false" placement="right"> <n-tooltip :delay="2" :show-arrow="false" placement="right">
<template #trigger> <template #trigger>
<n-icon :size="iconSize"> <n-icon :size="iconSize">
<component :is="m.icon" :stroke-width="3.5" /> <component :is="m.icon" :stroke-width="3.5"></component>
</n-icon> </n-icon>
</template> </template>
{{ m.label }} {{ m.label }}

View File

@ -17,7 +17,6 @@
"key": "Key", "key": "Key",
"value": "Value", "value": "Value",
"field": "Field", "field": "Field",
"score": "Score",
"index": "Position" "index": "Position"
}, },
"preferences": { "preferences": {
@ -105,6 +104,7 @@
"empty_server_list": "No redis server", "empty_server_list": "No redis server",
"action": "Action", "action": "Action",
"type": "Type", "type": "Type",
"score": "Score",
"cli_welcome": "Welcome to Tiny RDM Redis Console", "cli_welcome": "Welcome to Tiny RDM Redis Console",
"sub_tab": { "sub_tab": {
"status": "Status", "status": "Status",

View File

@ -17,7 +17,6 @@
"key": "Chave", "key": "Chave",
"value": "Valor", "value": "Valor",
"field": "Campo", "field": "Campo",
"score": "Pontuação",
"index": "Posição" "index": "Posição"
}, },
"preferences": { "preferences": {
@ -99,6 +98,7 @@
"empty_server_list": "Nenhum servidor Redis", "empty_server_list": "Nenhum servidor Redis",
"action": "Ação", "action": "Ação",
"type": "Tipo", "type": "Tipo",
"score": "Pontuação",
"cli_welcome": "Bem-vindo ao Console Redis Tiny RDM", "cli_welcome": "Bem-vindo ao Console Redis Tiny RDM",
"sub_tab": { "sub_tab": {
"status": "Status", "status": "Status",

View File

@ -17,7 +17,6 @@
"key": "键", "key": "键",
"value": "值", "value": "值",
"field": "字段", "field": "字段",
"score": "分值",
"index": "位置" "index": "位置"
}, },
"preferences": { "preferences": {
@ -105,6 +104,7 @@
"empty_server_list": "还没添加Redis服务器", "empty_server_list": "还没添加Redis服务器",
"action": "操作", "action": "操作",
"type": "类型", "type": "类型",
"score": "分值",
"cli_welcome": "欢迎使用Tiny RDM的Redis命令行控制台", "cli_welcome": "欢迎使用Tiny RDM的Redis命令行控制台",
"sub_tab": { "sub_tab": {
"status": "状态", "status": "状态",

View File

@ -1009,12 +1009,9 @@ const useBrowserStore = defineStore('browser', {
* @param {string} field * @param {string} field
* @param {string} [newField] * @param {string} [newField]
* @param {string} [value] * @param {string} [value]
* @param {decodeTypes} [decode] * @param {string} [decode]
* @param {formatTypes} [format] * @param {string} [format]
* @param {decodeTypes} [retDecode]
* @param {formatTypes} [retFormat]
* @param {boolean} [refresh] * @param {boolean} [refresh]
* @param {number} [index] index for retrieve affect entries quickly
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
*/ */
async setHash({ async setHash({
@ -1026,9 +1023,7 @@ const useBrowserStore = defineStore('browser', {
value = '', value = '',
decode = decodeTypes.NONE, decode = decodeTypes.NONE,
format = formatTypes.RAW, format = formatTypes.RAW,
retDecode, refresh,
retFormat,
index,
}) { }) {
try { try {
const { data, success, msg } = await SetHashValue({ const { data, success, msg } = await SetHashValue({
@ -1040,34 +1035,17 @@ const useBrowserStore = defineStore('browser', {
value, value,
decode, decode,
format, format,
retDecode,
retFormat,
}) })
if (success) { if (success) {
/** const { updated = {}, removed = [], replaced = {} } = data
* @type {{updated: HashEntryItem[], removed: HashEntryItem[], updated: HashEntryItem[], replaced: HashReplaceItem[]}} if (refresh === true) {
*/
const { updated = [], removed = [], added = [], replaced = [] } = data
const tab = useTabStore() const tab = useTabStore()
if (!isEmpty(removed)) { if (!isEmpty(removed)) {
const removedKeys = map(removed, 'k') tab.removeValueEntries({ server, db, key, type: 'hash', entries: removed })
tab.removeValueEntries({ server, db, key, type: 'hash', entries: removedKeys })
} }
if (!isEmpty(updated)) { if (!isEmpty(updated)) {
tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated }) tab.upsertValueEntries({ server, db, key, type: 'hash', entries: updated })
} }
if (!isEmpty(added)) {
tab.insertValueEntries({ server, db, key, type: 'hash', entries: added })
}
if (!isEmpty(replaced)) {
tab.replaceValueEntries({
server,
db,
key,
type: 'hash',
entries: replaced,
index: [index],
})
} }
return { success, updated } return { success, updated }
} else { } else {
@ -1080,26 +1058,21 @@ const useBrowserStore = defineStore('browser', {
/** /**
* insert or update hash field item * insert or update hash field item
* @param {string} server * @param {string} connName
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string|number[]} key
* @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields * @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields
* @param {string[]} fieldItems field1, value1, filed2, value2... * @param {string[]} fieldItems field1, value1, filed2, value2...
* @returns {Promise<{[msg]: string, success: boolean, [updated]: [], [added]: []}>} * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
*/ */
async addHashField(server, db, key, action, fieldItems) { async addHashField(connName, db, key, action, fieldItems) {
try { try {
const { data, success, msg } = await AddHashField(server, db, key, action, fieldItems) const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
if (success) { if (success) {
const { updated = [], added = [] } = data const { updated = {} } = data
const tab = useTabStore() const tab = useTabStore()
if (!isEmpty(updated)) { tab.upsertValueEntries({ server: connName, db, key, type: 'hash', entries: updated })
tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated }) return { success, updated }
}
if (!isEmpty(added)) {
tab.insertValueEntries({ server, db, key, type: 'hash', entries: added })
}
return { success, updated, added }
} else { } else {
return { success: false, msg } return { success: false, msg }
} }
@ -1165,13 +1138,15 @@ const useBrowserStore = defineStore('browser', {
if (success) { if (success) {
const { left = [] } = data const { left = [] } = data
if (!isEmpty(left)) { if (!isEmpty(left)) {
// TODO: convert to display value
const entries = map(left, (v) => ({ v }))
const tab = useTabStore() const tab = useTabStore()
tab.insertValueEntries({ tab.upsertValueEntries({
server: server, server: server,
db, db,
key, key,
type: 'list', type: 'list',
entries: left, entries,
prepend: true, prepend: true,
}) })
} }
@ -1197,15 +1172,15 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await AddListItem(server, db, key, 1, values) const { data, success, msg } = await AddListItem(server, db, key, 1, values)
if (success) { if (success) {
const { right = [] } = data const { right = [] } = data
// FIXME: do not append items if not all items loaded
if (!isEmpty(right)) { if (!isEmpty(right)) {
const entries = map(right, (v) => ({ v }))
const tab = useTabStore() const tab = useTabStore()
tab.insertValueEntries({ tab.upsertValueEntries({
server: server, server: server,
db, db,
key, key,
type: 'list', type: 'list',
entries: right, entries,
prepend: false, prepend: false,
}) })
} }
@ -1227,57 +1202,24 @@ const useBrowserStore = defineStore('browser', {
* @param {string|number[]} value * @param {string|number[]} value
* @param {decodeTypes} decode * @param {decodeTypes} decode
* @param {formatTypes} format * @param {formatTypes} format
* @param {decodeTypes} [retDecode] * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
* @param {formatTypes} [retFormat]
* @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async updateListItem({ async updateListItem({ server, db, key, index, value, decode = decodeTypes.NONE, format = formatTypes.RAW }) {
server,
db,
key,
index,
value,
decode = decodeTypes.NONE,
format = formatTypes.RAW,
retDecode,
retFormat,
}) {
try { try {
const { data, success, msg } = await SetListItem({ const { data, success, msg } = await SetListItem({ server, db, key, index, value, decode, format })
server,
db,
key,
index,
value,
decode,
format,
retDecode,
retFormat,
})
if (success) { if (success) {
/** @type {{replaced: ListReplaceItem[]}} **/ const { updated = {} } = data
const { replaced = [], removed = [] } = data // if (!isEmpty(updated)) {
const tab = useTabStore() // const tab = useTabStore()
if (!isEmpty(replaced)) { // tab.upsertValueEntries({
tab.replaceValueEntries({ // server,
server, // db,
db, // key,
key, // type: 'list',
type: 'list', // entries: updated,
entries: replaced, // })
}) // }
} return { success, updated }
if (!isEmpty(removed)) {
const removedIndex = map(removed, 'index')
tab.removeValueEntries({
server,
db,
key,
type: 'list',
entries: removedIndex,
})
}
return { success }
} else { } else {
return { success, msg } return { success, msg }
} }
@ -1299,17 +1241,16 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetListItem({ server, db, key, index }) const { data, success, msg } = await SetListItem({ server, db, key, index })
if (success) { if (success) {
const { removed = [] } = data const { removed = [] } = data
const tab = useTabStore() // if (!isEmpty(removed)) {
if (!isEmpty(removed)) { // const tab = useTabStore()
const removedIndexes = map(removed, 'index') // tab.removeValueEntries({
tab.removeValueEntries({ // server,
server, // db,
db, // key,
key, // type: 'list',
type: 'list', // entries: removed,
entries: removedIndexes, // })
}) // }
}
return { success, removed } return { success, removed }
} else { } else {
return { success, msg } return { success, msg }
@ -1321,24 +1262,21 @@ const useBrowserStore = defineStore('browser', {
/** /**
* add item to set * add item to set
* @param {string} server * @param {string} connName
* @param {number} db * @param {number} db
* @param {string|number} key * @param {string|number} key
* @param {string|string[]} value * @param {string|string[]} value
* @returns {Promise<{[msg]: string, success: boolean}>} * @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async addSetItem(server, db, key, value) { async addSetItem(connName, db, key, value) {
try { try {
if ((!value) instanceof Array) { if ((!value) instanceof Array) {
value = [value] value = [value]
} }
const { data, success, msg } = await SetSetItem(server, db, key, false, value) const { data, success, msg } = await SetSetItem(connName, db, key, false, value)
if (success) { if (success) {
const { added } = data
if (!isEmpty(added)) {
const tab = useTabStore() const tab = useTabStore()
tab.insertValueEntries({ server, db, key, type: 'set', entries: added }) tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: value })
}
return { success } return { success }
} else { } else {
return { success, msg } return { success, msg }
@ -1355,48 +1293,20 @@ const useBrowserStore = defineStore('browser', {
* @param {string|number[]} key * @param {string|number[]} key
* @param {string|number[]} value * @param {string|number[]} value
* @param {string|number[]} newValue * @param {string|number[]} newValue
* @param {decodeTypes} [decode] * @param {string} [decode]
* @param {formatTypes} [format] * @param {string} [format]
* @param {decodeTypes} [retDecode] * @returns {Promise<{[msg]: string, success: boolean, [added]: string|number[]}>}
* @param {formatTypes} [retFormat]
* @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async updateSetItem({ async updateSetItem({ server, db, key, value, newValue, decode = decodeTypes.NONE, format = formatTypes.RAW }) {
server,
db,
key,
value,
newValue,
decode = decodeTypes.NONE,
format = formatTypes.RAW,
retDecode,
retFormat,
}) {
try { try {
const { data, success, msg } = await UpdateSetItem({ const { data, success, msg } = await UpdateSetItem({ server, db, key, value, newValue, decode, format })
server,
db,
key,
value,
newValue,
decode,
format,
retDecode,
retFormat,
})
if (success) { if (success) {
const { added, removed } = data const { added } = data
const tab = useTabStore() // const tab = useTabStore()
if (!isEmpty(removed)) { // tab.upsertValueEntries({ server, db, key, type: 'set', entries: { [value]: newValue } })
const removedValues = map(removed, 'v') return { success: true, added }
tab.removeValueEntries({ server, db, key, type: 'set', entries: removedValues })
}
if (!isEmpty(added)) {
tab.insertValueEntries({ server, db, key, type: 'set', entries: added })
}
return { success }
} else { } else {
return { success: false, msg } return { success, msg }
} }
} catch (e) { } catch (e) {
return { success: false, msg: e.message } return { success: false, msg: e.message }
@ -1413,14 +1323,10 @@ const useBrowserStore = defineStore('browser', {
*/ */
async removeSetItem(server, db, key, value) { async removeSetItem(server, db, key, value) {
try { try {
const { data, success, msg } = await SetSetItem(server, db, key, true, [value]) const { success, msg } = await SetSetItem(server, db, key, true, [value])
if (success) { if (success) {
const { removed } = data // const tab = useTabStore()
const tab = useTabStore() // tab.removeValueEntries({ server: connName, db, key, type: 'set', entries: [value] })
if (!isEmpty(removed)) {
const removedValues = map(removed, 'v')
tab.removeValueEntries({ server, db, key, type: 'set', entries: removedValues })
}
return { success } return { success }
} else { } else {
return { success, msg } return { success, msg }
@ -1432,25 +1338,19 @@ const useBrowserStore = defineStore('browser', {
/** /**
* add item to sorted set * add item to sorted set
* @param {string} server * @param {string} connName
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string|number[]} key
* @param {number} action * @param {number} action
* @param {Object.<string, number>} vs value: score * @param {Object.<string, number>} vs value: score
* @returns {Promise<{[msg]: string, success: boolean}>} * @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async addZSetItem(server, db, key, action, vs) { async addZSetItem(connName, db, key, action, vs) {
try { try {
const { data, success, msg } = await AddZSetValue(server, db, key, action, vs) const { success, msg } = await AddZSetValue(connName, db, key, action, vs)
if (success) {
const { added, updated } = data
const tab = useTabStore() const tab = useTabStore()
if (!isEmpty(added)) { tab.upsertValueEntries({ server: connName, db, key, type: 'zset', entries: vs })
tab.insertValueEntries({ server, db, key, type: 'zset', entries: added }) if (success) {
}
if (!isEmpty(updated)) {
tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated })
}
return { success } return { success }
} else { } else {
return { success, msg } return { success, msg }
@ -1470,10 +1370,7 @@ const useBrowserStore = defineStore('browser', {
* @param {number} score * @param {number} score
* @param {decodeTypes} decode * @param {decodeTypes} decode
* @param {formatTypes} format * @param {formatTypes} format
* @param {decodeTypes} [retDecode] * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}, [removed]: []}>}
* @param {formatTypes} [retFormat]
* @param {number} [index] index for retrieve affect entries quickly
* @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async updateZSetItem({ async updateZSetItem({
server, server,
@ -1484,9 +1381,6 @@ const useBrowserStore = defineStore('browser', {
score, score,
decode = decodeTypes.NONE, decode = decodeTypes.NONE,
format = formatTypes.RAW, format = formatTypes.RAW,
retDecode,
retFormat,
index,
}) { }) {
try { try {
const { data, success, msg } = await UpdateZSetValue({ const { data, success, msg } = await UpdateZSetValue({
@ -1498,25 +1392,16 @@ const useBrowserStore = defineStore('browser', {
score, score,
decode, decode,
format, format,
retDecode,
retFormat,
}) })
if (success) { if (success) {
const { updated = [], added = [], removed = [], replaced = [] } = data const { updated, removed } = data
const tab = useTabStore() // const tab = useTabStore()
if (!isEmpty(removed)) { // if (!isEmpty(updated)) {
const removedValues = map(removed, 'v') // tab.upsertValueEntries({ server, db, key, type: 'zset', entries: updated })
tab.removeValueEntries({ server, db, key, type: 'zset', entries: removedValues }) // }
} // if (!isEmpty(removed)) {
if (!isEmpty(updated)) { // tab.removeValueEntries({ server, db, key, type: 'zset', entries: removed })
tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated }) // }
}
if (!isEmpty(added)) {
tab.insertValueEntries({ server, db, key, type: 'zset', entries: added })
}
if (!isEmpty(replaced)) {
tab.replaceValueEntries({ server, db, key, type: 'zset', entries: replaced, index: [index] })
}
return { success, updated, removed } return { success, updated, removed }
} else { } else {
return { success, msg } return { success, msg }
@ -1539,11 +1424,10 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await UpdateZSetValue({ server, db, key, value, newValue: '', score: 0 }) const { data, success, msg } = await UpdateZSetValue({ server, db, key, value, newValue: '', score: 0 })
if (success) { if (success) {
const { removed } = data const { removed } = data
const tab = useTabStore() // if (!isEmpty(removed)) {
if (!isEmpty(removed)) { // const tab = useTabStore()
const removeValues = map(removed, 'v') // tab.removeValueEntries({ server: server, db, key, type: 'zset', entries: removed })
tab.removeValueEntries({ server, db, key, type: 'zset', entries: removeValues }) // }
}
return { success, removed } return { success, removed }
} else { } else {
return { success, msg } return { success, msg }
@ -1555,28 +1439,26 @@ const useBrowserStore = defineStore('browser', {
/** /**
* insert new stream field item * insert new stream field item
* @param {string} server * @param {string} connName
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string|number[]} key
* @param {string} id * @param {string} id
* @param {string[]} values field1, value1, filed2, value2... * @param {string[]} values field1, value1, filed2, value2...
* @returns {Promise<{[msg]: string, success: boolean}>} * @returns {Promise<{[msg]: string, success: boolean}>}
*/ */
async addStreamValue(server, db, key, id, values) { async addStreamValue(connName, db, key, id, values) {
try { try {
const { data = {}, success, msg } = await AddStreamValue(server, db, key, id, values) const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values)
if (success) { if (success) {
const { added = [] } = data const { updateID } = data
if (!isEmpty(added)) {
const tab = useTabStore() const tab = useTabStore()
tab.insertValueEntries({ tab.upsertValueEntries({
server, server: connName,
db, db,
key, key,
type: 'stream', type: 'stream',
entries: added, entries: [{ id: updateID, value: values }],
}) })
}
return { success } return { success }
} else { } else {
return { success: false, msg } return { success: false, msg }

View File

@ -1,4 +1,4 @@
import { assign, find, findIndex, get, isEmpty, pullAt, remove, set, size } from 'lodash' import { assign, find, findIndex, get, indexOf, isEmpty, pullAt, remove, set, size } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
const useTabStore = defineStore('tab', { const useTabStore = defineStore('tab', {
@ -25,62 +25,6 @@ const useTabStore = defineStore('tab', {
* @param {boolean} [loading] * @param {boolean} [loading]
*/ */
/**
* @typedef {Object} ListEntryItem
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} ListReplaceItem
* @property {number} index
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} HashEntryItem
* @property {string} k field name
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} HashReplaceItem
* @property {string|number[]} k field name
* @property {string|number[]} nk new field name
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} SetEntryItem
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} ZSetEntryItem
* @property {number} s score
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} ZSetReplaceItem
* @property {number} s score
* @property {string|number[]} v value
* @property {string|number[]} nv new value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} StreamEntryItem
* @property {string} id
* @property {Object.<string, *>} v value
* @property {string} [dv] display value
*/
/** /**
* *
* @returns {{tabList: TabItem[], activatedTab: string, activatedIndex: number}} * @returns {{tabList: TabItem[], activatedTab: string, activatedIndex: number}}
@ -234,22 +178,29 @@ const useTabStore = defineStore('tab', {
}, },
/** /**
* insert entries * update or insert value entries
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string} key
* @param {string} type * @param {string} type
* @param {ListEntryItem[]|HashEntryItem[]|SetEntryItem[]|ZSetEntryItem[]|StreamEntryItem[]} entries * @param {string[]|Object.<string, number>|Object.<number, string>} entries
* @param {boolean} [prepend] for list only * @param {boolean} [prepend] for list only
* @param {boolean} [reset]
* @param {boolean} [nocheck] ignore conflict checking for hash/set/zset
*/ */
insertValueEntries({ server, db, key, type, entries, prepend }) { upsertValueEntries({ server, db, key, type, entries, prepend, reset, nocheck }) {
const tab = find(this.tabList, { name: server, db, key }) const tab = find(this.tabList, { name: server, db, key })
if (tab == null) { if (tab == null) {
return return
} }
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'list': // {v:string, dv:[string]}[] 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 || [] tab.value = tab.value || []
if (prepend === true) { if (prepend === true) {
tab.value = [...entries, ...tab.value] tab.value = [...entries, ...tab.value]
@ -257,189 +208,88 @@ const useTabStore = defineStore('tab', {
tab.value.push(...entries) tab.value.push(...entries)
} }
tab.length += size(entries) tab.length += size(entries)
break
case 'hash': // {k:string, v:string, dv:[string]}[]
case 'set': // {v: string, s: number}[]
case 'zset': // {v: string, s: number}[]
tab.value = tab.value || []
tab.value.push(...entries)
tab.length += size(entries)
break
case 'stream': // {id: string, v: {}}[]
tab.value = tab.value || []
tab.value = [...entries, ...tab.value]
tab.length += size(entries)
break
}
},
/**
* update entries' value
* @param {string} server
* @param {number} db
* @param {string|number[]} key
* @param {string} type
* @param {ListEntryItem[]|HashEntryItem[]|SetEntryItem[]|ZSetEntryItem[]|StreamEntryItem[]} entries
*/
updateValueEntries({ server, db, key, type, entries }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'hash': // {k:string, v:string, dv:string}[]
tab.value = tab.value || []
for (const entry of entries) {
let updated = false
for (const val of tab.value) {
if (val.k === entry.k) {
val.v = entry.v
val.dv = entry.dv
updated = true
break
}
}
if (!updated) {
// no match element, append
tab.value.push(entry)
tab.length += 1
}
}
break
case 'zset': // {s:number, v:string, dv:string}[]
tab.value = tab.value || []
for (const entry of entries) {
let updated = false
for (const val of tab.value) {
if (val.v === entry.v) {
val.s = entry.s
val.dv = entry.dv
updated = true
break
}
}
if (!updated) {
// no match element, append
tab.value.push(entry)
tab.length += 1
}
}
break
}
},
/**
* replace entry item key or field in value(modify the index key)
* @param {string} server
* @param {number} db
* @param {string|number[]} key
* @param {string} type
* @param {ListReplaceItem[]|HashReplaceItem[]|ZSetReplaceItem[]} entries
* @param {number[]} [index] indexes for replacement, can improve search efficiency if configured
*/
replaceValueEntries({ server, db, key, type, entries, index }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'list': // ListReplaceItem[]
tab.value = tab.value || []
for (const entry of entries) {
if (size(tab.value) > entry.index) {
tab.value[entry.index] = {
v: entry.v,
dv: entry.dv,
} }
} else { } else {
// out of range, append // replace by index
tab.value.push(entry)
tab.length += 1
}
}
break
case 'hash': // HashReplaceItem[]
tab.value = tab.value || [] tab.value = tab.value || []
for (const idx of index) { for (const idx in entries) {
const entry = get(tab.value, idx) set(tab.value, idx, entries[idx])
if (entry != null) {
/** @type HashReplaceItem[] **/
const replaceEntry = remove(entries, (e) => e.k === entry.k)
if (!isEmpty(replaceEntry)) {
entry.k = replaceEntry[0].nk
entry.v = replaceEntry[0].v
entry.dv = replaceEntry[0].dv
} }
} }
}
// the left entries do not included in index list, try to retrieve the whole list
for (const entry of entries) {
let updated = false
for (const val of tab.value) {
if (val.k === entry.k) {
val.k = entry.nk
val.v = entry.v
val.dv = entry.dv
updated = true
break 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 (!updated) { if (nocheck !== true && !tab.value.hasOwnProperty(k)) {
// no match element, append
tab.value.push({
k: entry.nk,
v: entry.v,
dv: entry.dv,
})
tab.length += 1 tab.length += 1
} }
tab.value[k] = entries[k]
} }
break break
case 'zset': // ZSetReplaceItem[] case 'set': // string[] | Object.{string, string}
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || [] tab.value = tab.value || []
for (const idx of index) { if (entries instanceof Array) {
const entry = get(tab.value, idx) // add items
if (entry != null) { for (const elem of entries) {
/** @type ZSetReplaceItem[] **/ if (nocheck !== true && indexOf(tab.value, elem) === -1) {
const replaceEntry = remove(entries, ({ v }) => v === entry.k) tab.value.push(elem)
if (!isEmpty(replaceEntry)) {
entry.s = replaceEntry[0].s
entry.v = replaceEntry[0].nv
entry.dv = replaceEntry[0].dv
}
}
}
// the left entries do not included in index list, try to retrieve the whole list
for (const entry of entries) {
let updated = false
for (const val of tab.value) {
if (val.v === entry.v) {
val.s = entry.s
val.v = entry.nv
val.dv = entry.dv
updated = true
break
}
}
if (!updated) {
// no match element, append
tab.value.push({
s: entry.s,
v: entry.nv,
dv: entry.dv,
})
tab.length += 1 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 break
} }
}, },
@ -462,7 +312,7 @@ const useTabStore = defineStore('tab', {
case 'list': // string[] | number[] case 'list': // string[] | number[]
tab.value = tab.value || [] tab.value = tab.value || []
if (typeof entries[0] === 'number') { if (typeof entries[0] === 'number') {
// remove by index, sort by desc first // remove by index
entries.sort((a, b) => b - a) entries.sort((a, b) => b - a)
const removed = pullAt(tab.value, ...entries) const removed = pullAt(tab.value, ...entries)
tab.length -= size(removed) tab.length -= size(removed)
@ -479,41 +329,26 @@ const useTabStore = defineStore('tab', {
case 'hash': // string[] case 'hash': // string[]
tab.value = tab.value || {} tab.value = tab.value || {}
for (const k of entries) { for (const k of entries) {
for (let i = 0; i < tab.value.length; i++) { if (tab.value.hasOwnProperty(k)) {
if (tab.value[i].k === k) { delete tab.value[k]
tab.value.splice(i, 1)
tab.length -= 1 tab.length -= 1
break
}
} }
} }
break break
case 'set': // string[] case 'set': // []string
tab.value = tab.value || []
tab.length -= size(remove(tab.value, (v) => entries.indexOf(v) >= 0))
break
case 'zset': // string[] case 'zset': // string[]
tab.value = tab.value || [] tab.value = tab.value || []
for (const v of entries) { tab.length -= size(remove(tab.value, (v) => entries.indexOf(v.value) >= 0))
for (let i = 0; i < tab.value.length; i++) {
if (tab.value[i].v === v) {
tab.value.splice(i, 1)
tab.length -= 1
break
}
}
}
break break
case 'stream': // string[] case 'stream': // string[]
tab.value = tab.value || [] tab.value = tab.value || []
for (const id of entries) { tab.length -= size(remove(tab.value, (v) => entries.indexOf(v.id) >= 0))
for (let i = 0; i < tab.value.length; i++) {
if (tab.value[i].id === id) {
tab.value.splice(i, 1)
tab.length -= 1
break
}
}
}
break break
} }
}, },

View File

@ -86,7 +86,6 @@ body {
padding-top: 5px; padding-top: 5px;
//padding: 5px; //padding: 5px;
box-sizing: border-box; box-sizing: border-box;
position: relative;
.tb2 { .tb2 {
gap: 5px; gap: 5px;

View File

@ -1,6 +1,6 @@
import usePreferencesStore from 'stores/preferences.js' import usePreferencesStore from 'stores/preferences.js'
import { createDiscreteApi, darkTheme } from 'naive-ui' import { createDiscreteApi, darkTheme } from 'naive-ui'
import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js' import { themeOverrides } from '@/utils/theme.js'
import { i18nGlobal } from '@/utils/i18n.js' import { i18nGlobal } from '@/utils/i18n.js'
import { computed } from 'vue' import { computed } from 'vue'
@ -106,10 +106,6 @@ export async function setupDiscreteApi() {
messageProviderProps: { messageProviderProps: {
placement: 'bottom', placement: 'bottom',
keepAliveOnHover: true, keepAliveOnHover: true,
containerStyle: {
marginBottom: '38px',
},
themeOverrides: prefStore.isDark ? darkThemeOverrides.Message : themeOverrides.Message,
}, },
notificationProviderProps: { notificationProviderProps: {
max: 5, max: 5,

View File

@ -57,6 +57,9 @@ export const themeOverrides = {
buttonColorActive: '#D13B37', buttonColorActive: '#D13B37',
buttonTextColorActive: '#FFF', buttonTextColorActive: '#FFF',
}, },
Message: {
margin: '0 0 38px 0',
},
DataTable: { DataTable: {
thPaddingSmall: '6px 8px', thPaddingSmall: '6px 8px',
tdPaddingSmall: '6px 8px', tdPaddingSmall: '6px 8px',