Compare commits

..

14 Commits

38 changed files with 2229 additions and 996 deletions

View File

@ -41,7 +41,6 @@
## 未来版本规划
- [ ] 命令实时监控
- [ ] 发布/订阅支持
- [ ] 引入Monaco Editor
- [ ] 连接配置导入/导出
- [ ] 数据导入/导出

View File

@ -2,11 +2,15 @@ package services
import (
"context"
"encoding/csv"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"github.com/wailsapp/wails/v2/pkg/runtime"
"net/url"
"os"
"slices"
"sort"
"strconv"
@ -140,7 +144,7 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
{
Name: "db0",
Index: 0,
Keys: int(clusterKeyCount),
MaxKeys: int(clusterKeyCount),
},
}
} else {
@ -175,7 +179,7 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
return types.ConnectionDB{
Name: dbName,
Index: idx,
Keys: dbInfo["keys"],
MaxKeys: dbInfo["keys"],
Expires: dbInfo["expires"],
AvgTTL: dbInfo["avg_ttl"],
}
@ -230,21 +234,8 @@ func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
return
}
// get a redis client from local cache or create a new open
// if db >= 0, will also switch to db index
func (b *browserService) getRedisClient(connName string, db int) (item *connectionItem, err error) {
var ok bool
var client redis.UniversalClient
if item, ok = b.connMap[connName]; ok {
client = item.client
} else {
selConn := Connection().getConnection(connName)
if selConn == nil {
err = fmt.Errorf("no match connection \"%s\"", connName)
return
}
hook := redis2.NewHook(connName, func(cmd string, cost int64) {
func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (client redis.UniversalClient, err error) {
hook := redis2.NewHook(selConn.Name, func(cmd string, cost int64) {
now := time.Now()
//last := strings.LastIndex(cmd, ":")
//if last != -1 {
@ -252,13 +243,13 @@ func (b *browserService) getRedisClient(connName string, db int) (item *connecti
//}
b.cmdHistory = append(b.cmdHistory, cmdHistoryItem{
Timestamp: now.UnixMilli(),
Server: connName,
Server: selConn.Name,
Cmd: cmd,
Cost: cost,
})
})
client, err = Connection().createRedisClient(selConn.ConnectionConfig)
client, err = Connection().createRedisClient(selConn)
if err != nil {
err = fmt.Errorf("create conenction error: %s", err.Error())
return
@ -266,8 +257,7 @@ func (b *browserService) getRedisClient(connName string, db int) (item *connecti
_ = client.Do(b.ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err()
// add hook to each node in cluster mode
var cluster *redis.ClusterClient
if cluster, ok = client.(*redis.ClusterClient); ok {
if cluster, ok := client.(*redis.ClusterClient); ok {
err = cluster.ForEachShard(b.ctx, func(ctx context.Context, cli *redis.Client) error {
cli.AddHook(hook)
return nil
@ -280,10 +270,27 @@ func (b *browserService) getRedisClient(connName string, db int) (item *connecti
client.AddHook(hook)
}
if _, err = client.Ping(b.ctx).Result(); err != nil && err != redis.Nil {
if _, err = client.Ping(b.ctx).Result(); err != nil && !errors.Is(err, redis.Nil) {
err = errors.New("can not connect to redis server:" + err.Error())
return
}
return
}
// get a redis client from local cache or create a new open
// if db >= 0, will also switch to db index
func (b *browserService) getRedisClient(server string, db int) (item *connectionItem, err error) {
var ok bool
var client redis.UniversalClient
if item, ok = b.connMap[server]; ok {
client = item.client
} else {
selConn := Connection().getConnection(server)
if selConn == nil {
err = fmt.Errorf("no match connection \"%s\"", server)
return
}
client, err = b.createRedisClient(selConn.ConnectionConfig)
ctx, cancelFunc := context.WithCancel(b.ctx)
item = &connectionItem{
client: client,
@ -296,7 +303,7 @@ func (b *browserService) getRedisClient(connName string, db int) (item *connecti
if item.stepSize <= 0 {
item.stepSize = consts.DEFAULT_LOAD_SIZE
}
b.connMap[connName] = item
b.connMap[server] = item
}
// BUG: go-redis might not be executing commands on the corresponding database
@ -311,7 +318,7 @@ func (b *browserService) getRedisClient(connName string, db int) (item *connecti
return
}
item.db = db
b.connMap[connName].db = db
b.connMap[server].db = db
}
}
return
@ -324,12 +331,12 @@ func (b *browserService) loadDBSize(ctx context.Context, client redis.UniversalC
}
// save current scan cursor
func (b *browserService) setClientCursor(connName string, db int, cursor uint64) {
if _, ok := b.connMap[connName]; ok {
func (b *browserService) setClientCursor(server string, db int, cursor uint64) {
if _, ok := b.connMap[server]; ok {
if cursor == 0 {
delete(b.connMap[connName].cursor, db)
delete(b.connMap[server].cursor, db)
} else {
b.connMap[connName].cursor[db] = cursor
b.connMap[server].cursor[db] = cursor
}
}
}
@ -447,7 +454,7 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli
return nil
}
var keys []any
keys := make([]any, 0)
if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode
var mutex sync.Mutex
@ -465,7 +472,7 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli
})
}
if err != nil {
return nil, cursor, err
return keys, cursor, err
}
return keys, cursor, nil
}
@ -497,8 +504,8 @@ func (b *browserService) LoadNextKeys(connName string, db int, match, keyType st
return
}
// LoadAllKeys load all keys
func (b *browserService) LoadAllKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
// LoadNextAllKeys load next all keys
func (b *browserService) LoadNextAllKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db)
if err != nil {
resp.Msg = err.Error()
@ -523,6 +530,28 @@ func (b *browserService) LoadAllKeys(connName string, db int, match, keyType str
return
}
// LoadAllKeys load all keys
func (b *browserService) LoadAllKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db)
if err != nil {
resp.Msg = err.Error()
return
}
client, ctx := item.client, item.ctx
keys, _, err := b.scanKeys(ctx, client, match, keyType, 0, 0)
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true
resp.Data = map[string]any{
"keys": keys,
}
return
}
func (b *browserService) GetKeyType(param types.KeySummaryParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB)
if err != nil {
@ -771,6 +800,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
if param.Full || matchPattern != "*" {
// load all
cursor, reset = 0, true
items = []types.HashEntryItem{}
for {
loadedVal, cursor, subErr = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result()
if subErr != nil {
@ -841,6 +871,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
if param.Full || matchPattern != "*" {
// load all
cursor, reset = 0, true
items = []types.SetEntryItem{}
for {
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
if subErr != nil {
@ -905,6 +936,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
// load all
var loadedVal []string
cursor, reset = 0, true
items = []types.ZSetEntryItem{}
for {
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, matchPattern, scanSize).Result()
if err != nil {
@ -1151,7 +1183,7 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
score, _ := strconv.ParseFloat(strs[i+1].(string), 64)
members = append(members, redis.Z{
Score: score,
Member: strs[i],
Member: strs[i].(string),
})
}
err = client.ZAdd(ctx, key, members...).Err()
@ -1936,6 +1968,293 @@ func (b *browserService) DeleteOneKey(server string, db int, k any) (resp types.
return
}
// DeleteKeys delete keys sync with notification
func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo string) (resp types.JSResp) {
// connect a new connection to export keys
conf := Connection().getConnection(server)
if conf == nil {
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
return
}
var client redis.UniversalClient
var err error
var connConfig = conf.ConnectionConfig
connConfig.LastDB = db
if client, err = b.createRedisClient(connConfig); err != nil {
resp.Msg = err.Error()
return
}
ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc()
cancelEvent := "delete:stop:" + serialNo
runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
cancelFunc()
})
processEvent := "deleting:" + serialNo
total := len(ks)
var failed atomic.Int64
var canceled bool
var deletedKeys = make([]any, 0, total)
var mutex sync.Mutex
del := func(ctx context.Context, cli redis.UniversalClient) error {
startTime := time.Now().Add(-10 * time.Second)
for i, k := range ks {
// emit progress per second
param := map[string]any{
"total": total,
"progress": i + 1,
"processing": k,
}
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now()
runtime.EventsEmit(b.ctx, processEvent, param)
// do some sleep to prevent blocking the Redis server
time.Sleep(10 * time.Millisecond)
}
key := strutil.DecodeRedisKey(k)
delErr := cli.Del(ctx, key).Err()
if err != nil {
failed.Add(1)
} else {
// save deleted key
mutex.Lock()
deletedKeys = append(deletedKeys, k)
mutex.Unlock()
}
if errors.Is(delErr, context.Canceled) || canceled {
canceled = true
break
}
}
return nil
}
if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
return del(ctx, cli)
})
} else {
err = del(ctx, client)
}
runtime.EventsOff(ctx, cancelEvent)
resp.Success = true
resp.Data = struct {
Canceled bool `json:"canceled"`
Deleted any `json:"deleted"`
Failed int64 `json:"failed"`
}{
Canceled: canceled,
Deleted: deletedKeys,
Failed: failed.Load(),
}
return
}
// ExportKey export keys
func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) {
// connect a new connection to export keys
conf := Connection().getConnection(server)
if conf == nil {
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
return
}
var client redis.UniversalClient
var err error
var connConfig = conf.ConnectionConfig
connConfig.LastDB = db
if client, err = b.createRedisClient(connConfig); err != nil {
resp.Msg = err.Error()
return
}
ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc()
file, err := os.Create(path)
if err != nil {
resp.Msg = err.Error()
return
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
cancelEvent := "export:stop:" + path
runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
cancelFunc()
})
processEvent := "exporting:" + path
total := len(ks)
var exported, failed int64
var canceled bool
startTime := time.Now().Add(-10 * time.Second)
for i, k := range ks {
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now()
param := map[string]any{
"total": total,
"progress": i + 1,
"processing": k,
}
runtime.EventsEmit(b.ctx, processEvent, param)
}
key := strutil.DecodeRedisKey(k)
content, dumpErr := client.Dump(ctx, key).Bytes()
if errors.Is(dumpErr, context.Canceled) || canceled {
canceled = true
break
}
record := []string{hex.EncodeToString([]byte(key)), hex.EncodeToString(content)}
if includeExpire {
if dur, ttlErr := client.PTTL(ctx, key).Result(); ttlErr == nil && dur > 0 {
record = append(record, strconv.FormatInt(time.Now().Add(dur).UnixMilli(), 10))
} else {
record = append(record, "-1")
}
}
if err = writer.Write(record); err != nil {
failed += 1
} else {
exported += 1
}
}
runtime.EventsOff(ctx, cancelEvent)
resp.Success = true
resp.Data = struct {
Canceled bool `json:"canceled"`
Exported int64 `json:"exported"`
Failed int64 `json:"failed"`
}{
Canceled: canceled,
Exported: exported,
Failed: failed,
}
return
}
// ImportCSV import data from csv file
func (b *browserService) ImportCSV(server string, db int, path string, conflict int, includeExpire bool) (resp types.JSResp) {
// connect a new connection to export keys
conf := Connection().getConnection(server)
if conf == nil {
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
return
}
var client redis.UniversalClient
var err error
var connConfig = conf.ConnectionConfig
connConfig.LastDB = db
if client, err = b.createRedisClient(connConfig); err != nil {
resp.Msg = err.Error()
return
}
ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc()
file, err := os.Open(path)
if err != nil {
resp.Msg = err.Error()
return
}
defer file.Close()
reader := csv.NewReader(file)
cancelEvent := "import:stop:" + path
runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
cancelFunc()
})
processEvent := "importing:" + path
var line []string
var readErr error
var key, value []byte
var ttl time.Duration
var imported, ignored int64
var canceled bool
startTime := time.Now().Add(-10 * time.Second)
for {
readErr = nil
ttl = redis.KeepTTL
line, readErr = reader.Read()
if readErr != nil {
break
}
if len(line) < 1 {
continue
}
if key, readErr = hex.DecodeString(line[0]); readErr != nil {
continue
}
if value, readErr = hex.DecodeString(line[1]); readErr != nil {
continue
}
// get ttl
if includeExpire && len(line) > 2 {
if expire, ttlErr := strconv.ParseInt(line[2], 10, 64); ttlErr == nil && expire > 0 {
ttl = time.UnixMilli(expire).Sub(time.Now())
}
}
if conflict == 0 {
readErr = client.RestoreReplace(ctx, string(key), ttl, string(value)).Err()
} else {
keyStr := string(key)
// go-redis may crash when batch calling restore
// use "exists" to filter first
if n, _ := client.Exists(ctx, keyStr).Result(); n <= 0 {
readErr = client.Restore(ctx, keyStr, ttl, string(value)).Err()
} else {
readErr = errors.New("key existed")
}
}
if readErr != nil {
// restore fail
ignored += 1
} else {
imported += 1
}
if errors.Is(readErr, context.Canceled) || canceled {
canceled = true
break
}
if time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now()
param := map[string]any{
"imported": imported,
"ignored": ignored,
//"processing": string(key),
}
runtime.EventsEmit(b.ctx, processEvent, param)
// do some sleep to prevent blocking the Redis server
time.Sleep(10 * time.Millisecond)
}
}
runtime.EventsOff(ctx, cancelEvent)
resp.Success = true
resp.Data = struct {
Canceled bool `json:"canceled"`
Imported int64 `json:"imported"`
Ignored int64 `json:"ignored"`
}{
Canceled: canceled,
Imported: imported,
Ignored: ignored,
}
return
}
// FlushDB flush database
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db)

View File

@ -127,6 +127,9 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
WriteTimeout: time.Duration(config.ExecTimeout) * time.Second,
TLSConfig: tlsConfig,
}
if config.LastDB > 0 {
option.DB = config.LastDB
}
if sshClient != nil {
option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
return sshClient.Dial(network, addr)

View File

@ -3,11 +3,11 @@ package services
import (
"context"
"github.com/wailsapp/wails/v2/pkg/runtime"
"log"
"sync"
"time"
"tinyrdm/backend/consts"
"tinyrdm/backend/types"
sliceutil "tinyrdm/backend/utils/slice"
)
type systemService struct {
@ -44,13 +44,42 @@ func (s *systemService) Start(ctx context.Context) {
}
// SelectFile open file dialog to select a file
func (s *systemService) SelectFile(title string) (resp types.JSResp) {
func (s *systemService) SelectFile(title string, extensions []string) (resp types.JSResp) {
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
return runtime.FileFilter{
Pattern: "*." + extensions[i],
}
})
filepath, err := runtime.OpenFileDialog(s.ctx, runtime.OpenDialogOptions{
Title: title,
ShowHiddenFiles: true,
Filters: filters,
})
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true
resp.Data = map[string]any{
"path": filepath,
}
return
}
// SaveFile open file dialog to save a file
func (s *systemService) SaveFile(title string, defaultName string, extensions []string) (resp types.JSResp) {
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
return runtime.FileFilter{
Pattern: "*." + extensions[i],
}
})
filepath, err := runtime.SaveFileDialog(s.ctx, runtime.SaveDialogOptions{
Title: title,
ShowHiddenFiles: true,
DefaultFilename: defaultName,
Filters: filters,
})
if err != nil {
log.Println(err)
resp.Msg = err.Error()
return
}

View File

@ -36,7 +36,7 @@ type Connections []Connection
type ConnectionDB struct {
Name string `json:"name"`
Index int `json:"index"`
Keys int `json:"keys"`
MaxKeys int `json:"maxKeys"`
Expires int `json:"expires,omitempty"`
AvgTTL int `json:"avgTtl,omitempty"`
}

View File

@ -14,19 +14,19 @@
"monaco-editor": "^0.45.0",
"pinia": "^2.1.7",
"sass": "^1.69.5",
"vue": "^3.3.11",
"vue": "^3.3.13",
"vue-i18n": "^9.8.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"naive-ui": "^2.35.0",
"@vitejs/plugin-vue": "^5.0.0",
"naive-ui": "^2.36.0",
"prettier": "^3.1.1",
"unplugin-auto-import": "^0.17.2",
"unplugin-icons": "^0.18.1",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.7"
"vite": "^5.0.10"
}
},
"node_modules/@antfu/install-pkg": {
@ -78,7 +78,7 @@
},
"node_modules/@css-render/vue3-ssr": {
"version": "0.15.12",
"resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.12.tgz",
"resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.12.tgz",
"integrity": "sha512-AQLGhhaE0F+rwybRCkKUdzBdTEM/5PZBYy+fSYe1T9z9+yxMuV/k7ZRqa4M69X+EI1W8pa4kc9Iq2VjQkZx4rg==",
"dev": true,
"peerDependencies": {
@ -607,7 +607,7 @@
},
"node_modules/@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"dev": true
},
@ -852,49 +852,49 @@
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz",
"integrity": "sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.0.tgz",
"integrity": "sha512-7x5e8X4J1Wi4NxudGjJBd2OFerAi/0nzF80ojCzvfj347WVr0YSn82C8BSsgwSHzlk9Kw5xnZfj0/7RLnNwP5w==",
"dev": true,
"engines": {
"node": "^14.18.0 || >=16.0.0"
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^4.0.0 || ^5.0.0",
"vite": "^5.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.11.tgz",
"integrity": "sha512-h97/TGWBilnLuRaj58sxNrsUU66fwdRKLOLQ9N/5iNDfp+DZhYH9Obhe0bXxhedl8fjAgpRANpiZfbgWyruQ0w==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.13.tgz",
"integrity": "sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A==",
"dependencies": {
"@babel/parser": "^7.23.5",
"@vue/shared": "3.3.11",
"@vue/shared": "3.3.13",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.11.tgz",
"integrity": "sha512-zoAiUIqSKqAJ81WhfPXYmFGwDRuO+loqLxvXmfUdR5fOitPoUiIeFI9cTTyv9MU5O1+ZZglJVTusWzy+wfk5hw==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.13.tgz",
"integrity": "sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw==",
"dependencies": {
"@vue/compiler-core": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-core": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.11.tgz",
"integrity": "sha512-U4iqPlHO0KQeK1mrsxCN0vZzw43/lL8POxgpzcJweopmqtoYy9nljJzWDIQS3EfjiYhfdtdk9Gtgz7MRXnz3GA==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.13.tgz",
"integrity": "sha512-DQVmHEy/EKIgggvnGRLx21hSqnr1smUS9Aq8tfxiiot8UR0/pXKHN9k78/qQ7etyQTFj5em5nruODON7dBeumw==",
"dependencies": {
"@babel/parser": "^7.23.5",
"@vue/compiler-core": "3.3.11",
"@vue/compiler-dom": "3.3.11",
"@vue/compiler-ssr": "3.3.11",
"@vue/reactivity-transform": "3.3.11",
"@vue/shared": "3.3.11",
"@vue/compiler-core": "3.3.13",
"@vue/compiler-dom": "3.3.13",
"@vue/compiler-ssr": "3.3.13",
"@vue/reactivity-transform": "3.3.13",
"@vue/shared": "3.3.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.32",
@ -902,12 +902,12 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.11.tgz",
"integrity": "sha512-Zd66ZwMvndxRTgVPdo+muV4Rv9n9DwQ4SSgWWKWkPFebHQfVYRrVjeygmmDmPewsHyznCNvJ2P2d6iOOhdv8Qg==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.13.tgz",
"integrity": "sha512-d/P3bCeUGmkJNS1QUZSAvoCIW4fkOKK3l2deE7zrp0ypJEy+En2AcypIkqvcFQOcw3F0zt2VfMvNsA9JmExTaw==",
"dependencies": {
"@vue/compiler-dom": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-dom": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"node_modules/@vue/devtools-api": {
@ -916,65 +916,65 @@
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
},
"node_modules/@vue/reactivity": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.11.tgz",
"integrity": "sha512-D5tcw091f0nuu+hXq5XANofD0OXnBmaRqMYl5B3fCR+mX+cXJIGNw/VNawBqkjLNWETrFW0i+xH9NvDbTPVh7g==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.13.tgz",
"integrity": "sha512-fjzCxceMahHhi4AxUBzQqqVhuA21RJ0COaWTbIBl1PruGW1CeY97louZzLi4smpYx+CHfFPPU/CS8NybbGvPKQ==",
"dependencies": {
"@vue/shared": "3.3.11"
"@vue/shared": "3.3.13"
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.11.tgz",
"integrity": "sha512-fPGjH0wqJo68A0wQ1k158utDq/cRyZNlFoxGwNScE28aUFOKFEnCBsvyD8jHn+0kd0UKVpuGuaZEQ6r9FJRqCg==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.13.tgz",
"integrity": "sha512-oWnydGH0bBauhXvh5KXUy61xr9gKaMbtsMHk40IK9M4gMuKPJ342tKFarY0eQ6jef8906m35q37wwA8DMZOm5Q==",
"dependencies": {
"@babel/parser": "^7.23.5",
"@vue/compiler-core": "3.3.11",
"@vue/shared": "3.3.11",
"@vue/compiler-core": "3.3.13",
"@vue/shared": "3.3.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.11.tgz",
"integrity": "sha512-g9ztHGwEbS5RyWaOpXuyIVFTschclnwhqEbdy5AwGhYOgc7m/q3NFwr50MirZwTTzX55JY8pSkeib9BX04NIpw==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.13.tgz",
"integrity": "sha512-1TzA5TvGuh2zUwMJgdfvrBABWZ7y8kBwBhm7BXk8rvdx2SsgcGfz2ruv2GzuGZNvL1aKnK8CQMV/jFOrxNQUMA==",
"dependencies": {
"@vue/reactivity": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/reactivity": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.11.tgz",
"integrity": "sha512-OlhtV1PVpbgk+I2zl+Y5rQtDNcCDs12rsRg71XwaA2/Rbllw6mBLMi57VOn8G0AjOJ4Mdb4k56V37+g8ukShpQ==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.13.tgz",
"integrity": "sha512-JJkpE8R/hJKXqVTgUoODwS5wqKtOsmJPEqmp90PDVGygtJ4C0PtOkcEYXwhiVEmef6xeXcIlrT3Yo5aQ4qkHhQ==",
"dependencies": {
"@vue/runtime-core": "3.3.11",
"@vue/shared": "3.3.11",
"csstype": "^3.1.2"
"@vue/runtime-core": "3.3.13",
"@vue/shared": "3.3.13",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/runtime-dom/node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/@vue/server-renderer": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.11.tgz",
"integrity": "sha512-AIWk0VwwxCAm4wqtJyxBylRTXSy1wCLOKbWxHaHiu14wjsNYtiRCSgVuqEPVuDpErOlRdNnuRgipQfXRLjLN5A==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.13.tgz",
"integrity": "sha512-vSnN+nuf6iSqTL3Qgx/9A+BT+0Zf/VJOgF5uMZrKjYPs38GMYyAU1coDyBNHauehXDaP+zl73VhwWv0vBRBHcg==",
"dependencies": {
"@vue/compiler-ssr": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-ssr": "3.3.13",
"@vue/shared": "3.3.13"
},
"peerDependencies": {
"vue": "3.3.11"
"vue": "3.3.13"
}
},
"node_modules/@vue/shared": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.11.tgz",
"integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw=="
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.13.tgz",
"integrity": "sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA=="
},
"node_modules/acorn": {
"version": "8.11.2",
@ -1194,7 +1194,7 @@
},
"node_modules/evtd": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",
"resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz",
"integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
"dev": true
},
@ -1531,9 +1531,9 @@
"dev": true
},
"node_modules/naive-ui": {
"version": "2.35.0",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.35.0.tgz",
"integrity": "sha512-PdnLpOip1LQaKs5+rXLZoPDPQkTq26TnHWeABvUA2eOQjtHxE4+TQvj0Jq/W8clM2On/7jptoGmenLt48G3Bhg==",
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.36.0.tgz",
"integrity": "sha512-r1ydtEm1Ryf/aWpbLCf32mQAGK99jd1eXgpkCtIomcBRZeAtusfy6zCtIpCppoCuIKM3BW5DMafhVxilubk/lQ==",
"dev": true,
"dependencies": {
"@css-render/plugin-bem": "^0.15.12",
@ -1549,11 +1549,11 @@
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"seemly": "^0.3.6",
"seemly": "^0.3.8",
"treemate": "^0.3.11",
"vdirs": "^0.1.8",
"vooks": "^0.2.12",
"vueuc": "^0.4.51"
"vueuc": "^0.4.54"
},
"peerDependencies": {
"vue": "^3.0.0"
@ -1868,9 +1868,9 @@
"dev": true
},
"node_modules/seemly": {
"version": "0.3.6",
"resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.6.tgz",
"integrity": "sha512-lEV5VB8BUKTo/AfktXJcy+JeXns26ylbMkIUco8CYREsQijuz4mrXres2Q+vMLdwkuLxJdIPQ8IlCIxLYm71Yw==",
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.8.tgz",
"integrity": "sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==",
"dev": true
},
"node_modules/shebang-command": {
@ -2137,7 +2137,7 @@
},
"node_modules/vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
"integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
"dev": true,
"dependencies": {
@ -2148,9 +2148,9 @@
}
},
"node_modules/vite": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.7.tgz",
"integrity": "sha512-B4T4rJCDPihrQo2B+h1MbeGL/k/GMAHzhQ8S0LjQ142s6/+l3hHTT095ORvsshj4QCkoWu3Xtmob5mazvakaOw==",
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
"integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",
@ -2204,7 +2204,7 @@
},
"node_modules/vooks": {
"version": "0.2.12",
"resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
"resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz",
"integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
"dev": true,
"dependencies": {
@ -2215,15 +2215,15 @@
}
},
"node_modules/vue": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.11.tgz",
"integrity": "sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.13.tgz",
"integrity": "sha512-LDnUpQvDgsfc0u/YgtAgTMXJlJQqjkxW1PVcOnJA5cshPleULDjHi7U45pl2VJYazSSvLH8UKcid/kzH8I0a0Q==",
"dependencies": {
"@vue/compiler-dom": "3.3.11",
"@vue/compiler-sfc": "3.3.11",
"@vue/runtime-dom": "3.3.11",
"@vue/server-renderer": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-dom": "3.3.13",
"@vue/compiler-sfc": "3.3.13",
"@vue/runtime-dom": "3.3.13",
"@vue/server-renderer": "3.3.13",
"@vue/shared": "3.3.13"
},
"peerDependencies": {
"typescript": "*"
@ -2251,9 +2251,9 @@
}
},
"node_modules/vueuc": {
"version": "0.4.51",
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.51.tgz",
"integrity": "sha512-pLiMChM4f+W8czlIClGvGBYo656lc2Y0/mXFSCydcSmnCR1izlKPGMgiYBGjbY9FDkFG8a2HEVz7t0DNzBWbDw==",
"version": "0.4.54",
"resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.54.tgz",
"integrity": "sha512-2LED7h1BSnCRPBI6AlSIf+1Yte1shN+Vb2gpspO5wHI7zWzbcq4bAu2f9nFh5yXIUKdzqmLvzRsOXDl4TrDyCw==",
"dev": true,
"dependencies": {
"@css-render/vue3-ssr": "^0.15.10",
@ -2360,7 +2360,7 @@
},
"@css-render/vue3-ssr": {
"version": "0.15.12",
"resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.12.tgz",
"resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.12.tgz",
"integrity": "sha512-AQLGhhaE0F+rwybRCkKUdzBdTEM/5PZBYy+fSYe1T9z9+yxMuV/k7ZRqa4M69X+EI1W8pa4kc9Iq2VjQkZx4rg==",
"dev": true,
"requires": {}
@ -2658,7 +2658,7 @@
},
"@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"dev": true
},
@ -2811,43 +2811,43 @@
}
},
"@vitejs/plugin-vue": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz",
"integrity": "sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.0.tgz",
"integrity": "sha512-7x5e8X4J1Wi4NxudGjJBd2OFerAi/0nzF80ojCzvfj347WVr0YSn82C8BSsgwSHzlk9Kw5xnZfj0/7RLnNwP5w==",
"dev": true,
"requires": {}
},
"@vue/compiler-core": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.11.tgz",
"integrity": "sha512-h97/TGWBilnLuRaj58sxNrsUU66fwdRKLOLQ9N/5iNDfp+DZhYH9Obhe0bXxhedl8fjAgpRANpiZfbgWyruQ0w==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.13.tgz",
"integrity": "sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A==",
"requires": {
"@babel/parser": "^7.23.5",
"@vue/shared": "3.3.11",
"@vue/shared": "3.3.13",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"@vue/compiler-dom": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.11.tgz",
"integrity": "sha512-zoAiUIqSKqAJ81WhfPXYmFGwDRuO+loqLxvXmfUdR5fOitPoUiIeFI9cTTyv9MU5O1+ZZglJVTusWzy+wfk5hw==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.13.tgz",
"integrity": "sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw==",
"requires": {
"@vue/compiler-core": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-core": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"@vue/compiler-sfc": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.11.tgz",
"integrity": "sha512-U4iqPlHO0KQeK1mrsxCN0vZzw43/lL8POxgpzcJweopmqtoYy9nljJzWDIQS3EfjiYhfdtdk9Gtgz7MRXnz3GA==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.13.tgz",
"integrity": "sha512-DQVmHEy/EKIgggvnGRLx21hSqnr1smUS9Aq8tfxiiot8UR0/pXKHN9k78/qQ7etyQTFj5em5nruODON7dBeumw==",
"requires": {
"@babel/parser": "^7.23.5",
"@vue/compiler-core": "3.3.11",
"@vue/compiler-dom": "3.3.11",
"@vue/compiler-ssr": "3.3.11",
"@vue/reactivity-transform": "3.3.11",
"@vue/shared": "3.3.11",
"@vue/compiler-core": "3.3.13",
"@vue/compiler-dom": "3.3.13",
"@vue/compiler-ssr": "3.3.13",
"@vue/reactivity-transform": "3.3.13",
"@vue/shared": "3.3.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.32",
@ -2855,12 +2855,12 @@
}
},
"@vue/compiler-ssr": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.11.tgz",
"integrity": "sha512-Zd66ZwMvndxRTgVPdo+muV4Rv9n9DwQ4SSgWWKWkPFebHQfVYRrVjeygmmDmPewsHyznCNvJ2P2d6iOOhdv8Qg==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.13.tgz",
"integrity": "sha512-d/P3bCeUGmkJNS1QUZSAvoCIW4fkOKK3l2deE7zrp0ypJEy+En2AcypIkqvcFQOcw3F0zt2VfMvNsA9JmExTaw==",
"requires": {
"@vue/compiler-dom": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-dom": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"@vue/devtools-api": {
@ -2869,64 +2869,64 @@
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
},
"@vue/reactivity": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.11.tgz",
"integrity": "sha512-D5tcw091f0nuu+hXq5XANofD0OXnBmaRqMYl5B3fCR+mX+cXJIGNw/VNawBqkjLNWETrFW0i+xH9NvDbTPVh7g==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.13.tgz",
"integrity": "sha512-fjzCxceMahHhi4AxUBzQqqVhuA21RJ0COaWTbIBl1PruGW1CeY97louZzLi4smpYx+CHfFPPU/CS8NybbGvPKQ==",
"requires": {
"@vue/shared": "3.3.11"
"@vue/shared": "3.3.13"
}
},
"@vue/reactivity-transform": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.11.tgz",
"integrity": "sha512-fPGjH0wqJo68A0wQ1k158utDq/cRyZNlFoxGwNScE28aUFOKFEnCBsvyD8jHn+0kd0UKVpuGuaZEQ6r9FJRqCg==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.13.tgz",
"integrity": "sha512-oWnydGH0bBauhXvh5KXUy61xr9gKaMbtsMHk40IK9M4gMuKPJ342tKFarY0eQ6jef8906m35q37wwA8DMZOm5Q==",
"requires": {
"@babel/parser": "^7.23.5",
"@vue/compiler-core": "3.3.11",
"@vue/shared": "3.3.11",
"@vue/compiler-core": "3.3.13",
"@vue/shared": "3.3.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5"
}
},
"@vue/runtime-core": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.11.tgz",
"integrity": "sha512-g9ztHGwEbS5RyWaOpXuyIVFTschclnwhqEbdy5AwGhYOgc7m/q3NFwr50MirZwTTzX55JY8pSkeib9BX04NIpw==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.13.tgz",
"integrity": "sha512-1TzA5TvGuh2zUwMJgdfvrBABWZ7y8kBwBhm7BXk8rvdx2SsgcGfz2ruv2GzuGZNvL1aKnK8CQMV/jFOrxNQUMA==",
"requires": {
"@vue/reactivity": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/reactivity": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"@vue/runtime-dom": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.11.tgz",
"integrity": "sha512-OlhtV1PVpbgk+I2zl+Y5rQtDNcCDs12rsRg71XwaA2/Rbllw6mBLMi57VOn8G0AjOJ4Mdb4k56V37+g8ukShpQ==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.13.tgz",
"integrity": "sha512-JJkpE8R/hJKXqVTgUoODwS5wqKtOsmJPEqmp90PDVGygtJ4C0PtOkcEYXwhiVEmef6xeXcIlrT3Yo5aQ4qkHhQ==",
"requires": {
"@vue/runtime-core": "3.3.11",
"@vue/shared": "3.3.11",
"csstype": "^3.1.2"
"@vue/runtime-core": "3.3.13",
"@vue/shared": "3.3.13",
"csstype": "^3.1.3"
},
"dependencies": {
"csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}
}
},
"@vue/server-renderer": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.11.tgz",
"integrity": "sha512-AIWk0VwwxCAm4wqtJyxBylRTXSy1wCLOKbWxHaHiu14wjsNYtiRCSgVuqEPVuDpErOlRdNnuRgipQfXRLjLN5A==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.13.tgz",
"integrity": "sha512-vSnN+nuf6iSqTL3Qgx/9A+BT+0Zf/VJOgF5uMZrKjYPs38GMYyAU1coDyBNHauehXDaP+zl73VhwWv0vBRBHcg==",
"requires": {
"@vue/compiler-ssr": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-ssr": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"@vue/shared": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.11.tgz",
"integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw=="
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.13.tgz",
"integrity": "sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA=="
},
"acorn": {
"version": "8.11.2",
@ -3097,7 +3097,7 @@
},
"evtd": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",
"resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz",
"integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
"dev": true
},
@ -3364,9 +3364,9 @@
"dev": true
},
"naive-ui": {
"version": "2.35.0",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.35.0.tgz",
"integrity": "sha512-PdnLpOip1LQaKs5+rXLZoPDPQkTq26TnHWeABvUA2eOQjtHxE4+TQvj0Jq/W8clM2On/7jptoGmenLt48G3Bhg==",
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.36.0.tgz",
"integrity": "sha512-r1ydtEm1Ryf/aWpbLCf32mQAGK99jd1eXgpkCtIomcBRZeAtusfy6zCtIpCppoCuIKM3BW5DMafhVxilubk/lQ==",
"dev": true,
"requires": {
"@css-render/plugin-bem": "^0.15.12",
@ -3382,11 +3382,11 @@
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"seemly": "^0.3.6",
"seemly": "^0.3.8",
"treemate": "^0.3.11",
"vdirs": "^0.1.8",
"vooks": "^0.2.12",
"vueuc": "^0.4.51"
"vueuc": "^0.4.54"
}
},
"nanoid": {
@ -3605,9 +3605,9 @@
"dev": true
},
"seemly": {
"version": "0.3.6",
"resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.6.tgz",
"integrity": "sha512-lEV5VB8BUKTo/AfktXJcy+JeXns26ylbMkIUco8CYREsQijuz4mrXres2Q+vMLdwkuLxJdIPQ8IlCIxLYm71Yw==",
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.8.tgz",
"integrity": "sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==",
"dev": true
},
"shebang-command": {
@ -3795,7 +3795,7 @@
},
"vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
"integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
"dev": true,
"requires": {
@ -3803,9 +3803,9 @@
}
},
"vite": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.7.tgz",
"integrity": "sha512-B4T4rJCDPihrQo2B+h1MbeGL/k/GMAHzhQ8S0LjQ142s6/+l3hHTT095ORvsshj4QCkoWu3Xtmob5mazvakaOw==",
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
"integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==",
"dev": true,
"requires": {
"esbuild": "^0.19.3",
@ -3816,7 +3816,7 @@
},
"vooks": {
"version": "0.2.12",
"resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
"resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz",
"integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
"dev": true,
"requires": {
@ -3824,15 +3824,15 @@
}
},
"vue": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.11.tgz",
"integrity": "sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w==",
"version": "3.3.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.13.tgz",
"integrity": "sha512-LDnUpQvDgsfc0u/YgtAgTMXJlJQqjkxW1PVcOnJA5cshPleULDjHi7U45pl2VJYazSSvLH8UKcid/kzH8I0a0Q==",
"requires": {
"@vue/compiler-dom": "3.3.11",
"@vue/compiler-sfc": "3.3.11",
"@vue/runtime-dom": "3.3.11",
"@vue/server-renderer": "3.3.11",
"@vue/shared": "3.3.11"
"@vue/compiler-dom": "3.3.13",
"@vue/compiler-sfc": "3.3.13",
"@vue/runtime-dom": "3.3.13",
"@vue/server-renderer": "3.3.13",
"@vue/shared": "3.3.13"
}
},
"vue-i18n": {
@ -3846,9 +3846,9 @@
}
},
"vueuc": {
"version": "0.4.51",
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.51.tgz",
"integrity": "sha512-pLiMChM4f+W8czlIClGvGBYo656lc2Y0/mXFSCydcSmnCR1izlKPGMgiYBGjbY9FDkFG8a2HEVz7t0DNzBWbDw==",
"version": "0.4.54",
"resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.54.tgz",
"integrity": "sha512-2LED7h1BSnCRPBI6AlSIf+1Yte1shN+Vb2gpspO5wHI7zWzbcq4bAu2f9nFh5yXIUKdzqmLvzRsOXDl4TrDyCw==",
"dev": true,
"requires": {
"@css-render/vue3-ssr": "^0.15.10",

View File

@ -15,18 +15,18 @@
"monaco-editor": "^0.45.0",
"pinia": "^2.1.7",
"sass": "^1.69.5",
"vue": "^3.3.11",
"vue": "^3.3.13",
"vue-i18n": "^9.8.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"naive-ui": "^2.35.0",
"@vitejs/plugin-vue": "^5.0.0",
"naive-ui": "^2.36.0",
"prettier": "^3.1.1",
"unplugin-auto-import": "^0.17.2",
"unplugin-icons": "^0.18.1",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.7"
"vite": "^5.0.10"
}
}

View File

@ -1 +1 @@
c645c91528721588c620ff74fd475d09
8dd2611ec12ad4782807cfbdc3ab5216

View File

@ -18,6 +18,8 @@ import { WindowSetDarkTheme, WindowSetLightTheme } from 'wailsjs/runtime/runtime
import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
import AboutDialog from '@/components/dialogs/AboutDialog.vue'
import FlushDbDialog from '@/components/dialogs/FlushDbDialog.vue'
import ExportKeyDialog from '@/components/dialogs/ExportKeyDialog.vue'
import ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue'
const prefStore = usePreferencesStore()
const connectionStore = useConnectionStore()
@ -67,6 +69,8 @@ watch(
<add-fields-dialog />
<rename-key-dialog />
<delete-key-dialog />
<export-key-dialog />
<import-key-dialog />
<flush-db-dialog />
<set-ttl-dialog />
<preferences-dialog />

View File

@ -1,17 +1,26 @@
<script setup>
import { SelectFile } from 'wailsjs/go/services/systemService.js'
import { get } from 'lodash'
import { get, isEmpty } from 'lodash'
const props = defineProps({
value: String,
placeholder: String,
disabled: Boolean,
ext: String,
})
const emit = defineEmits(['update:value'])
const onInput = (val) => {
emit('update:value', val)
}
const onClear = () => {
emit('update:value', '')
}
const handleSelectFile = async () => {
const { success, data } = await SelectFile()
const { success, data } = await SelectFile('', isEmpty(props.ext) ? null : [props.ext])
if (success) {
const path = get(data, 'path', '')
emit('update:value', path)
@ -23,7 +32,13 @@ const handleSelectFile = async () => {
<template>
<n-input-group>
<n-input v-model:value="props.value" :disabled="props.disabled" :placeholder="placeholder" clearable />
<n-input
:disabled="props.disabled"
:placeholder="placeholder"
:value="props.value"
clearable
@clear="onClear"
@input="onInput" />
<n-button :disabled="props.disabled" :focusable="false" @click="handleSelectFile">...</n-button>
</n-input-group>
</template>

View File

@ -0,0 +1,46 @@
<script setup>
import { SaveFile } from 'wailsjs/go/services/systemService.js'
import { get } from 'lodash'
const props = defineProps({
value: String,
placeholder: String,
disabled: Boolean,
defaultPath: String,
})
const emit = defineEmits(['update:value'])
const onInput = (val) => {
emit('update:value', val)
}
const onClear = () => {
emit('update:value', '')
}
const handleSaveFile = async () => {
const { success, data } = await SaveFile(null, props.defaultPath, ['csv'])
if (success) {
const path = get(data, 'path', '')
emit('update:value', path)
} else {
emit('update:value', '')
}
}
</script>
<template>
<n-input-group>
<n-input
:value="props.value"
:disabled="props.disabled"
:placeholder="placeholder"
clearable
@input="onInput"
@clear="onClear" />
<n-button :disabled="props.disabled" :focusable="false" @click="handleSaveFile">...</n-button>
</n-input-group>
</template>
<style lang="scss" scoped></style>

View File

@ -58,7 +58,11 @@ const columns = computed(() => [
align: 'center',
titleAlign: 'center',
ellipsis: {
tooltip: true,
tooltip: {
style: {
maxWidth: '80vw',
},
},
},
render: ({ client, addr }, index) => {
let content = ''

View File

@ -89,7 +89,11 @@ const fieldColumn = computed(() => ({
titleAlign: 'center',
resizable: true,
ellipsis: {
tooltip: true,
tooltip: {
style: {
maxWidth: '80vw',
},
},
},
filterOptionValue: fieldFilterOption.value,
className: inEdit.value ? 'clickable' : '',
@ -114,7 +118,11 @@ const valueColumn = computed(() => ({
ellipsis: displayCode.value
? false
: {
tooltip: true,
tooltip: {
style: {
maxWidth: '80vw',
},
},
},
// filterOptionValue: valueFilterOption.value,
className: inEdit.value ? 'clickable' : '',

View File

@ -90,7 +90,11 @@ const valueColumn = computed(() => ({
ellipsis: displayCode.value
? false
: {
tooltip: true,
tooltip: {
style: {
maxWidth: '80vw',
},
},
},
filterOptionValue: valueFilterOption.value,
className: inEdit.value ? 'clickable' : '',

View File

@ -89,7 +89,11 @@ const valueColumn = computed(() => ({
ellipsis: displayCode.value
? false
: {
tooltip: true,
tooltip: {
style: {
maxWidth: '80vw',
},
},
},
filterOptionValue: valueFilterOption.value,
className: inEdit.value ? 'clickable' : '',

View File

@ -138,7 +138,11 @@ const valueColumn = computed(() => ({
ellipsis: displayCode.value
? false
: {
tooltip: true,
tooltip: {
style: {
maxWidth: '80vw',
},
},
},
filterOptionValue: valueFilterOption.value,
className: inEdit.value ? 'clickable' : '',

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { computed, nextTick, reactive, ref, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import { isEmpty, map, size } from 'lodash'
@ -47,7 +47,12 @@ const scanAffectedKey = async () => {
try {
loading.value = true
deleteForm.loadingAffected = true
const { keys = [] } = await browserStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
const { keys = [] } = await browserStore.scanKeys({
server: deleteForm.server,
db: deleteForm.db,
match: deleteForm.key,
loadType: 2,
})
deleteForm.affectedKeys = keys || []
deleteForm.showAffected = true
} finally {
@ -70,6 +75,7 @@ const onConfirmDelete = async () => {
try {
deleting.value = true
const { server, db, key, affectedKeys } = deleteForm
await nextTick()
browserStore.deleteKeys(server, db, affectedKeys).catch((e) => {})
} catch (e) {
$message.error(e.message)
@ -99,10 +105,10 @@ const onClose = () => {
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
<n-grid :x-gap="10">
<n-form-item-gi :label="$t('dialogue.key.server')" :span="12">
<n-input :value="deleteForm.server" readonly />
<n-input :autofocus="false" :value="deleteForm.server" readonly />
</n-form-item-gi>
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
<n-input :value="deleteForm.db.toString()" readonly />
<n-input :autofocus="false" :value="deleteForm.db.toString()" readonly />
</n-form-item-gi>
</n-grid>
<n-form-item

View File

@ -0,0 +1,121 @@
<script setup>
import { computed, reactive, ref, watchEffect } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js'
import FileSaveInput from '@/components/common/FileSaveInput.vue'
import { isEmpty, map, size } from 'lodash'
import { decodeRedisKey } from '@/utils/key_convert.js'
import dayjs from 'dayjs'
const exportKeyForm = reactive({
server: '',
db: 0,
expire: false,
keys: [],
file: '',
})
const dialogStore = useDialog()
const browserStore = useBrowserStore()
const loading = ref(false)
const exporting = ref(false)
watchEffect(() => {
if (dialogStore.exportKeyDialogVisible) {
const { server, db, keys } = dialogStore.exportKeyParam
exportKeyForm.server = server
exportKeyForm.db = db
exportKeyForm.ttl = false
exportKeyForm.keys = keys
exportKeyForm.file = ''
exporting.value = false
}
})
const keyLines = computed(() => {
return map(exportKeyForm.keys, (k) => decodeRedisKey(k))
})
const exportEnable = computed(() => {
return !isEmpty(exportKeyForm.keys) && !isEmpty(exportKeyForm.file)
})
const i18n = useI18n()
const onConfirmExport = async () => {
try {
exporting.value = true
const { server, db, keys, file, expire } = exportKeyForm
browserStore.exportKeys(server, db, keys, file, expire).catch((e) => {})
} catch (e) {
$message.error(e.message)
return
} finally {
exporting.value = false
}
dialogStore.closeExportKeyDialog()
}
const onClose = () => {
dialogStore.closeExportKeyDialog()
}
</script>
<template>
<n-modal
v-model:show="dialogStore.exportKeyDialogVisible"
:closable="false"
:close-on-esc="false"
:mask-closable="false"
:show-icon="false"
:title="$t('dialogue.export.name')"
preset="dialog"
transform-origin="center">
<n-spin :show="loading">
<n-form :model="exportKeyForm" :show-require-mark="false" label-placement="top">
<n-grid :x-gap="10">
<n-form-item-gi :label="$t('dialogue.key.server')" :span="12">
<n-input :autofocus="false" :value="exportKeyForm.server" readonly />
</n-form-item-gi>
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
<n-input :autofocus="false" :value="exportKeyForm.db.toString()" readonly />
</n-form-item-gi>
</n-grid>
<n-form-item :label="$t('dialogue.export.export_expire_title')">
<n-checkbox v-model:checked="exportKeyForm.expire" :autofocus="false">
{{ $t('dialogue.export.export_expire') }}
</n-checkbox>
</n-form-item>
<n-form-item :label="$t('dialogue.export.save_file')" required>
<file-save-input
v-model:value="exportKeyForm.file"
:default-path="`export_${dayjs().format('YYYYMMDDHHmmss')}.csv`"
:placeholder="$t('dialogue.export.save_file_tip')" />
</n-form-item>
<n-card
:title="$t('dialogue.key.affected_key') + `(${size(exportKeyForm.keys)})`"
embedded
size="small">
<n-log :line-height="1.5" :lines="keyLines" :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 :disabled="loading" :focusable="false" @click="onClose">
{{ $t('common.cancel') }}
</n-button>
<n-button
:disabled="!exportEnable"
:focusable="false"
:loading="loading"
type="primary"
@click="onConfirmExport">
{{ $t('dialogue.export.export') }}
</n-button>
</div>
</template>
</n-modal>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,129 @@
<script setup>
import { computed, reactive, ref, watchEffect } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js'
import { isEmpty } from 'lodash'
import FileOpenInput from '@/components/common/FileOpenInput.vue'
const importKeyForm = reactive({
server: '',
db: 0,
expire: true,
file: '',
type: 0,
conflict: 0,
})
const dialogStore = useDialog()
const browserStore = useBrowserStore()
const loading = ref(false)
const importing = ref(false)
watchEffect(() => {
if (dialogStore.importKeyDialogVisible) {
const { server, db } = dialogStore.importKeyParam
importKeyForm.server = server
importKeyForm.db = db
importKeyForm.expire = true
importKeyForm.file = ''
importKeyForm.type = 0
importKeyForm.conflict = 0
importing.value = false
}
})
const i18n = useI18n()
const conflictOption = [
{
value: 0,
label: i18n.t('dialogue.import.conflict_overwrite'),
},
{
value: 1,
label: i18n.t('dialogue.import.conflict_ignore'),
},
]
const importEnable = computed(() => {
return !isEmpty(importKeyForm.file)
})
const onConfirmImport = async () => {
try {
importing.value = true
const { server, db, file, conflict, expire } = importKeyForm
browserStore.importKeysFromCSVFile(server, db, file, conflict, expire).catch((e) => {})
} catch (e) {
$message.error(e.message)
return
} finally {
importing.value = false
}
dialogStore.closeImportKeyDialog()
}
const onClose = () => {
dialogStore.closeImportKeyDialog()
}
</script>
<template>
<n-modal
v-model:show="dialogStore.importKeyDialogVisible"
:closable="false"
:close-on-esc="false"
:mask-closable="false"
:show-icon="false"
:title="$t('dialogue.import.name')"
preset="dialog"
transform-origin="center">
<n-spin :show="loading">
<n-form :model="importKeyForm" :show-require-mark="false" label-placement="top">
<n-grid :x-gap="10">
<n-form-item-gi :label="$t('dialogue.key.server')" :span="12">
<n-input :autofocus="false" :value="importKeyForm.server" readonly />
</n-form-item-gi>
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
<n-input :autofocus="false" :value="importKeyForm.db.toString()" readonly />
</n-form-item-gi>
</n-grid>
<n-form-item :label="$t('dialogue.import.open_csv_file')" required>
<file-open-input
v-model:value="importKeyForm.file"
:placeholder="$t('dialogue.import.open_csv_file_tip')"
ext="csv" />
</n-form-item>
<n-form-item :label="$t('dialogue.import.import_expire_title')">
<n-checkbox v-model:checked="importKeyForm.expire" :autofocus="false">
{{ $t('dialogue.import.import_expire') }}
</n-checkbox>
</n-form-item>
<n-form-item :label="$t('dialogue.import.conflict_handle')">
<n-radio-group v-model:value="importKeyForm.conflict">
<n-radio-button
v-for="(op, i) in conflictOption"
:key="i"
:label="op.label"
:value="op.value" />
</n-radio-group>
</n-form-item>
</n-form>
</n-spin>
<template #action>
<div class="flex-item n-dialog__action">
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button
:disabled="!importEnable"
:focusable="false"
:loading="loading"
type="primary"
@click="onConfirmImport">
{{ $t('dialogue.export.export') }}
</n-button>
</div>
</template>
</n-modal>
</template>
<style lang="scss" scoped></style>

View File

@ -1,8 +1,8 @@
<script setup>
import { computed, h, reactive, ref, watch } from 'vue'
import { computed, h, nextTick, reactive, ref, watch } from 'vue'
import { types, typesColor } from '@/consts/support_redis_type.js'
import useDialog from 'stores/dialog'
import { get, isEmpty, keys, map, trim } from 'lodash'
import { endsWith, get, isEmpty, keys, map, trim } from 'lodash'
import NewStringValue from '@/components/new_value/NewStringValue.vue'
import NewHashValue from '@/components/new_value/NewHashValue.vue'
import NewListValue from '@/components/new_value/NewListValue.vue'
@ -32,7 +32,7 @@ const formRules = computed(() => {
}
})
const dbOptions = computed(() =>
map(keys(browserStore.databases[newForm.server]), (key) => ({
map(keys(browserStore.getDBList(newForm.server)), (key) => ({
label: key,
value: parseInt(key),
})),
@ -69,8 +69,17 @@ watch(
(visible) => {
if (visible) {
const { prefix, server, db } = dialogStore.newKeyParam
const separator = browserStore.getSeparator(server)
newForm.server = server
newForm.key = isEmpty(prefix) ? '' : prefix
if (isEmpty(prefix)) {
newForm.key = ''
} else {
if (!endsWith(prefix, separator)) {
newForm.key = prefix + separator
} else {
newForm.key = prefix
}
}
newForm.db = db
newForm.type = options.value[0].value
newForm.ttl = -1
@ -139,6 +148,7 @@ const onAdd = async () => {
})
if (success) {
// select current key
await nextTick()
tabStore.setSelectedKeys(server, nodeKey)
browserStore.loadKeySummary({ server, db, key, clearValue: true })
} else if (!isEmpty(msg)) {

View File

@ -0,0 +1,33 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M6 24V42H42V24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M33 15L24 6L15 15"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M24 6V32"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,33 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M6 24.0083V42H42V24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M33 23L24 32L15 23"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M23.9917 6V32"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -45,7 +45,7 @@ const onUpdate = (val) => {
</script>
<template>
<n-form-item :label="$t('interface.type')">
<n-form-item :label="$t('dialogue.field.conflict_handle')">
<n-radio-group :value="props.type" @update:value="(val) => emit('update:type', val)">
<n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" />
</n-radio-group>

View File

@ -41,7 +41,7 @@ defineExpose({
</script>
<template>
<n-form-item :label="$t('dialogue.field.element')" required>
<n-form-item :label="$t('dialogue.field.conflict_handle')" required>
<n-dynamic-input v-model:value="zset" @create="onCreate" @update:value="onUpdate">
<template #default="{ value }">
<n-input

View File

@ -4,7 +4,7 @@ import BrowserTree from './BrowserTree.vue'
import IconButton from '@/components/common/IconButton.vue'
import useTabStore from 'stores/tab.js'
import { computed, nextTick, onMounted, reactive, ref, unref } from 'vue'
import { find, map, size } from 'lodash'
import { find, get, map, size } from 'lodash'
import Refresh from '@/components/icons/Refresh.vue'
import useDialogStore from 'stores/dialog.js'
import { useI18n } from 'vue-i18n'
@ -23,6 +23,9 @@ import useConnectionStore from 'stores/connections.js'
import ListCheckbox from '@/components/icons/ListCheckbox.vue'
import Close from '@/components/icons/Close.vue'
import More from '@/components/icons/More.vue'
import Export from '@/components/icons/Export.vue'
import { ConnectionType } from '@/consts/connection_type.js'
import Import from '@/components/icons/Import.vue'
const props = defineProps({
server: String,
@ -50,7 +53,7 @@ const dbSelectOptions = computed(() => {
if (props.db === db.db) {
return {
value: db.db,
label: `db${db.db} (${db.keys}/${db.maxKeys})`,
label: `db${db.db} (${db.keyCount}/${db.maxKeys})`,
}
}
return {
@ -62,6 +65,8 @@ const dbSelectOptions = computed(() => {
const moreOptions = computed(() => {
return [
{ key: 'import', label: i18n.t('interface.import_key'), icon: render.renderIcon(Import, { strokeWidth: 3.5 }) },
{ key: 'divider', type: 'divider' },
{ key: 'flush', label: i18n.t('interface.flush_db'), icon: render.renderIcon(Delete, { strokeWidth: 3.5 }) },
{
key: 'disconnect',
@ -76,7 +81,7 @@ const loadProgress = computed(() => {
if (db.maxKeys <= 0) {
return 100
}
return (db.keys * 100) / Math.max(db.keys, db.maxKeys)
return (db.keyCount * 100) / Math.max(db.keyCount, db.maxKeys)
})
const checkedCount = computed(() => {
@ -86,7 +91,7 @@ const checkedCount = computed(() => {
const checkedTip = computed(() => {
const dblist = browserStore.getDBList(props.server)
const db = find(dblist, { db: props.db })
return `${checkedCount.value} / ${Math.max(db.maxKeys, checkedCount.value)}`
return `${checkedCount.value} / ${Math.max(db.keyCount, checkedCount.value)}`
})
const onReload = async () => {
@ -116,6 +121,16 @@ const onReload = async () => {
}
const onAddKey = () => {
const selectedKey = get(browserTreeRef.value?.getSelectedKey(), 0)
if (selectedKey != null) {
const node = browserStore.getNode(selectedKey)
const { type = ConnectionType.RedisValue, redisKey } = node
if (type === ConnectionType.RedisKey) {
// has prefix
dialogStore.openNewKeyDialog(redisKey, props.server, props.db)
return
}
}
dialogStore.openNewKeyDialog('', props.server, props.db)
}
@ -146,6 +161,14 @@ const onDeleteChecked = () => {
browserTreeRef.value?.deleteCheckedItems()
}
const onExportChecked = () => {
browserTreeRef.value?.exportCheckedItems()
}
const onImportData = () => {
dialogStore.openImportKeyDialog(props.server, props.db)
}
const onFlush = () => {
dialogStore.openFlushDBDialog(props.server, props.db)
}
@ -199,6 +222,9 @@ const onMatchInput = (matchVal, filterVal) => {
const onSelectOptions = (select) => {
switch (select) {
case 'import':
onImportData()
break
case 'flush':
onFlush()
break
@ -259,7 +285,7 @@ onMounted(() => onReload())
<!-- loaded progress -->
<n-progress
:border-radius="0"
:color="fullyLoaded ? '#0000' : themeVars.primaryColor"
:color="loadProgress >= 100 ? '#0000' : themeVars.primaryColor"
:height="2"
:percentage="loadProgress"
:processing="loading"
@ -329,6 +355,14 @@ onMounted(() => onReload())
<!-- check mode function bar -->
<div v-else class="flex-box-h nav-pane-func">
<icon-button
:button-class="['nav-pane-func-btn']"
:disabled="checkedCount <= 0"
:icon="Export"
:stroke-width="3.5"
size="20"
t-tooltip="interface.export_checked"
@click="onExportChecked" />
<icon-button
:button-class="['nav-pane-func-btn']"
:disabled="checkedCount <= 0"

View File

@ -40,6 +40,7 @@ const props = defineProps({
const themeVars = useThemeVars()
const render = useRender()
const i18n = useI18n()
/** @type {Ref<UnwrapRef<string[]>>} */
const expandedKeys = ref([])
const connectionStore = useConnectionStore()
const browserStore = useBrowserStore()
@ -47,15 +48,6 @@ const prefStore = usePreferencesStore()
const tabStore = useTabStore()
const dialogStore = useDialogStore()
watchEffect(
() => {
if (!props.checkMode) {
tabStore.setCheckedKeys(props.server)
}
},
{ flush: 'post' },
)
/**
*
* @type {ComputedRef<string[]>}
@ -82,9 +74,7 @@ const checkedKeys = computed(() => {
})
const data = computed(() => {
// const dbs = get(browserStore.databases, props.server, [])
// return dbs
return browserStore.getKeyList(props.server)
return browserStore.getKeyStruct(props.server, props.checkMode)
})
const backgroundColor = computed(() => {
@ -178,6 +168,10 @@ const renderContextLabel = (option) => {
return h('div', { class: 'context-menu-item' }, option.label)
}
/**
*
* @param {string} key
*/
const expandKey = (key) => {
const idx = indexOf(expandedKeys.value, key)
if (idx === -1) {
@ -423,7 +417,7 @@ const renderPrefix = ({ option }) => {
const renderLabel = ({ option }) => {
switch (option.type) {
case ConnectionType.RedisKey:
return `${option.label} (${option.keys || 0})`
return `${option.label} (${option.keyCount || 0})`
// case ConnectionType.RedisValue:
// return `[${option.keyType}]${option.label}`
}
@ -563,6 +557,17 @@ const handleOutsideContextMenu = () => {
contextMenuParam.show = false
}
watchEffect(
() => {
if (!props.checkMode) {
tabStore.setCheckedKeys(props.server)
} else {
expandKey(`${ConnectionType.RedisDB}`)
}
},
{ flush: 'post' },
)
// the NTree node may get incorrect height after change data
// add key property to force refresh the component and then everything back to normal
const treeKey = ref(0)
@ -581,6 +586,16 @@ defineExpose({
dialogStore.openDeleteKeyDialog(props.server, props.db, redisKeys)
}
},
exportCheckedItems: () => {
const checkedKeys = tabStore.currentCheckedKeys
const redisKeys = map(checkedKeys, 'redisKey')
if (!isEmpty(redisKeys)) {
dialogStore.openExportKeyDialog(props.server, props.db, redisKeys)
}
},
getSelectedKey: () => {
return selectedKeys.value || []
},
})
</script>

View File

@ -68,6 +68,10 @@ const preferencesOptions = computed(() => {
// key: 'help',
// icon: render.renderIcon(Help, { strokeWidth: 3 }),
// },
{
label: i18n.t('menu.report_bug'),
key: 'report',
},
{
label: i18n.t('menu.check_update'),
key: 'update',
@ -97,6 +101,9 @@ const onSelectPreferenceMenu = (key) => {
case 'update':
prefStore.checkForUpdate(true)
break
case 'report':
BrowserOpenURL('https://github.com/tiny-craft/tiny-rdm/issues')
break
case 'about':
dialogStore.openAboutDialog()
break

View File

@ -75,10 +75,12 @@
"rename_key": "Rename Key",
"delete_key": "Delete Key",
"batch_delete_key": "Batch Delete Keys",
"import_key": "Import Key",
"flush_db": "Flush Database",
"check_mode": "Check Mode",
"quit_check_mode": "Quit Check Mode",
"delete_checked": "Delete Checked Items",
"export_checked": "Export Checked Items",
"copy_value": "Copy Value",
"edit_value": "Edit Value",
"save_update": "Save Update",
@ -142,10 +144,11 @@
"remove_tip": "{type} \"{name}\" will be deleted",
"remove_group_tip": "Group \"{name}\" and all connections in it will be deleted",
"delete_key_succ": "\"{key}\" has been deleted",
"deleting_key": "Deleting key: {key} ({index}/{count})",
"deleting_key": "Deleting key({index}/{count})",
"delete_completed": "Deletion process has been completed, {success} successed, {fail} failed",
"rename_binary_key_fail": "Rename binary key name is unsupported",
"handle_succ": "Success!",
"handle_cancel": "The operation has been canceled.",
"reload_succ": "Reloaded!",
"field_required": "This item should not be blank",
"spec_field_required": "\"{key}\" should not be blank",
@ -223,8 +226,7 @@
},
"cluster": {
"title": "Cluster",
"enable": "Serve as Cluster Node",
"readonly": "Enables read-only commands on slave nodes"
"enable": "Serve as Cluster Node"
}
},
"group": {
@ -250,6 +252,7 @@
"field": {
"new": "Add New Field",
"new_item": "Add New Item",
"conflict_handle": "When Field Conflict",
"overwrite_field": "Overwrite Existing Field",
"ignore_field": "Ignore Existing Field",
"insert_type": "Insert",
@ -269,11 +272,34 @@
"filter_pattern": "Pattern",
"filter_pattern_tip": "* : Matches zero or more characters. For example, 'key*' matches all keys starting with 'key'.\n? : Matches a single character. For example, 'key?' matches 'key1', 'key2'.\n[] : Matches a single character within the specified range. For example, 'key[1-3]' matches keys like 'key1', 'key2', 'key3'.\n\\ : Escape character. To match *, ?, [, or ], use the backslash '\\' for escaping."
},
"export": {
"name": "Export Data",
"export": "Export",
"save_file": "Export Path",
"save_file_tip": "Select the path to save exported file",
"exporting": "Exporting keys({index}/{count})",
"export_completed": "Export completed, {success} successes, {fail} failed"
},
"import": {
"name": "Import Data",
"export_expire_title": "Expiration",
"export_expire": "Export Expiration Time",
"import": "Import",
"open_csv_file": "Import File",
"open_csv_file_tip": "Select the file for import",
"conflict_handle": "Handle Key Conflict",
"conflict_overwrite": "Overwrite",
"conflict_ignore": "Ignore",
"importing": "Importing Keys imported/overwrite:{imported} conflict/fail:{conflict}",
"import_completed": "Import completed, {success} successes, {ignored} failed"
},
"ttl": {
"title": "Set Key TTL"
},
"upgrade": {
"title": "New Version Available",
"import_expire_title": "Expiration",
"import_expire": "Try Import Expiration Time",
"new_version_tip": "A new version({ver}) is available. Download now?",
"no_update": "You're update to date",
"download_now": "Download",
@ -293,6 +319,7 @@
"preferences": "Preferences",
"help": "Help",
"check_update": "Check for Updates...",
"report_bug": "Report a Bug",
"about": "About"
},
"log": {

View File

@ -75,10 +75,12 @@
"rename_key": "重命名键",
"delete_key": "删除键",
"batch_delete_key": "批量删除键",
"import_key": "导入数据",
"flush_db": "清空数据库",
"check_mode": "勾选模式",
"quit_check_mode": "退出勾选模式",
"delete_checked": "删除勾选项",
"delete_checked": "删除所选项",
"export_checked": "导出所选项",
"copy_value": "复制值",
"edit_value": "修改值",
"save_update": "保存修改",
@ -142,10 +144,11 @@
"remove_tip": "{type} \"{name}\" 将会被删除",
"remove_group_tip": "分组 \"{name}\"及其所有连接将会被删除",
"delete_key_succ": "{key} 已被删除",
"deleting_key": "正在删除键{key} ({index}/{count})",
"deleting_key": "正在删除键({index}/{count})",
"delete_completed": "已完成删除操作,成功{success}个,失败{fail}个",
"rename_binary_key_fail": "不支持重命名二进制键名",
"handle_succ": "操作成功",
"handle_cancel": "操作已取消",
"reload_succ": "已重新载入",
"field_required": "此项不能为空",
"spec_field_required": "{key} 不能为空",
@ -249,8 +252,9 @@
"field": {
"new": "添加新字段",
"new_item": "添加新元素",
"overwrite_field": "覆盖同名字段",
"ignore_field": "忽略同名字段",
"conflict_handle": "字段冲突处理",
"overwrite_field": "覆盖",
"ignore_field": "忽略",
"insert_type": "插入类型",
"append_item": "尾部追加",
"prepend_item": "插入头部",
@ -268,6 +272,29 @@
"filter_pattern": "过滤表达式",
"filter_pattern_tip": "*:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义"
},
"export": {
"name": "导出数据",
"export_expire_title": "过期时间",
"export_expire": "同时导出过期时间",
"export": "确认导出",
"save_file": "导出路径",
"save_file_tip": "选择导出文件保存路径",
"exporting": "正在导出键({index}/{count})",
"export_completed": "已完成导出操作,成功{success}个,失败{fail}个"
},
"import": {
"name": "导入数据",
"import_expire_title": "过期时间",
"import_expire": "尝试同时导入过期时间",
"import": "确认导入",
"open_csv_file": "导入文件路径",
"open_csv_file_tip": "选择需要导入的文件",
"conflict_handle": "键冲突处理",
"conflict_overwrite": "覆盖",
"conflict_ignore": "忽略",
"importing": "正在导入数据 已导入/覆盖:{imported} 冲突/失败:{conflict}",
"import_completed": "已完成导入操作,成功{success}个,忽略{ignored}个"
},
"ttl": {
"title": "设置键存活时间"
},
@ -292,6 +319,7 @@
"preferences": "偏好设置",
"help": "帮助",
"check_update": "检查更新...",
"report_bug": "报告错误",
"about": "关于"
},
"log": {

View File

@ -0,0 +1,10 @@
/**
* redis database item
*/
export class RedisDatabaseItem {
constructor({ db = 0, keyCount = 0, maxKeys = 0 }) {
this.db = db
this.keyCount = keyCount
this.maxKeys = maxKeys
}
}

View File

@ -0,0 +1,144 @@
import { isEmpty, remove, size, sortedIndexBy, sumBy } from 'lodash'
import { ConnectionType } from '@/consts/connection_type.js'
/**
* redis node item in tree view
*/
export class RedisNodeItem {
/**
*
* @param {string} key - tree node unique key
* @param {string} label
* @param {string} [name] - server name, type != ConnectionType.Group only
* @param {ConnectionType} type
* @param {number} [db] - database index, type == ConnectionType.RedisDB only
* @param {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue only
* @param {number[]} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
* @param {number} [keyCount] - children key count
* @param {number} [maxKeys] - max key count for database
* @param {boolean} [isLeaf]
* @param {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
* @param {boolean} [expanded] - current node is expanded
* @param {RedisNodeItem[]} [children]
* @param {string} [redisType] - redis type name, 'loading' indicate that is in loading progress
*/
constructor({
key,
label,
name,
type,
db = 0,
redisKey,
redisKeyCode,
keyCount = 0,
maxKeys = 0,
isLeaf = false,
opened = false,
expanded = false,
children,
redisType,
}) {
this.key = key
this.label = label
this.name = name
this.type = type
this.db = db
this.redisKey = redisKey
this.redisKeyCode = redisKeyCode
this.keyCount = keyCount
this.maxKeys = maxKeys
this.isLeaf = isLeaf
this.opened = opened
this.expanded = expanded
this.children = children
this.redisType = redisType
}
/**
* sort node list
* @param {RedisNodeItem[]} nodeList
* @private
*/
_sortNodes(nodeList) {
if (nodeList == null) {
return
}
nodeList.sort((a, b) => {
return a.key > b.key ? 1 : -1
})
}
/**
* sort all node item's children and calculate keys count
* @param skipSort skip sorting children
* @returns {boolean} return whether key count changed
*/
tidy(skipSort) {
if (this.type === ConnectionType.RedisValue) {
this.keyCount = 1
} else if (this.type === ConnectionType.RedisKey || this.type === ConnectionType.RedisDB) {
let keyCount = 0
if (!isEmpty(this.children)) {
if (!!!skipSort) {
this.sortChildren()
}
for (const child of this.children) {
child.tidy(skipSort)
keyCount += child.keyCount
}
} else {
keyCount = 0
}
if (this.keyCount !== keyCount) {
this.keyCount = keyCount
return true
}
}
return false
}
sortChildren() {
this.children.sort((a, b) => {
return a.key > b.key ? 1 : -1
})
}
/**
*
* @param {RedisNodeItem} child
* @param {boolean} [sorted]
*/
addChild(child, sorted) {
if (!!!sorted) {
this.children.push(child)
} else {
const idx = sortedIndexBy(this.children, child, 'key')
this.children.splice(idx, 0, child)
}
}
/**
*
* @param {{}} predicate
* @return {number}
*/
removeChild(predicate) {
if (this.type !== ConnectionType.RedisKey && this.type !== ConnectionType.RedisDB) {
return 0
}
const removed = remove(this.children, predicate)
return size(removed)
}
getChildren() {
return this.children
}
reCalcKeyCount() {
if (this.type === ConnectionType.RedisValue) {
this.keyCount = 1
}
this.keyCount = sumBy(this.children, (c) => c.keyCount)
return this.keyCount
}
}

View File

@ -0,0 +1,449 @@
import { initial, isEmpty, join, last, mapValues, size, slice, sortBy, split, toUpper } from 'lodash'
import useConnectionStore from 'stores/connections.js'
import { ConnectionType } from '@/consts/connection_type.js'
import { RedisDatabaseItem } from '@/objects/redisDatabaseItem.js'
import { KeyViewType } from '@/consts/key_view_type.js'
import { RedisNodeItem } from '@/objects/redisNodeItem.js'
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
/**
* server connection state
*/
export class RedisServerState {
/**
* @typedef {Object} LoadingState
* @property {boolean} loading indicated that is loading children now
* @property {boolean} fullLoaded indicated that all children already loaded
*/
/**
* @param {string} name server name
* @param {number} db current opened database
* @param {{}} stats current server status info
* @param {Object.<number, RedisDatabaseItem>} databases database list
* @param {string|null} patternFilter pattern filter
* @param {string|null} typeFilter redis type filter
* @param {LoadingState} loadingState all loading state in opened connections map by server and LoadingState
* @param {KeyViewType} viewType view type selection for all opened connections group by 'server'
* @param {Map<string, RedisNodeItem>} nodeMap map nodes by "type#key"
*/
constructor({
name,
db = 0,
stats = {},
databases = {},
patternFilter = null,
typeFilter = null,
loadingState = {},
viewType = KeyViewType.Tree,
nodeMap = new Map(),
}) {
this.name = name
this.db = db
this.stats = stats
this.databases = databases
this.patternFilter = patternFilter
this.typeFilter = typeFilter
this.loadingState = loadingState
this.viewType = viewType
this.nodeMap = nodeMap
this.getRoot()
const connStore = useConnectionStore()
const { keySeparator } = connStore.getDefaultSeparator(name)
this.separator = isEmpty(keySeparator) ? ':' : keySeparator
}
dispose() {
this.stats = {}
this.patternFilter = null
this.typeFilter = null
this.nodeMap.clear()
}
closeDatabase() {
this.patternFilter = null
this.typeFilter = null
this.nodeMap.clear()
}
setDatabaseKeyCount(db, maxKeys) {
const dbInst = this.databases[db]
if (dbInst == null) {
this.databases[db] = new RedisDatabaseItem({ db, maxKeys })
} else {
dbInst.maxKeys = maxKeys
}
return dbInst
}
/**
* update max key by increase/decrease value
* @param {number} db
* @param {number} updateVal
*/
updateDBKeyCount(db, updateVal) {
const dbInst = this.databases[this.db]
if (dbInst != null) {
dbInst.maxKeys = Math.max(0, dbInst.maxKeys + updateVal)
}
}
/**
* set db max keys value
* @param {number} db
* @param {number} count
*/
setDBKeyCount(db, count) {
const dbInst = this.databases[db]
if (dbInst != null) {
dbInst.maxKeys = Math.max(0, count)
}
}
/**
* get tree root item
* @returns {RedisNodeItem}
*/
getRoot() {
const rootKey = `${ConnectionType.RedisDB}`
let root = this.nodeMap.get(rootKey)
if (root == null) {
// create root node
root = new RedisNodeItem({
key: rootKey,
label: `db${this.db}`,
type: ConnectionType.RedisDB,
children: [],
})
this.nodeMap.set(rootKey, root)
}
return root
}
/**
* get database list sort by db asc
* @return {RedisDatabaseItem[]}
*/
getDatabase() {
return sortBy(mapValues(this.databases), 'db')
}
/**
*
* @param {ConnectionType} type
* @param {string} keyPath
* @param {RedisNodeItem} node
*/
addNode(type, keyPath, node) {
this.nodeMap.set(`${type}/${keyPath}`, node)
}
/**
* add keys to current opened database
* @param {Array<string|number[]>|Set<string|number[]>} keys
* @param {boolean} [sortInsert]
* @return {{newKey: number, newLayer: number, success: boolean, replaceKey: number}}
*/
addKeyNodes(keys, sortInsert) {
const result = {
success: false,
newLayer: 0,
newKey: 0,
replaceKey: 0,
}
const root = this.getRoot()
if (this.viewType === KeyViewType.List) {
// construct list view data
for (const key of keys) {
const k = decodeRedisKey(key)
const isBinaryKey = k !== key
const nodeKey = `${ConnectionType.RedisValue}/${nativeRedisKey(key)}`
const replaceKey = this.nodeMap.has(nodeKey)
const selectedNode = new RedisNodeItem({
key: `${this.name}/db${this.db}#${nodeKey}`,
label: k,
db: this.db,
keyCount: 0,
redisKey: k,
redisKeyCode: isBinaryKey ? key : undefined,
redisKeyType: undefined,
type: ConnectionType.RedisValue,
isLeaf: true,
})
this.nodeMap.set(nodeKey, selectedNode)
if (!replaceKey) {
root.addChild(selectedNode, sortInsert)
result.newKey += 1
} else {
result.replaceKey += 1
}
}
} else {
// construct tree view data
for (const key of keys) {
const k = decodeRedisKey(key)
const isBinaryKey = k !== key
const keyParts = isBinaryKey ? [nativeRedisKey(key)] : split(k, this.separator)
const len = size(keyParts)
const lastIdx = len - 1
let handlePath = ''
let node = root
for (let i = 0; i < len; i++) {
handlePath += keyParts[i]
if (i !== lastIdx) {
// layer
const nodeKey = `${ConnectionType.RedisKey}/${handlePath}`
let selectedNode = this.nodeMap.get(nodeKey)
if (selectedNode == null) {
selectedNode = new RedisNodeItem({
key: `${this.name}/db${this.db}#${nodeKey}`,
label: keyParts[i],
db: this.db,
keyCount: 0,
redisKey: handlePath,
type: ConnectionType.RedisKey,
isLeaf: false,
children: [],
})
this.nodeMap.set(nodeKey, selectedNode)
node.addChild(selectedNode, sortInsert)
result.newLayer += 1
}
node = selectedNode
handlePath += this.separator
} else {
// key
const nodeKey = `${ConnectionType.RedisValue}/${handlePath}`
const replaceKey = this.nodeMap.has(nodeKey)
const selectedNode = new RedisNodeItem({
key: `${this.name}/db${this.db}#${nodeKey}`,
label: isBinaryKey ? k : keyParts[i],
db: this.db,
keyCount: 0,
redisKey: handlePath,
redisKeyCode: isBinaryKey ? key : undefined,
redisKeyType: undefined,
type: ConnectionType.RedisValue,
isLeaf: true,
})
this.nodeMap.set(nodeKey, selectedNode)
if (!replaceKey) {
node.addChild(selectedNode, sortInsert)
result.newKey += 1
} else {
result.replaceKey += 1
}
}
}
}
}
return result
}
/**
* rename key to a new name
* @param key
* @param newKey
*/
renameKey(key, newKey) {
const oldLayer = initial(key.split(this.separator)).join(this.separator)
const newLayer = initial(newKey.split(this.separator)).join(this.separator)
if (oldLayer !== newLayer) {
// also change layer
this.removeKeyNode(key, false)
const { success } = this.addKeyNodes([newKey], true)
if (success) {
this.tidyNode(newLayer)
}
} else {
// change key name only
const oldNodeKeyName = `${ConnectionType.RedisValue}/${key}`
const newNodeKeyName = `${ConnectionType.RedisValue}/${newKey}`
const keyNode = this.nodeMap.get(oldNodeKeyName)
keyNode.key = `${this.name}/db${this.db}#${newNodeKeyName}`
keyNode.label = last(split(newKey, this.separator))
keyNode.redisKey = newKey
// not support rename binary key name yet
// keyNode.redisKeyCode = []
this.nodeMap.set(newNodeKeyName, keyNode)
this.nodeMap.delete(oldNodeKeyName)
}
}
/**
* remove key node by key name
* @param {string} [key]
* @param {boolean} [isLayer]
* @return {boolean}
*/
removeKeyNode(key, isLayer) {
if (isLayer === true) {
this.deleteChildrenKeyNodes(key)
}
const dbRoot = this.getRoot()
if (isEmpty(key)) {
// clear all key nodes
this.nodeMap.clear()
this.getRoot()
} else {
const keyParts = split(key, this.separator)
const totalParts = size(keyParts)
// remove from parent in tree node
const parentKey = slice(keyParts, 0, totalParts - 1)
let parentNode
if (isEmpty(parentKey)) {
parentNode = dbRoot
} else {
parentNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, this.separator)}`)
}
// not found parent node
if (parentNode == null) {
return false
}
parentNode.removeChild({
type: isLayer ? ConnectionType.RedisKey : ConnectionType.RedisValue,
redisKey: key,
})
// // check and remove empty layer node
// let i = totalParts - 1
// for (; i >= 0; i--) {
// const anceKey = join(slice(keyParts, 0, i), this.separator)
// if (i > 0) {
// const anceNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
// const redisKey = join(slice(keyParts, 0, i + 1), this.separator)
// anceNode.removeChild({ type: ConnectionType.RedisKey, redisKey })
//
// if (isEmpty(anceNode.children)) {
// this.nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`)
// } else {
// break
// }
// } else {
// // last one, remove from db node
// dbRoot.removeChild({ type: ConnectionType.RedisKey, redisKey: keyParts[0] })
// this.nodeMap.delete(`${ConnectionType.RedisValue}/${keyParts[0]}`)
// }
// }
}
return true
}
/**
* tidy node by key
* @param {string} [key]
* @param {boolean} [skipResort]
* @return
*/
tidyNode(key, skipResort) {
const rootNode = this.getRoot()
const keyParts = split(key, this.separator)
const totalParts = size(keyParts)
let node
// find last exists ancestor key
let i = totalParts - 1
for (; i > 0; i--) {
const parentKey = join(slice(keyParts, 0, i), this.separator)
node = this.nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
if (node != null) {
break
}
}
if (node == null) {
node = rootNode
}
const keyCountUpdated = node.tidy(skipResort)
if (keyCountUpdated) {
// update key count of parent and above
for (; i > 0; i--) {
const parentKey = join(slice(keyParts, 0, i), this.separator)
const parentNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
if (parentNode == null) {
break
}
const count = parentNode.reCalcKeyCount()
if (count <= 0) {
let anceKeyNode = rootNode
// remove from ancestor node
if (i > 1) {
const anceKey = join(slice(keyParts, 0, i - 1), this.separator)
anceKeyNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
}
if (anceKeyNode != null) {
anceKeyNode.removeChild({ type: ConnectionType.RedisKey, redisKey: parentKey })
}
}
}
// update key count of db
const dbInst = this.databases[this.db]
if (dbInst != null) {
dbInst.keyCount = rootNode.reCalcKeyCount()
}
}
}
/**
* add keys to current opened database
* @param {ConnectionType} type
* @param {string} keyPath
* @return {RedisNodeItem|null}
*/
getNode(type, keyPath) {
return this.nodeMap.get(`${type}/${keyPath}`) || null
}
/**
* delete node and all it's children from nodeMap
* @param {string} [key] clean nodeMap if key is empty
* @private
*/
deleteChildrenKeyNodes(key) {
if (isEmpty(key)) {
this.nodeMap.clear()
this.getRoot()
} else {
const nodeKey = `${ConnectionType.RedisKey}/${key}`
const node = this.nodeMap.get(nodeKey)
const children = node.children || []
for (const child of children) {
if (child.type === ConnectionType.RedisValue) {
if (!this.nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) {
console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`)
}
} else if (child.type === ConnectionType.RedisKey) {
this.deleteChildrenKeyNodes(child.redisKey)
}
}
if (!this.nodeMap.delete(nodeKey)) {
console.warn('delete map key', nodeKey)
}
}
}
getFilter() {
let pattern = this.patternFilter
if (isEmpty(pattern)) {
const conn = useConnectionStore()
pattern = conn.getDefaultKeyFilter(this.name)
}
return {
match: pattern,
type: toUpper(this.typeFilter),
}
}
/**
* set key filter
* @param {string} [pattern]
* @param {string} [type]
*/
setFilter({ pattern, type }) {
this.patternFilter = pattern === null ? this.patternFilter : pattern
this.typeFilter = type === null ? this.typeFilter : type
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { defineStore } from "pinia";
import useConnectionStore from "./connections.js";
import { defineStore } from 'pinia'
import useConnectionStore from './connections.js'
/**
* connection dialog type
@ -63,6 +63,19 @@ const useDialogStore = defineStore('dialog', {
},
deleteKeyDialogVisible: false,
exportKeyParam: {
server: '',
db: 0,
keys: [],
},
exportKeyDialogVisible: false,
importKeyParam: {
server: '',
db: 0,
},
importKeyDialogVisible: false,
flushDBParam: {
server: '',
db: 0,
@ -176,6 +189,36 @@ const useDialogStore = defineStore('dialog', {
this.deleteKeyDialogVisible = false
},
/**
*
* @param {string} server
* @param {number} db
* @param {string|string[]} keys
*/
openExportKeyDialog(server, db, keys) {
this.exportKeyParam.server = server
this.exportKeyParam.db = db
this.exportKeyParam.keys = keys
this.exportKeyDialogVisible = true
},
closeExportKeyDialog() {
this.exportKeyDialogVisible = false
},
/**
*
* @param {string} server
* @param {number} db
*/
openImportKeyDialog(server, db) {
this.importKeyParam.server = server
this.importKeyParam.db = db
this.importKeyDialogVisible = true
},
closeImportKeyDialog() {
this.importKeyDialogVisible = false
},
openFlushDBDialog(server, db) {
this.flushDBParam.server = server
this.flushDBParam.db = db

View File

@ -29,8 +29,8 @@ const useTabStore = defineStore('tab', {
/**
* @typedef {Object} CheckedKey
* @property {string[]} key
* @property {string[]} redisKey
* @property {string} key
* @property {string} [redisKey]
*/
/**

View File

@ -0,0 +1,3 @@
export const timeout = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms))
}

6
go.mod
View File

@ -5,12 +5,12 @@ go 1.21
require (
github.com/adrg/sysfont v0.1.2
github.com/andybalholm/brotli v1.0.6
github.com/google/uuid v1.4.0
github.com/google/uuid v1.5.0
github.com/klauspost/compress v1.17.4
github.com/redis/go-redis/v9 v9.3.0
github.com/redis/go-redis/v9 v9.3.1
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
github.com/wailsapp/wails/v2 v2.7.1
golang.org/x/crypto v0.16.0
golang.org/x/crypto v0.17.0
gopkg.in/yaml.v3 v3.0.1
)

12
go.sum
View File

@ -26,8 +26,8 @@ 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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.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.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
@ -68,8 +68,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.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
github.com/redis/go-redis/v9 v9.3.1/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=
@ -100,8 +100,8 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.7.1 h1:HAzp2c5ODOzsLC6ZMDVtNOB72ozM7/SJecJPB2Ur+UU=
github.com/wailsapp/wails/v2 v2.7.1/go.mod h1:oIJVwwso5fdOgprBYWXBBqtx6PaSvxg8/KTQHNGkadc=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=