Compare commits
14 Commits
c5abaa6573
...
93b04071e2
Author | SHA1 | Date |
---|---|---|
Lykin | 93b04071e2 | |
Lykin | b7433fadaa | |
Lykin | f597002378 | |
Lykin | 3fe8767c44 | |
Lykin | 2bc7a57773 | |
Lykin | 0fb93258e9 | |
Lykin | 21a569d9bb | |
Lykin | e28bb57a25 | |
Lykin | 5b9f261824 | |
Lykin | 7fa1ecfa0a | |
Lykin | f98229b9fa | |
Lykin | bce4e2323e | |
Lykin | b06217adc0 | |
Lykin | f5611a2635 |
|
@ -41,7 +41,6 @@
|
|||
## 未来版本规划
|
||||
- [ ] 命令实时监控
|
||||
- [ ] 发布/订阅支持
|
||||
- [ ] 引入Monaco Editor
|
||||
- [ ] 连接配置导入/导出
|
||||
- [ ] 数据导入/导出
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
c645c91528721588c620ff74fd475d09
|
||||
8dd2611ec12ad4782807cfbdc3ab5216
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -58,7 +58,11 @@ const columns = computed(() => [
|
|||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
tooltip: {
|
||||
style: {
|
||||
maxWidth: '80vw',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: ({ client, addr }, index) => {
|
||||
let content = ''
|
||||
|
|
|
@ -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' : '',
|
||||
|
|
|
@ -90,7 +90,11 @@ const valueColumn = computed(() => ({
|
|||
ellipsis: displayCode.value
|
||||
? false
|
||||
: {
|
||||
tooltip: true,
|
||||
tooltip: {
|
||||
style: {
|
||||
maxWidth: '80vw',
|
||||
},
|
||||
},
|
||||
},
|
||||
filterOptionValue: valueFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
|
|
|
@ -89,7 +89,11 @@ const valueColumn = computed(() => ({
|
|||
ellipsis: displayCode.value
|
||||
? false
|
||||
: {
|
||||
tooltip: true,
|
||||
tooltip: {
|
||||
style: {
|
||||
maxWidth: '80vw',
|
||||
},
|
||||
},
|
||||
},
|
||||
filterOptionValue: valueFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
|
|
|
@ -138,7 +138,11 @@ const valueColumn = computed(() => ({
|
|||
ellipsis: displayCode.value
|
||||
? false
|
||||
: {
|
||||
tooltip: true,
|
||||
tooltip: {
|
||||
style: {
|
||||
maxWidth: '80vw',
|
||||
},
|
||||
},
|
||||
},
|
||||
filterOptionValue: valueFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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,
|
||||
|
@ -164,7 +177,7 @@ const useDialogStore = defineStore('dialog', {
|
|||
*
|
||||
* @param {string} server
|
||||
* @param {number} db
|
||||
* @param {string | string[]} key
|
||||
* @param {string|string[]} key
|
||||
*/
|
||||
openDeleteKeyDialog(server, db, key) {
|
||||
this.deleteKeyParam.server = server
|
||||
|
@ -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
|
||||
|
|
|
@ -29,8 +29,8 @@ const useTabStore = defineStore('tab', {
|
|||
|
||||
/**
|
||||
* @typedef {Object} CheckedKey
|
||||
* @property {string[]} key
|
||||
* @property {string[]} redisKey
|
||||
* @property {string} key
|
||||
* @property {string} [redisKey]
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const timeout = (ms) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
6
go.mod
6
go.mod
|
@ -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
12
go.sum
|
@ -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=
|
||||
|
|
Loading…
Reference in New Issue