Compare commits

..

No commits in common. "bdfa31e4b6130e88da4a9e5b833bb160327d21ba" and "8c30daec150f1c47ad688da0338c30f3ac8a8052" have entirely different histories.

16 changed files with 516 additions and 79 deletions

View File

@ -98,9 +98,9 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
selConn := Connection().getConnection(name) selConn := Connection().getConnection(name)
// correct last database index // correct last database index
lastDB := selConn.LastDB lastDB := selConn.LastDB
if selConn.DBFilterType == "show" && !slices.Contains(selConn.DBFilterList, lastDB) { if selConn.DBFilterType == "show" && !sliceutil.Contains(selConn.DBFilterList, lastDB) {
lastDB = selConn.DBFilterList[0] lastDB = selConn.DBFilterList[0]
} else if selConn.DBFilterType == "hide" && slices.Contains(selConn.DBFilterList, lastDB) { } else if selConn.DBFilterType == "hide" && sliceutil.Contains(selConn.DBFilterList, lastDB) {
lastDB = selConn.DBFilterList[0] lastDB = selConn.DBFilterList[0]
} }
if lastDB != selConn.LastDB { if lastDB != selConn.LastDB {
@ -233,12 +233,13 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
// CloseConnection close redis server connection // CloseConnection close redis server connection
func (b *browserService) CloseConnection(name string) (resp types.JSResp) { func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
if item, ok := b.connMap[name]; ok { item, ok := b.connMap[name]
if ok {
delete(b.connMap, name) delete(b.connMap, name)
if item.client != nil {
if item.cancelFunc != nil { if item.cancelFunc != nil {
item.cancelFunc() item.cancelFunc()
} }
if item.client != nil {
item.client.Close() item.client.Close()
} }
} }
@ -246,7 +247,7 @@ func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
return return
} }
func (b *browserService) createRedisClient(ctx context.Context, selConn types.ConnectionConfig) (client redis.UniversalClient, err error) { func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (client redis.UniversalClient, err error) {
hook := redis2.NewHook(selConn.Name, func(cmd string, cost int64) { hook := redis2.NewHook(selConn.Name, func(cmd string, cost int64) {
now := time.Now() now := time.Now()
//last := strings.LastIndex(cmd, ":") //last := strings.LastIndex(cmd, ":")
@ -267,10 +268,10 @@ func (b *browserService) createRedisClient(ctx context.Context, selConn types.Co
return return
} }
_ = client.Do(ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err() _ = client.Do(b.ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err()
// add hook to each node in cluster mode // add hook to each node in cluster mode
if cluster, ok := client.(*redis.ClusterClient); ok { if cluster, ok := client.(*redis.ClusterClient); ok {
err = cluster.ForEachShard(ctx, func(ctx context.Context, cli *redis.Client) error { err = cluster.ForEachShard(b.ctx, func(ctx context.Context, cli *redis.Client) error {
cli.AddHook(hook) cli.AddHook(hook)
return nil return nil
}) })
@ -282,7 +283,7 @@ func (b *browserService) createRedisClient(ctx context.Context, selConn types.Co
client.AddHook(hook) client.AddHook(hook)
} }
if _, err = client.Ping(ctx).Result(); err != nil && !errors.Is(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()) err = errors.New("can not connect to redis server:" + err.Error())
return return
} }
@ -317,18 +318,13 @@ func (b *browserService) getRedisClient(server string, db int) (item *connection
err = fmt.Errorf("no match connection \"%s\"", server) err = fmt.Errorf("no match connection \"%s\"", server)
return return
} }
ctx, cancelFunc := context.WithCancel(b.ctx)
b.connMap[server] = &connectionItem{
ctx: ctx,
cancelFunc: cancelFunc,
}
var connConfig = selConn.ConnectionConfig var connConfig = selConn.ConnectionConfig
connConfig.LastDB = db connConfig.LastDB = db
client, err = b.createRedisClient(ctx, connConfig) client, err = b.createRedisClient(connConfig)
if err != nil { if err != nil {
return return
} }
ctx, cancelFunc := context.WithCancel(b.ctx)
item = &connectionItem{ item = &connectionItem{
client: client, client: client,
ctx: ctx, ctx: ctx,
@ -2013,13 +2009,21 @@ func (b *browserService) SetKeyTTL(server string, db int, k any, ttl int64) (res
// BatchSetTTL batch set ttl // BatchSetTTL batch set ttl
func (b *browserService) BatchSetTTL(server string, db int, ks []any, ttl int64, serialNo string) (resp types.JSResp) { func (b *browserService) BatchSetTTL(server string, db int, ks []any, ttl int64, serialNo string) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) conf := Connection().getConnection(server)
if err != nil { 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() resp.Msg = err.Error()
return return
} }
client := item.client
ctx, cancelFunc := context.WithCancel(b.ctx) ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc() defer cancelFunc()
//cancelEvent := "ttling:stop:" + serialNo //cancelEvent := "ttling:stop:" + serialNo
@ -2043,7 +2047,7 @@ func (b *browserService) BatchSetTTL(server string, db int, ks []any, ttl int64,
//} //}
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 { if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now() startTime = time.Now()
//runtime.EventsEmit(ctx, processEvent, param) //runtime.EventsEmit(b.ctx, processEvent, param)
// do some sleep to prevent blocking the Redis server // do some sleep to prevent blocking the Redis server
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
@ -2213,13 +2217,22 @@ func (b *browserService) DeleteOneKey(server string, db int, k any) (resp types.
// DeleteKeys delete keys sync with notification // DeleteKeys delete keys sync with notification
func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo string) (resp types.JSResp) { func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo string) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) // connect a new connection to export keys
if err != nil { 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() resp.Msg = err.Error()
return return
} }
client := item.client
ctx, cancelFunc := context.WithCancel(b.ctx) ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc() defer cancelFunc()
cancelEvent := "delete:stop:" + serialNo cancelEvent := "delete:stop:" + serialNo
@ -2281,13 +2294,21 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
// DeleteKeysByPattern delete keys by pattern // DeleteKeysByPattern delete keys by pattern
func (b *browserService) DeleteKeysByPattern(server string, db int, pattern string) (resp types.JSResp) { func (b *browserService) DeleteKeysByPattern(server string, db int, pattern string) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) conf := Connection().getConnection(server)
if err != nil { 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() resp.Msg = err.Error()
return return
} }
client := item.client
ctx, cancelFunc := context.WithCancel(b.ctx) ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc() defer cancelFunc()
var ks []any var ks []any
@ -2351,13 +2372,22 @@ func (b *browserService) DeleteKeysByPattern(server string, db int, pattern stri
// ExportKey export keys // ExportKey export keys
func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) { func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) // connect a new connection to export keys
if err != nil { 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() resp.Msg = err.Error()
return return
} }
client := item.client
ctx, cancelFunc := context.WithCancel(b.ctx) ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc() defer cancelFunc()
file, err := os.Create(path) file, err := os.Create(path)
@ -2426,13 +2456,22 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string,
// ImportCSV import data from csv file // ImportCSV import data from csv file
func (b *browserService) ImportCSV(server string, db int, path string, conflict int, ttl int64) (resp types.JSResp) { func (b *browserService) ImportCSV(server string, db int, path string, conflict int, ttl int64) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) // connect a new connection to export keys
if err != nil { 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() resp.Msg = err.Error()
return return
} }
client := item.client
ctx, cancelFunc := context.WithCancel(b.ctx) ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc() defer cancelFunc()
file, err := os.Open(path) file, err := os.Open(path)
@ -2514,7 +2553,7 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
"ignored": ignored, "ignored": ignored,
//"processing": string(key), //"processing": string(key),
} }
runtime.EventsEmit(ctx, processEvent, param) runtime.EventsEmit(b.ctx, processEvent, param)
// do some sleep to prevent blocking the Redis server // do some sleep to prevent blocking the Redis server
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }

View File

@ -3,10 +3,10 @@ package storage
import ( import (
"errors" "errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"slices"
"sync" "sync"
"tinyrdm/backend/consts" "tinyrdm/backend/consts"
"tinyrdm/backend/types" "tinyrdm/backend/types"
sliceutil "tinyrdm/backend/utils/slice"
) )
type ConnectionsStorage struct { type ConnectionsStorage struct {
@ -256,10 +256,10 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
conns := c.GetConnectionsFlat() conns := c.GetConnectionsFlat()
takeConn := func(name string) (types.Connection, bool) { takeConn := func(name string) (types.Connection, bool) {
idx := slices.IndexFunc(conns, func(connection types.Connection) bool { idx, ok := sliceutil.Find(conns, func(i int) bool {
return connection.Name == name return conns[i].Name == name
}) })
if idx >= 0 { if ok {
ret := conns[idx] ret := conns[idx]
conns = append(conns[:idx], conns[idx+1:]...) conns = append(conns[:idx], conns[idx+1:]...)
return ret, true return ret, true

View File

@ -1,11 +1,137 @@
package sliceutil package sliceutil
import ( import (
"sort"
"strconv"
"strings" "strings"
. "tinyrdm/backend/utils" . "tinyrdm/backend/utils"
) )
// Map map items to new array // Get 获取指定索引的值, 如果不存在则返回默认值
func Get[S ~[]T, T any](arr S, index int, defaultVal T) T {
if index < 0 || index >= len(arr) {
return defaultVal
}
return arr[index]
}
// Remove 删除指定索引的元素
func Remove[S ~[]T, T any](arr S, index int) S {
return append(arr[:index], arr[index+1:]...)
}
// RemoveIf 移除指定条件的元素
func RemoveIf[S ~[]T, T any](arr S, cond func(T) bool) S {
l := len(arr)
if l <= 0 {
return arr
}
for i := l - 1; i >= 0; i-- {
if cond(arr[i]) {
arr = append(arr[:i], arr[i+1:]...)
}
}
return arr
}
// RemoveRange 删除从[from, to]部分元素
func RemoveRange[S ~[]T, T any](arr S, from, to int) S {
return append(arr[:from], arr[to:]...)
}
// Find 查找指定条件的元素第一个出现位置
func Find[S ~[]T, T any](arr S, matchFunc func(int) bool) (int, bool) {
total := len(arr)
for i := 0; i < total; i++ {
if matchFunc(i) {
return i, true
}
}
return -1, false
}
// AnyMatch 判断是否有任意元素符合条件
func AnyMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
total := len(arr)
if total > 0 {
for i := 0; i < total; i++ {
if matchFunc(i) {
return true
}
}
}
return false
}
// AllMatch 判断是否所有元素都符合条件
func AllMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
total := len(arr)
for i := 0; i < total; i++ {
if !matchFunc(i) {
return false
}
}
return true
}
// Equals 比较两个切片内容是否完全一致
func Equals[S ~[]T, T comparable](arr1, arr2 S) bool {
if &arr1 == &arr2 {
return true
}
len1, len2 := len(arr1), len(arr2)
if len1 != len2 {
return false
}
for i := 0; i < len1; i++ {
if arr1[i] != arr2[i] {
return false
}
}
return true
}
// Contains 判断数组是否包含指定元素
func Contains[S ~[]T, T Hashable](arr S, elem T) bool {
return AnyMatch(arr, func(idx int) bool {
return arr[idx] == elem
})
}
// ContainsAny 判断数组是否包含任意指定元素
func ContainsAny[S ~[]T, T Hashable](arr S, elems ...T) bool {
for _, elem := range elems {
if Contains(arr, elem) {
return true
}
}
return false
}
// ContainsAll 判断数组是否包含所有指定元素
func ContainsAll[S ~[]T, T Hashable](arr S, elems ...T) bool {
for _, elem := range elems {
if !Contains(arr, elem) {
return false
}
}
return true
}
// Filter 筛选出符合指定条件的所有元素
func Filter[S ~[]T, T any](arr S, filterFunc func(int) bool) []T {
total := len(arr)
var result []T
for i := 0; i < total; i++ {
if filterFunc(i) {
result = append(result, arr[i])
}
}
return result
}
// Map 数组映射转换
func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R { func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
total := len(arr) total := len(arr)
result := make([]R, total) result := make([]R, total)
@ -15,7 +141,7 @@ func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
return result return result
} }
// FilterMap filter and map items to new array // FilterMap 数组过滤和映射转换
func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R { func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R {
total := len(arr) total := len(arr)
result := make([]R, 0, total) result := make([]R, 0, total)
@ -29,7 +155,68 @@ func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R
return result return result
} }
// Join join any array to a single string by custom function // ToMap 数组转键值对
func ToMap[S ~[]T, T any, K Hashable, V any](arr S, mappingFunc func(int) (K, V)) map[K]V {
total := len(arr)
result := map[K]V{}
for i := 0; i < total; i++ {
key, val := mappingFunc(i)
result[key] = val
}
return result
}
// Flat 二维数组扁平化
func Flat[T any](arr [][]T) []T {
total := len(arr)
var result []T
for i := 0; i < total; i++ {
subTotal := len(arr[i])
for j := 0; j < subTotal; j++ {
result = append(result, arr[i][j])
}
}
return result
}
// FlatMap 二维数组扁平化映射
func FlatMap[T any, R any](arr [][]T, mappingFunc func(int, int) R) []R {
total := len(arr)
var result []R
for i := 0; i < total; i++ {
subTotal := len(arr[i])
for j := 0; j < subTotal; j++ {
result = append(result, mappingFunc(i, j))
}
}
return result
}
func FlatValueMap[T Hashable](arr [][]T) []T {
return FlatMap(arr, func(i, j int) T {
return arr[i][j]
})
}
// Reduce 数组累计
func Reduce[S ~[]T, T any, R any](arr S, init R, reduceFunc func(R, T) R) R {
result := init
for _, item := range arr {
result = reduceFunc(result, item)
}
return result
}
// Reverse 反转数组(会修改原数组)
func Reverse[S ~[]T, T any](arr S) S {
total := len(arr)
for i := 0; i < total/2; i++ {
arr[i], arr[total-i-1] = arr[total-i-1], arr[i]
}
return arr
}
// Join 数组拼接转字符串
func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) string { func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) string {
total := len(arr) total := len(arr)
if total <= 0 { if total <= 0 {
@ -49,14 +236,21 @@ func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) strin
return sb.String() return sb.String()
} }
// JoinString join string array to a single string // JoinString 字符串数组拼接成字符串
func JoinString(arr []string, sep string) string { func JoinString(arr []string, sep string) string {
return Join(arr, sep, func(idx int) string { return Join(arr, sep, func(idx int) string {
return arr[idx] return arr[idx]
}) })
} }
// Unique filter unique item // JoinInt 整形数组拼接转字符串
func JoinInt(arr []int, sep string) string {
return Join(arr, sep, func(idx int) string {
return strconv.Itoa(arr[idx])
})
}
// Unique 数组去重
func Unique[S ~[]T, T Hashable](arr S) S { func Unique[S ~[]T, T Hashable](arr S) S {
result := make(S, 0, len(arr)) result := make(S, 0, len(arr))
uniKeys := map[T]struct{}{} uniKeys := map[T]struct{}{}
@ -69,3 +263,136 @@ func Unique[S ~[]T, T Hashable](arr S) S {
} }
return result return result
} }
// UniqueEx 数组去重(任意类型)
// @param toKeyFunc 数组元素转为唯一标识字符串函数, 如转为哈希值等
func UniqueEx[S ~[]T, T any](arr S, toKeyFunc func(i int) string) S {
result := make(S, 0, len(arr))
keyArr := Map(arr, toKeyFunc)
uniKeys := map[string]struct{}{}
var exists bool
for i, item := range arr {
if _, exists = uniKeys[keyArr[i]]; !exists {
uniKeys[keyArr[i]] = struct{}{}
result = append(result, item)
}
}
return result
}
// Sort 顺序排序(会修改原数组)
func Sort[S ~[]T, T Hashable](arr S) S {
sort.Slice(arr, func(i, j int) bool {
return arr[i] <= arr[j]
})
return arr
}
// SortDesc 倒序排序(会修改原数组)
func SortDesc[S ~[]T, T Hashable](arr S) S {
sort.Slice(arr, func(i, j int) bool {
return arr[i] > arr[j]
})
return arr
}
// Union 返回两个切片共同拥有的元素
func Union[S ~[]T, T Hashable](arr1 S, arr2 S) S {
hashArr, compArr := arr1, arr2
if len(arr1) < len(arr2) {
hashArr, compArr = compArr, hashArr
}
hash := map[T]struct{}{}
for _, item := range hashArr {
hash[item] = struct{}{}
}
uniq := map[T]struct{}{}
ret := make(S, 0, len(compArr))
exists := false
for _, item := range compArr {
if _, exists = hash[item]; exists {
if _, exists = uniq[item]; !exists {
ret = append(ret, item)
uniq[item] = struct{}{}
}
}
}
return ret
}
// Exclude 返回不包含的元素
func Exclude[S ~[]T, T Hashable](arr1 S, arr2 S) S {
diff := make([]T, 0, len(arr1))
hash := map[T]struct{}{}
for _, item := range arr2 {
hash[item] = struct{}{}
}
for _, item := range arr1 {
if _, exists := hash[item]; !exists {
diff = append(diff, item)
}
}
return diff
}
// PadLeft 左边填充指定数量
func PadLeft[S ~[]T, T any](arr S, val T, count int) S {
prefix := make(S, count)
for i := 0; i < count; i++ {
prefix[i] = val
}
arr = append(prefix, arr...)
return arr
}
// PadRight 右边填充指定数量
func PadRight[S ~[]T, T any](arr S, val T, count int) S {
for i := 0; i < count; i++ {
arr = append(arr, val)
}
return arr
}
// RemoveLeft 移除左侧相同元素
func RemoveLeft[S ~[]T, T comparable](arr S, val T) S {
for len(arr) > 0 && arr[0] == val {
arr = arr[1:]
}
return arr
}
// RemoveRight 移除右侧相同元素
func RemoveRight[S ~[]T, T comparable](arr S, val T) S {
for {
length := len(arr)
if length > 0 && arr[length-1] == val {
arr = arr[:length]
} else {
break
}
}
return arr
}
// Count 统计制定条件元素数量
func Count[S ~[]T, T any](arr S, filter func(int) bool) int {
count := 0
for i := range arr {
if filter(i) {
count += 1
}
}
return count
}
// Group 根据分组函数对数组进行分组汇总
func Group[S ~[]T, T any, K Hashable, R any](arr S, groupFunc func(int) (K, R)) map[K][]R {
ret := map[K][]R{}
for i := range arr {
key, val := groupFunc(i)
ret[key] = append(ret[key], val)
}
return ret
}

View File

@ -7,13 +7,12 @@ import useBrowserStore from 'stores/browser.js'
import Play from '@/components/icons/Play.vue' import Play from '@/components/icons/Play.vue'
import Pause from '@/components/icons/Pause.vue' import Pause from '@/components/icons/Pause.vue'
import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js' import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js'
import { EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js' import { ClipboardSetText, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
import Copy from '@/components/icons/Copy.vue' import Copy from '@/components/icons/Copy.vue'
import Export from '@/components/icons/Export.vue' import Export from '@/components/icons/Export.vue'
import Delete from '@/components/icons/Delete.vue' import Delete from '@/components/icons/Delete.vue'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import Bottom from '@/components/icons/Bottom.vue' import Bottom from '@/components/icons/Bottom.vue'
import copy from 'copy-text-to-clipboard'
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -96,9 +95,16 @@ const onStopMonitor = async () => {
} }
const onCopyLog = async () => { const onCopyLog = async () => {
copy(join(data.list, '\n')) try {
const content = join(data.list, '\n')
const succ = await ClipboardSetText(content)
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
} }
} catch (e) {
$message.error(e.message)
}
}
const onExportLog = () => { const onExportLog = () => {
ExportLog(data.list) ExportLog(data.list)

View File

@ -9,12 +9,12 @@ import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import Copy from '@/components/icons/Copy.vue' import Copy from '@/components/icons/Copy.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { computed, onMounted, onUnmounted, reactive, watch } from 'vue' import { computed, onMounted, onUnmounted, reactive, watch } from 'vue'
import { NIcon, useThemeVars } from 'naive-ui' import { NIcon, useThemeVars } from 'naive-ui'
import { timeout } from '@/utils/promise.js' import { timeout } from '@/utils/promise.js'
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue' import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
import { toHumanReadable } from '@/utils/date.js' import { toHumanReadable } from '@/utils/date.js'
import copy from 'copy-text-to-clipboard'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -139,9 +139,16 @@ const onToggleRefresh = (on) => {
} }
const onCopyKey = () => { const onCopyKey = () => {
copy(props.keyPath) ClipboardSetText(props.keyPath)
.then((succ) => {
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
} }
})
.catch((e) => {
$message.error(e.message)
})
}
const onTTL = () => { const onTTL = () => {
dialogStore.openTTLDialog({ dialogStore.openTTLDialog({

View File

@ -17,8 +17,8 @@ import Edit from '@/components/icons/Edit.vue'
import FormatSelector from '@/components/content_value/FormatSelector.vue' import FormatSelector from '@/components/content_value/FormatSelector.vue'
import { decodeRedisKey } from '@/utils/key_convert.js' import { decodeRedisKey } from '@/utils/key_convert.js'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue' import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { formatBytes } from '@/utils/byte_convert.js' import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -230,8 +230,14 @@ const actionColumn = {
} }
}, },
onCopy: async () => { onCopy: async () => {
copy(row.v) try {
const succ = await ClipboardSetText(row.v)
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
}
} catch (e) {
$message.error(e.message)
}
}, },
onEdit: () => startEdit(index + 1, row.k, row.v), onEdit: () => startEdit(index + 1, row.k, row.v),
onDelete: async () => { onDelete: async () => {

View File

@ -5,13 +5,13 @@ import Copy from '@/components/icons/Copy.vue'
import Save from '@/components/icons/Save.vue' import Save from '@/components/icons/Save.vue'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import { types as redisTypes } from '@/consts/support_redis_type.js' import { types as redisTypes } from '@/consts/support_redis_type.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { isEmpty, toLower } from 'lodash' import { isEmpty, toLower } from 'lodash'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { decodeRedisKey } from '@/utils/key_convert.js' import { decodeRedisKey } from '@/utils/key_convert.js'
import ContentEditor from '@/components/content_value/ContentEditor.vue' import ContentEditor from '@/components/content_value/ContentEditor.vue'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { formatBytes } from '@/utils/byte_convert.js' import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
const props = defineProps({ const props = defineProps({
name: String, name: String,
@ -62,9 +62,16 @@ const showMemoryUsage = computed(() => {
* Copy value * Copy value
*/ */
const onCopyValue = () => { const onCopyValue = () => {
copy(displayValue.value) ClipboardSetText(displayValue.value)
.then((succ) => {
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
} }
})
.catch((e) => {
$message.error(e.message)
})
}
/** /**
* Save value * Save value

View File

@ -16,8 +16,8 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
import FormatSelector from '@/components/content_value/FormatSelector.vue' import FormatSelector from '@/components/content_value/FormatSelector.vue'
import Edit from '@/components/icons/Edit.vue' import Edit from '@/components/icons/Edit.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue' import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { formatBytes } from '@/utils/byte_convert.js' import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -180,8 +180,14 @@ const actionColumn = {
editing: false, editing: false,
bindKey: `#${index + 1}`, bindKey: `#${index + 1}`,
onCopy: async () => { onCopy: async () => {
copy(row.v) try {
const succ = await ClipboardSetText(row.v)
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
}
} catch (e) {
$message.error(e.message)
}
}, },
onEdit: () => { onEdit: () => {
startEdit(index + 1, row.v) startEdit(index + 1, row.v)

View File

@ -16,8 +16,8 @@ import Edit from '@/components/icons/Edit.vue'
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue' import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
import FormatSelector from '@/components/content_value/FormatSelector.vue' import FormatSelector from '@/components/content_value/FormatSelector.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue' import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { formatBytes } from '@/utils/byte_convert.js' import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -177,8 +177,14 @@ const actionColumn = {
editing: false, editing: false,
bindKey: `#${index + 1}`, bindKey: `#${index + 1}`,
onCopy: async () => { onCopy: async () => {
copy(row.v) try {
const succ = await ClipboardSetText(row.v)
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
}
} catch (e) {
$message.error(e.message)
}
}, },
onEdit: () => { onEdit: () => {
startEdit(index + 1, row.v) startEdit(index + 1, row.v)

View File

@ -13,8 +13,8 @@ import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue' import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue' import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { formatBytes } from '@/utils/byte_convert.js' import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -108,8 +108,14 @@ const actionColumn = {
bindKey: row.id, bindKey: row.id,
readonly: true, readonly: true,
onCopy: async () => { onCopy: async () => {
copy(JSON.stringify(row.v)) try {
const succ = await ClipboardSetText(JSON.stringify(row.v))
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
}
} catch (e) {
$message.error(e.message)
}
}, },
onDelete: async () => { onDelete: async () => {
try { try {

View File

@ -6,13 +6,13 @@ import Save from '@/components/icons/Save.vue'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import { formatTypes } from '@/consts/value_view_type.js' import { formatTypes } from '@/consts/value_view_type.js'
import { types as redisTypes } from '@/consts/support_redis_type.js' import { types as redisTypes } from '@/consts/support_redis_type.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { isEmpty, toLower } from 'lodash' import { isEmpty, toLower } from 'lodash'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { decodeRedisKey } from '@/utils/key_convert.js' import { decodeRedisKey } from '@/utils/key_convert.js'
import FormatSelector from '@/components/content_value/FormatSelector.vue' import FormatSelector from '@/components/content_value/FormatSelector.vue'
import ContentEditor from '@/components/content_value/ContentEditor.vue' import ContentEditor from '@/components/content_value/ContentEditor.vue'
import { formatBytes } from '@/utils/byte_convert.js' import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
const props = defineProps({ const props = defineProps({
name: String, name: String,
@ -121,9 +121,16 @@ const onFormatChanged = async (decode = '', format = '') => {
* Copy value * Copy value
*/ */
const onCopyValue = () => { const onCopyValue = () => {
copy(displayValue.value) ClipboardSetText(displayValue.value)
.then((succ) => {
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
} }
})
.catch((e) => {
$message.error(e.message)
})
}
/** /**
* Save value * Save value

View File

@ -16,8 +16,8 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
import FormatSelector from '@/components/content_value/FormatSelector.vue' import FormatSelector from '@/components/content_value/FormatSelector.vue'
import Edit from '@/components/icons/Edit.vue' import Edit from '@/components/icons/Edit.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue' import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { formatBytes } from '@/utils/byte_convert.js' import { formatBytes } from '@/utils/byte_convert.js'
import copy from 'copy-text-to-clipboard'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -224,8 +224,14 @@ const actionColumn = {
editing: false, editing: false,
bindKey: row.v, bindKey: row.v,
onCopy: async () => { onCopy: async () => {
copy(row.v) try {
const succ = await ClipboardSetText(row.v)
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
}
} catch (e) {
$message.error(e.message)
}
}, },
onEdit: () => startEdit(index + 1, row.s, row.v), onEdit: () => startEdit(index + 1, row.s, row.v),
onDelete: async () => { onDelete: async () => {

View File

@ -507,7 +507,7 @@ const pasteFromClipboard = async () => {
</n-radio-group> </n-radio-group>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
v-if="generalForm.dbFilterType !== 'none'" v-show="generalForm.dbFilterType !== 'none'"
:label="$t('dialogue.connection.advn.dbfilter_input')" :label="$t('dialogue.connection.advn.dbfilter_input')"
:span="24"> :span="24">
<n-select <n-select

View File

@ -130,7 +130,7 @@ const onClose = () => {
required> required>
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" /> <n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
</n-form-item> </n-form-item>
<n-checkbox v-if="!deleteForm.showAffected" v-model:checked="deleteForm.direct"> <n-checkbox v-model:checked="deleteForm.direct">
{{ $t('dialogue.key.direct_delete') }} {{ $t('dialogue.key.direct_delete') }}
</n-checkbox> </n-checkbox>
<n-card <n-card

View File

@ -13,6 +13,7 @@ import Add from '@/components/icons/Add.vue'
import Layer from '@/components/icons/Layer.vue' import Layer from '@/components/icons/Layer.vue'
import Delete from '@/components/icons/Delete.vue' import Delete from '@/components/icons/Delete.vue'
import useDialogStore from 'stores/dialog.js' import useDialogStore from 'stores/dialog.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import useTabStore from 'stores/tab.js' import useTabStore from 'stores/tab.js'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
@ -26,7 +27,6 @@ import usePreferencesStore from 'stores/preferences.js'
import { typesIconStyle } from '@/consts/support_redis_type.js' import { typesIconStyle } from '@/consts/support_redis_type.js'
import { nativeRedisKey } from '@/utils/key_convert.js' import { nativeRedisKey } from '@/utils/key_convert.js'
import { isMacOS } from '@/utils/platform.js' import { isMacOS } from '@/utils/platform.js'
import copy from 'copy-text-to-clipboard'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -321,9 +321,16 @@ const handleKeyCopy = () => {
} }
if (node.type === ConnectionType.RedisValue) { if (node.type === ConnectionType.RedisValue) {
copy(nativeRedisKey(node.redisKeyCode || node.redisKey)) ClipboardSetText(nativeRedisKey(node.redisKeyCode || node.redisKey))
.then((succ) => {
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
} }
})
.catch((e) => {
$message.error(e.message)
})
}
} }
const onKeyShortcut = (e) => { const onKeyShortcut = (e) => {
@ -406,8 +413,15 @@ const handleSelectContextMenu = (action) => {
break break
case 'key_copy': case 'key_copy':
case 'value_copy': case 'value_copy':
copy(nativeRedisKey(redisKey)) ClipboardSetText(nativeRedisKey(redisKey))
.then((succ) => {
if (succ) {
$message.success(i18n.t('interface.copy_succ')) $message.success(i18n.t('interface.copy_succ'))
}
})
.catch((e) => {
$message.error(e.message)
})
break break
case 'db_loadall': case 'db_loadall':
if (node != null && !!!node.loading) { if (node != null && !!!node.loading) {

View File

@ -437,7 +437,7 @@ const findSiblingsAndIndex = (node, nodes) => {
} }
// delay save until drop stopped after 2 seconds // delay save until drop stopped after 2 seconds
const saveSort = debounce(connectionStore.saveConnectionSorted, 1500, { trailing: true }) const saveSort = debounce(connectionStore.saveConnectionSorted, 2000, { trailing: true })
const handleDrop = ({ node, dragNode, dropPosition }) => { const handleDrop = ({ node, dragNode, dropPosition }) => {
const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections) const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)
if (dragNodeSiblings === null || dragNodeIndex === null) { if (dragNodeSiblings === null || dragNodeIndex === null) {