Compare commits
4 Commits
8c30daec15
...
bdfa31e4b6
Author | SHA1 | Date |
---|---|---|
Lykin | bdfa31e4b6 | |
Lykin | aa8c5495c1 | |
Lykin | 65cfdd1bcc | |
Lykin | 6bd1b23a64 |
|
@ -98,9 +98,9 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
|
|||
selConn := Connection().getConnection(name)
|
||||
// correct last database index
|
||||
lastDB := selConn.LastDB
|
||||
if selConn.DBFilterType == "show" && !sliceutil.Contains(selConn.DBFilterList, lastDB) {
|
||||
if selConn.DBFilterType == "show" && !slices.Contains(selConn.DBFilterList, lastDB) {
|
||||
lastDB = selConn.DBFilterList[0]
|
||||
} else if selConn.DBFilterType == "hide" && sliceutil.Contains(selConn.DBFilterList, lastDB) {
|
||||
} else if selConn.DBFilterType == "hide" && slices.Contains(selConn.DBFilterList, lastDB) {
|
||||
lastDB = selConn.DBFilterList[0]
|
||||
}
|
||||
if lastDB != selConn.LastDB {
|
||||
|
@ -233,13 +233,12 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
|
|||
|
||||
// CloseConnection close redis server connection
|
||||
func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
|
||||
item, ok := b.connMap[name]
|
||||
if ok {
|
||||
if item, ok := b.connMap[name]; ok {
|
||||
delete(b.connMap, name)
|
||||
if item.client != nil {
|
||||
if item.cancelFunc != nil {
|
||||
item.cancelFunc()
|
||||
}
|
||||
if item.client != nil {
|
||||
item.client.Close()
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +246,7 @@ func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
|
|||
return
|
||||
}
|
||||
|
||||
func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (client redis.UniversalClient, err error) {
|
||||
func (b *browserService) createRedisClient(ctx context.Context, 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, ":")
|
||||
|
@ -268,10 +267,10 @@ func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (clie
|
|||
return
|
||||
}
|
||||
|
||||
_ = client.Do(b.ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err()
|
||||
_ = client.Do(ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err()
|
||||
// add hook to each node in cluster mode
|
||||
if cluster, ok := client.(*redis.ClusterClient); ok {
|
||||
err = cluster.ForEachShard(b.ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
err = cluster.ForEachShard(ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
cli.AddHook(hook)
|
||||
return nil
|
||||
})
|
||||
|
@ -283,7 +282,7 @@ func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (clie
|
|||
client.AddHook(hook)
|
||||
}
|
||||
|
||||
if _, err = client.Ping(b.ctx).Result(); err != nil && !errors.Is(err, redis.Nil) {
|
||||
if _, err = client.Ping(ctx).Result(); err != nil && !errors.Is(err, redis.Nil) {
|
||||
err = errors.New("can not connect to redis server:" + err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -318,13 +317,18 @@ func (b *browserService) getRedisClient(server string, db int) (item *connection
|
|||
err = fmt.Errorf("no match connection \"%s\"", server)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
b.connMap[server] = &connectionItem{
|
||||
ctx: ctx,
|
||||
cancelFunc: cancelFunc,
|
||||
}
|
||||
var connConfig = selConn.ConnectionConfig
|
||||
connConfig.LastDB = db
|
||||
client, err = b.createRedisClient(connConfig)
|
||||
client, err = b.createRedisClient(ctx, connConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
item = &connectionItem{
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
|
@ -2009,21 +2013,13 @@ func (b *browserService) SetKeyTTL(server string, db int, k any, ttl int64) (res
|
|||
|
||||
// BatchSetTTL batch set ttl
|
||||
func (b *browserService) BatchSetTTL(server string, db int, ks []any, ttl int64, serialNo string) (resp types.JSResp) {
|
||||
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 {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
//cancelEvent := "ttling:stop:" + serialNo
|
||||
|
@ -2047,7 +2043,7 @@ func (b *browserService) BatchSetTTL(server string, db int, ks []any, ttl int64,
|
|||
//}
|
||||
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
|
||||
startTime = time.Now()
|
||||
//runtime.EventsEmit(b.ctx, processEvent, param)
|
||||
//runtime.EventsEmit(ctx, processEvent, param)
|
||||
// do some sleep to prevent blocking the Redis server
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
@ -2217,22 +2213,13 @@ func (b *browserService) DeleteOneKey(server string, db int, k any) (resp types.
|
|||
|
||||
// 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 {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
cancelEvent := "delete:stop:" + serialNo
|
||||
|
@ -2294,21 +2281,13 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
|
|||
|
||||
// DeleteKeysByPattern delete keys by pattern
|
||||
func (b *browserService) DeleteKeysByPattern(server string, db int, pattern string) (resp types.JSResp) {
|
||||
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 {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
var ks []any
|
||||
|
@ -2372,22 +2351,13 @@ func (b *browserService) DeleteKeysByPattern(server string, db int, pattern stri
|
|||
|
||||
// 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 {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
file, err := os.Create(path)
|
||||
|
@ -2456,22 +2426,13 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string,
|
|||
|
||||
// ImportCSV import data from csv file
|
||||
func (b *browserService) ImportCSV(server string, db int, path string, conflict int, ttl int64) (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 {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
file, err := os.Open(path)
|
||||
|
@ -2553,7 +2514,7 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
|
|||
"ignored": ignored,
|
||||
//"processing": string(key),
|
||||
}
|
||||
runtime.EventsEmit(b.ctx, processEvent, param)
|
||||
runtime.EventsEmit(ctx, processEvent, param)
|
||||
// do some sleep to prevent blocking the Redis server
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ package storage
|
|||
import (
|
||||
"errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"slices"
|
||||
"sync"
|
||||
"tinyrdm/backend/consts"
|
||||
"tinyrdm/backend/types"
|
||||
sliceutil "tinyrdm/backend/utils/slice"
|
||||
)
|
||||
|
||||
type ConnectionsStorage struct {
|
||||
|
@ -256,10 +256,10 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
|
|||
|
||||
conns := c.GetConnectionsFlat()
|
||||
takeConn := func(name string) (types.Connection, bool) {
|
||||
idx, ok := sliceutil.Find(conns, func(i int) bool {
|
||||
return conns[i].Name == name
|
||||
idx := slices.IndexFunc(conns, func(connection types.Connection) bool {
|
||||
return connection.Name == name
|
||||
})
|
||||
if ok {
|
||||
if idx >= 0 {
|
||||
ret := conns[idx]
|
||||
conns = append(conns[:idx], conns[idx+1:]...)
|
||||
return ret, true
|
||||
|
|
|
@ -1,137 +1,11 @@
|
|||
package sliceutil
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
. "tinyrdm/backend/utils"
|
||||
)
|
||||
|
||||
// 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 数组映射转换
|
||||
// Map map items to new array
|
||||
func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
||||
total := len(arr)
|
||||
result := make([]R, total)
|
||||
|
@ -141,7 +15,7 @@ func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
|||
return result
|
||||
}
|
||||
|
||||
// FilterMap 数组过滤和映射转换
|
||||
// FilterMap filter and map items to new array
|
||||
func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R {
|
||||
total := len(arr)
|
||||
result := make([]R, 0, total)
|
||||
|
@ -155,68 +29,7 @@ func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R
|
|||
return result
|
||||
}
|
||||
|
||||
// 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 数组拼接转字符串
|
||||
// Join join any array to a single string by custom function
|
||||
func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) string {
|
||||
total := len(arr)
|
||||
if total <= 0 {
|
||||
|
@ -236,21 +49,14 @@ func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) strin
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// JoinString 字符串数组拼接成字符串
|
||||
// JoinString join string array to a single string
|
||||
func JoinString(arr []string, sep string) string {
|
||||
return Join(arr, sep, func(idx int) string {
|
||||
return arr[idx]
|
||||
})
|
||||
}
|
||||
|
||||
// JoinInt 整形数组拼接转字符串
|
||||
func JoinInt(arr []int, sep string) string {
|
||||
return Join(arr, sep, func(idx int) string {
|
||||
return strconv.Itoa(arr[idx])
|
||||
})
|
||||
}
|
||||
|
||||
// Unique 数组去重
|
||||
// Unique filter unique item
|
||||
func Unique[S ~[]T, T Hashable](arr S) S {
|
||||
result := make(S, 0, len(arr))
|
||||
uniKeys := map[T]struct{}{}
|
||||
|
@ -263,136 +69,3 @@ func Unique[S ~[]T, T Hashable](arr S) S {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -7,12 +7,13 @@ import useBrowserStore from 'stores/browser.js'
|
|||
import Play from '@/components/icons/Play.vue'
|
||||
import Pause from '@/components/icons/Pause.vue'
|
||||
import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js'
|
||||
import { ClipboardSetText, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||
import { EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||
import Copy from '@/components/icons/Copy.vue'
|
||||
import Export from '@/components/icons/Export.vue'
|
||||
import Delete from '@/components/icons/Delete.vue'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import Bottom from '@/components/icons/Bottom.vue'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const themeVars = useThemeVars()
|
||||
|
||||
|
@ -95,15 +96,8 @@ const onStopMonitor = async () => {
|
|||
}
|
||||
|
||||
const onCopyLog = async () => {
|
||||
try {
|
||||
const content = join(data.list, '\n')
|
||||
const succ = await ClipboardSetText(content)
|
||||
if (succ) {
|
||||
copy(join(data.list, '\n'))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const onExportLog = () => {
|
||||
|
|
|
@ -9,12 +9,12 @@ import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import Copy from '@/components/icons/Copy.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { computed, onMounted, onUnmounted, reactive, watch } from 'vue'
|
||||
import { NIcon, useThemeVars } from 'naive-ui'
|
||||
import { timeout } from '@/utils/promise.js'
|
||||
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
||||
import { toHumanReadable } from '@/utils/date.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -139,15 +139,8 @@ const onToggleRefresh = (on) => {
|
|||
}
|
||||
|
||||
const onCopyKey = () => {
|
||||
ClipboardSetText(props.keyPath)
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
copy(props.keyPath)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const onTTL = () => {
|
||||
|
|
|
@ -17,8 +17,8 @@ import Edit from '@/components/icons/Edit.vue'
|
|||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -230,14 +230,8 @@ const actionColumn = {
|
|||
}
|
||||
},
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
copy(row.v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
},
|
||||
onEdit: () => startEdit(index + 1, row.k, row.v),
|
||||
onDelete: async () => {
|
||||
|
|
|
@ -5,13 +5,13 @@ import Copy from '@/components/icons/Copy.vue'
|
|||
import Save from '@/components/icons/Save.vue'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { types as redisTypes } from '@/consts/support_redis_type.js'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { isEmpty, toLower } from 'lodash'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
|
@ -62,15 +62,8 @@ const showMemoryUsage = computed(() => {
|
|||
* Copy value
|
||||
*/
|
||||
const onCopyValue = () => {
|
||||
ClipboardSetText(displayValue.value)
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
copy(displayValue.value)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,8 +16,8 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
|
|||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import Edit from '@/components/icons/Edit.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -180,14 +180,8 @@ const actionColumn = {
|
|||
editing: false,
|
||||
bindKey: `#${index + 1}`,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
copy(row.v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
},
|
||||
onEdit: () => {
|
||||
startEdit(index + 1, row.v)
|
||||
|
|
|
@ -16,8 +16,8 @@ import Edit from '@/components/icons/Edit.vue'
|
|||
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -177,14 +177,8 @@ const actionColumn = {
|
|||
editing: false,
|
||||
bindKey: `#${index + 1}`,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
copy(row.v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
},
|
||||
onEdit: () => {
|
||||
startEdit(index + 1, row.v)
|
||||
|
|
|
@ -13,8 +13,8 @@ import LoadList from '@/components/icons/LoadList.vue'
|
|||
import LoadAll from '@/components/icons/LoadAll.vue'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -108,14 +108,8 @@ const actionColumn = {
|
|||
bindKey: row.id,
|
||||
readonly: true,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(JSON.stringify(row.v))
|
||||
if (succ) {
|
||||
copy(JSON.stringify(row.v))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
},
|
||||
onDelete: async () => {
|
||||
try {
|
||||
|
|
|
@ -6,13 +6,13 @@ import Save from '@/components/icons/Save.vue'
|
|||
import { useThemeVars } from 'naive-ui'
|
||||
import { formatTypes } from '@/consts/value_view_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 useBrowserStore from 'stores/browser.js'
|
||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
|
@ -121,15 +121,8 @@ const onFormatChanged = async (decode = '', format = '') => {
|
|||
* Copy value
|
||||
*/
|
||||
const onCopyValue = () => {
|
||||
ClipboardSetText(displayValue.value)
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
copy(displayValue.value)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,8 +16,8 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
|
|||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import Edit from '@/components/icons/Edit.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -224,14 +224,8 @@ const actionColumn = {
|
|||
editing: false,
|
||||
bindKey: row.v,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
copy(row.v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
},
|
||||
onEdit: () => startEdit(index + 1, row.s, row.v),
|
||||
onDelete: async () => {
|
||||
|
|
|
@ -507,7 +507,7 @@ const pasteFromClipboard = async () => {
|
|||
</n-radio-group>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
v-show="generalForm.dbFilterType !== 'none'"
|
||||
v-if="generalForm.dbFilterType !== 'none'"
|
||||
:label="$t('dialogue.connection.advn.dbfilter_input')"
|
||||
:span="24">
|
||||
<n-select
|
||||
|
|
|
@ -130,7 +130,7 @@ const onClose = () => {
|
|||
required>
|
||||
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
||||
</n-form-item>
|
||||
<n-checkbox v-model:checked="deleteForm.direct">
|
||||
<n-checkbox v-if="!deleteForm.showAffected" v-model:checked="deleteForm.direct">
|
||||
{{ $t('dialogue.key.direct_delete') }}
|
||||
</n-checkbox>
|
||||
<n-card
|
||||
|
|
|
@ -13,7 +13,6 @@ import Add from '@/components/icons/Add.vue'
|
|||
import Layer from '@/components/icons/Layer.vue'
|
||||
import Delete from '@/components/icons/Delete.vue'
|
||||
import useDialogStore from 'stores/dialog.js'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import useConnectionStore from 'stores/connections.js'
|
||||
import useTabStore from 'stores/tab.js'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
|
@ -27,6 +26,7 @@ import usePreferencesStore from 'stores/preferences.js'
|
|||
import { typesIconStyle } from '@/consts/support_redis_type.js'
|
||||
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||
import { isMacOS } from '@/utils/platform.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -321,16 +321,9 @@ const handleKeyCopy = () => {
|
|||
}
|
||||
|
||||
if (node.type === ConnectionType.RedisValue) {
|
||||
ClipboardSetText(nativeRedisKey(node.redisKeyCode || node.redisKey))
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
copy(nativeRedisKey(node.redisKeyCode || node.redisKey))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyShortcut = (e) => {
|
||||
|
@ -413,15 +406,8 @@ const handleSelectContextMenu = (action) => {
|
|||
break
|
||||
case 'key_copy':
|
||||
case 'value_copy':
|
||||
ClipboardSetText(nativeRedisKey(redisKey))
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
copy(nativeRedisKey(redisKey))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
break
|
||||
case 'db_loadall':
|
||||
if (node != null && !!!node.loading) {
|
||||
|
|
|
@ -437,7 +437,7 @@ const findSiblingsAndIndex = (node, nodes) => {
|
|||
}
|
||||
|
||||
// delay save until drop stopped after 2 seconds
|
||||
const saveSort = debounce(connectionStore.saveConnectionSorted, 2000, { trailing: true })
|
||||
const saveSort = debounce(connectionStore.saveConnectionSorted, 1500, { trailing: true })
|
||||
const handleDrop = ({ node, dragNode, dropPosition }) => {
|
||||
const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)
|
||||
if (dragNodeSiblings === null || dragNodeIndex === null) {
|
||||
|
|
Loading…
Reference in New Issue