Compare commits

..

7 Commits

24 changed files with 499 additions and 177 deletions

View File

@ -483,8 +483,8 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli
} }
// LoadNextKeys load next key from saved cursor // LoadNextKeys load next key from saved cursor
func (b *browserService) LoadNextKeys(connName string, db int, match, keyType string) (resp types.JSResp) { func (b *browserService) LoadNextKeys(server string, db int, match, keyType string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -497,7 +497,7 @@ func (b *browserService) LoadNextKeys(connName string, db int, match, keyType st
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
b.setClientCursor(connName, db, cursor) b.setClientCursor(server, db, cursor)
maxKeys := b.loadDBSize(ctx, client) maxKeys := b.loadDBSize(ctx, client)
resp.Success = true resp.Success = true
@ -510,8 +510,8 @@ func (b *browserService) LoadNextKeys(connName string, db int, match, keyType st
} }
// LoadNextAllKeys load next all keys // LoadNextAllKeys load next all keys
func (b *browserService) LoadNextAllKeys(connName string, db int, match, keyType string) (resp types.JSResp) { func (b *browserService) LoadNextAllKeys(server string, db int, match, keyType string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -524,7 +524,7 @@ func (b *browserService) LoadNextAllKeys(connName string, db int, match, keyType
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
b.setClientCursor(connName, db, 0) b.setClientCursor(server, db, 0)
maxKeys := b.loadDBSize(ctx, client) maxKeys := b.loadDBSize(ctx, client)
resp.Success = true resp.Success = true
@ -536,8 +536,8 @@ func (b *browserService) LoadNextAllKeys(connName string, db int, match, keyType
} }
// LoadAllKeys load all keys // LoadAllKeys load all keys
func (b *browserService) LoadAllKeys(connName string, db int, match, keyType string) (resp types.JSResp) { func (b *browserService) LoadAllKeys(server string, db int, match, keyType string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1320,8 +1320,8 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe
} }
// AddHashField add or update hash field // AddHashField add or update hash field
func (b *browserService) AddHashField(connName string, db int, k any, action int, fieldItems []any) (resp types.JSResp) { func (b *browserService) AddHashField(server string, db int, k any, action int, fieldItems []any) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1383,8 +1383,8 @@ func (b *browserService) AddHashField(connName string, db int, k any, action int
} }
// AddListItem add item to list or remove from it // AddListItem add item to list or remove from it
func (b *browserService) AddListItem(connName string, db int, k any, action int, items []any) (resp types.JSResp) { func (b *browserService) AddListItem(server string, db int, k any, action int, items []any) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1699,8 +1699,8 @@ func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (resp types.J
} }
// AddZSetValue add item to sorted set // AddZSetValue add item to sorted set
func (b *browserService) AddZSetValue(connName string, db int, k any, action int, valueScore map[string]float64) (resp types.JSResp) { func (b *browserService) AddZSetValue(server string, db int, k any, action int, valueScore map[string]float64) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1757,8 +1757,8 @@ func (b *browserService) AddZSetValue(connName string, db int, k any, action int
} }
// AddStreamValue add stream field // AddStreamValue add stream field
func (b *browserService) AddStreamValue(connName string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) { func (b *browserService) AddStreamValue(server string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1800,8 +1800,8 @@ func (b *browserService) AddStreamValue(connName string, db int, k any, ID strin
} }
// RemoveStreamValues remove stream values by id // RemoveStreamValues remove stream values by id
func (b *browserService) RemoveStreamValues(connName string, db int, k any, IDs []string) (resp types.JSResp) { func (b *browserService) RemoveStreamValues(server string, db int, k any, IDs []string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1827,8 +1827,8 @@ func (b *browserService) RemoveStreamValues(connName string, db int, k any, IDs
} }
// SetKeyTTL set ttl of key // SetKeyTTL set ttl of key
func (b *browserService) SetKeyTTL(connName string, db int, k any, ttl int64) (resp types.JSResp) { func (b *browserService) SetKeyTTL(server string, db int, k any, ttl int64) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1836,14 +1836,13 @@ func (b *browserService) SetKeyTTL(connName string, db int, k any, ttl int64) (r
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var expiration time.Duration
if ttl < 0 { if ttl < 0 {
if err = client.Persist(ctx, key).Err(); err != nil { if err = client.Persist(ctx, key).Err(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
} else { } else {
expiration = time.Duration(ttl) * time.Second expiration := time.Duration(ttl) * time.Second
if err = client.Expire(ctx, key, expiration).Err(); err != nil { if err = client.Expire(ctx, key, expiration).Err(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1854,6 +1853,95 @@ func (b *browserService) SetKeyTTL(connName string, db int, k any, ttl int64) (r
return return
} }
// 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 {
resp.Msg = err.Error()
return
}
ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc()
//cancelEvent := "ttling:stop:" + serialNo
//runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
// cancelFunc()
//})
//processEvent := "ttling:" + serialNo
total := len(ks)
var failed, updated atomic.Int64
var canceled bool
expiration := time.Now().Add(time.Duration(ttl) * time.Second)
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)
var expErr error
if ttl < 0 {
expErr = cli.Persist(ctx, key).Err()
} else {
expErr = cli.ExpireAt(ctx, key, expiration).Err()
}
if err != nil {
failed.Add(1)
} else {
// save deleted key
updated.Add(1)
}
if errors.Is(expErr, 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"`
Updated int64 `json:"updated"`
Failed int64 `json:"failed"`
}{
Canceled: canceled,
Updated: updated.Load(),
Failed: failed.Load(),
}
return
}
// DeleteKey remove redis key // DeleteKey remove redis key
func (b *browserService) DeleteKey(server string, db int, k any, async bool) (resp types.JSResp) { func (b *browserService) DeleteKey(server string, db int, k any, async bool) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) item, err := b.getRedisClient(server, db)
@ -2007,13 +2095,13 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
startTime := time.Now().Add(-10 * time.Second) startTime := time.Now().Add(-10 * time.Second)
for i, k := range ks { for i, k := range ks {
// emit progress per second // 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 { if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now() startTime = time.Now()
param := map[string]any{
"total": total,
"progress": i + 1,
"processing": k,
}
runtime.EventsEmit(b.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)
@ -2146,7 +2234,7 @@ 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, includeExpire bool) (resp types.JSResp) { func (b *browserService) ImportCSV(server string, db int, path string, conflict int, ttl int64) (resp types.JSResp) {
// connect a new connection to export keys // connect a new connection to export keys
conf := Connection().getConnection(server) conf := Connection().getConnection(server)
if conf == nil { if conf == nil {
@ -2182,14 +2270,14 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
var line []string var line []string
var readErr error var readErr error
var key, value []byte var key, value []byte
var ttl time.Duration var ttlValue time.Duration
var imported, ignored int64 var imported, ignored int64
var canceled bool var canceled bool
startTime := time.Now().Add(-10 * time.Second) startTime := time.Now().Add(-10 * time.Second)
for { for {
readErr = nil readErr = nil
ttl = redis.KeepTTL ttlValue = redis.KeepTTL
line, readErr = reader.Read() line, readErr = reader.Read()
if readErr != nil { if readErr != nil {
break break
@ -2205,21 +2293,25 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
continue continue
} }
// get ttl // get ttl
if includeExpire && len(line) > 2 { if ttl < 0 {
// use previous
if expire, ttlErr := strconv.ParseInt(line[2], 10, 64); ttlErr == nil && expire > 0 { if expire, ttlErr := strconv.ParseInt(line[2], 10, 64); ttlErr == nil && expire > 0 {
ttl = time.UnixMilli(expire).Sub(time.Now()) ttlValue = time.UnixMilli(expire).Sub(time.Now())
} }
} else if ttl > 0 {
// custom ttl
ttlValue = time.Duration(ttl) * time.Second
} }
if conflict == 0 { if conflict == 0 {
readErr = client.RestoreReplace(ctx, string(key), ttl, string(value)).Err() readErr = client.RestoreReplace(ctx, string(key), ttlValue, string(value)).Err()
} else { } else {
keyStr := string(key) keyStr := string(key)
// go-redis may crash when batch calling restore // go-redis may crash when batch calling restore
// use "exists" to filter first // use "exists" to filter first
if n, _ := client.Exists(ctx, keyStr).Result(); n <= 0 { if n, _ := client.Exists(ctx, keyStr).Result(); n <= 0 {
readErr = client.Restore(ctx, keyStr, ttl, string(value)).Err() readErr = client.Restore(ctx, keyStr, ttlValue, string(value)).Err()
} else { } else {
readErr = errors.New("key existed") readErr = errors.New("key already existed")
} }
} }
if readErr != nil { if readErr != nil {
@ -2261,8 +2353,8 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
} }
// FlushDB flush database // FlushDB flush database
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) { func (b *browserService) FlushDB(server string, db int, async bool) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -2309,8 +2401,8 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
} }
// RenameKey rename key // RenameKey rename key
func (b *browserService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) { func (b *browserService) RenameKey(server string, db int, key, newKey string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -2362,8 +2454,8 @@ func (b *browserService) CleanCmdHistory() (resp types.JSResp) {
} }
// GetSlowLogs get slow log list // GetSlowLogs get slow log list
func (b *browserService) GetSlowLogs(connName string, db int, num int64) (resp types.JSResp) { func (b *browserService) GetSlowLogs(server string, db int, num int64) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return

View File

@ -17,6 +17,7 @@ import (
type monitorItem struct { type monitorItem struct {
client *redis.Client client *redis.Client
cmd *redis.MonitorCmd cmd *redis.MonitorCmd
mutex sync.Mutex
ch chan string ch chan string
closeCh chan struct{} closeCh chan struct{}
eventName string eventName string
@ -88,7 +89,7 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
item.cmd = item.client.Monitor(c.ctx, item.ch) item.cmd = item.client.Monitor(c.ctx, item.ch)
item.cmd.Start() item.cmd.Start()
go c.processMonitor(item.ch, item.closeCh, item.eventName) go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.eventName)
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = struct {
EventName string `json:"eventName"` EventName string `json:"eventName"`
@ -98,12 +99,23 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
return return
} }
func (c *monitorService) processMonitor(ch <-chan string, closeCh <-chan struct{}, eventName string) { func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, eventName string) {
lastEmitTime := time.Now().Add(-1 * time.Minute)
cache := make([]string, 0, 1000)
for { for {
select { select {
case data := <-ch: case data := <-ch:
if data != "OK" { if data != "OK" {
runtime.EventsEmit(c.ctx, eventName, data) go func() {
mutex.Lock()
defer mutex.Unlock()
cache = append(cache, data)
if time.Now().Sub(lastEmitTime) > 1*time.Second || len(cache) > 300 {
runtime.EventsEmit(c.ctx, eventName, cache)
cache = cache[:0:cap(cache)]
lastEmitTime = time.Now()
}
}()
} }
case <-closeCh: case <-closeCh:

View File

@ -0,0 +1,66 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
const props = defineProps({
value: {
type: Number,
default: -1,
},
unit: {
type: Number,
default: 1,
},
})
const emit = defineEmits(['update:value', 'update:unit'])
const i18n = useI18n()
const unit = computed(() => [
{ value: 1, label: i18n.t('common.second') },
{
value: 60,
label: i18n.t('common.minute'),
},
{
value: 3600,
label: i18n.t('common.hour'),
},
{
value: 86400,
label: i18n.t('common.day'),
},
])
const unitValue = computed(() => {
switch (props.unit) {
case 60:
return 60
case 3600:
return 3600
case 86400:
return 86400
default:
return 1
}
})
</script>
<template>
<n-input-group>
<n-input-number
:max="Number.MAX_SAFE_INTEGER"
:min="-1"
:show-button="false"
:value="props.value"
class="flex-item-expand"
@update:value="(val) => emit('update:value', val)" />
<n-select
:options="unit"
:value="unitValue"
style="max-width: 150px"
@update:value="(val) => emit('update:unit', val)" />
</n-input-group>
</template>
<style lang="scss" scoped></style>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue' import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
import { filter, get, includes, isEmpty, join } from 'lodash' import { debounce, filter, get, includes, isEmpty, join } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
@ -53,6 +53,13 @@ const displayList = computed(() => {
return data.list return data.list
}) })
const _scrollToBottom = () => {
nextTick(() => {
listRef.value?.scrollTo({ position: 'bottom' })
})
}
const scrollToBottom = debounce(_scrollToBottom, 1000, { leading: true, trailing: true })
const onStartMonitor = async () => { const onStartMonitor = async () => {
if (isMonitoring.value) { if (isMonitoring.value) {
return return
@ -65,11 +72,13 @@ const onStartMonitor = async () => {
} }
data.monitorEvent = get(ret, 'eventName') data.monitorEvent = get(ret, 'eventName')
EventsOn(data.monitorEvent, (content) => { EventsOn(data.monitorEvent, (content) => {
data.list.push(content) if (content instanceof Array) {
data.list.push(...content)
} else {
data.list.push(content)
}
if (data.autoShowLast) { if (data.autoShowLast) {
nextTick(() => { scrollToBottom()
listRef.value.scrollTo({ position: 'bottom' })
})
} }
}) })
} }
@ -108,7 +117,7 @@ const onCleanLog = () => {
<template> <template>
<div class="content-log content-container fill-height flex-box-v"> <div class="content-log content-container fill-height flex-box-v">
<n-form class="flex-item" label-align="left" label-placement="left" label-width="auto" size="small"> <n-form class="flex-item" label-align="left" label-placement="left" label-width="auto" size="small">
<n-form-item :label="$t('monitor.actions')"> <n-form-item :feedback="$t('monitor.warning')" :label="$t('monitor.actions')">
<n-space> <n-space>
<n-button <n-button
v-if="!isMonitoring" v-if="!isMonitoring"

View File

@ -72,6 +72,15 @@ const onCopyKey = () => {
$message.error(e.message) $message.error(e.message)
}) })
} }
const onTTL = () => {
dialogStore.openTTLDialog({
server: props.server,
db: props.db,
key: binaryKey.value ? props.keyCode : props.keyPath,
ttl: props.ttl,
})
}
</script> </script>
<template> <template>
@ -93,7 +102,7 @@ const onCopyKey = () => {
<n-button-group> <n-button-group>
<n-tooltip> <n-tooltip>
<template #trigger> <template #trigger>
<n-button :focusable="false" @click="dialogStore.openTTLDialog(props.ttl)"> <n-button :focusable="false" @click="onTTL">
<template #icon> <template #icon>
<n-icon :component="Timer" size="18" /> <n-icon :component="Timer" size="18" />
</template> </template>

View File

@ -222,7 +222,7 @@ const actionColumn = {
}) })
if (success) { if (success) {
props.value.splice(index, 1) props.value.splice(index, 1)
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.k })) $message.success(i18n.t('dialogue.delete.success', { key: row.k }))
} else { } else {
$message.error(msg) $message.error(msg)
} }

View File

@ -194,7 +194,7 @@ const actionColumn = {
index, index,
}) })
if (success) { if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: `#${index + 1}` })) $message.success(i18n.t('dialogue.delete.success', { key: `#${index + 1}` }))
} else { } else {
$message.error(msg) $message.error(msg)
} }

View File

@ -191,7 +191,7 @@ const actionColumn = {
value: row.v, value: row.v,
}) })
if (success) { if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.v })) $message.success(i18n.t('dialogue.delete.success', { key: row.v }))
} else { } else {
$message.error(msg) $message.error(msg)
} }

View File

@ -127,7 +127,7 @@ const actionColumn = {
ids: row.id, ids: row.id,
}) })
if (success) { if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.id })) $message.success(i18n.t('dialogue.delete.success', { key: row.id }))
} else { } else {
$message.error(msg) $message.error(msg)
} }

View File

@ -129,7 +129,7 @@ const onDelete = () => {
const { name, db } = data.value const { name, db } = data.value
browserStore.deleteKey(name, db, keyName.value).then((success) => { browserStore.deleteKey(name, db, keyName.value).then((success) => {
if (success) { if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: data.value.keyPath })) $message.success(i18n.t('dialogue.delete.success', { key: data.value.keyPath }))
} }
}) })
}) })

View File

@ -238,7 +238,7 @@ const actionColumn = {
value: row.v, value: row.v,
}) })
if (success) { if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.v })) $message.success(i18n.t('dialogue.delete.success', { key: row.v }))
} else { } else {
$message.error(msg) $message.error(msg)
} }

View File

@ -5,15 +5,18 @@ import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import FileOpenInput from '@/components/common/FileOpenInput.vue' import FileOpenInput from '@/components/common/FileOpenInput.vue'
import TtlInput from '@/components/common/TtlInput.vue'
const importKeyForm = reactive({ const importKeyForm = reactive({
server: '', server: '',
db: 0, db: 0,
expire: true,
reload: true, reload: true,
file: '', file: '',
type: 0, type: 0,
conflict: 0, conflict: 0,
ttlType: 0,
ttl: -1,
ttlUnit: 1,
}) })
const dialogStore = useDialog() const dialogStore = useDialog()
@ -25,11 +28,12 @@ watchEffect(() => {
const { server, db } = dialogStore.importKeyParam const { server, db } = dialogStore.importKeyParam
importKeyForm.server = server importKeyForm.server = server
importKeyForm.db = db importKeyForm.db = db
importKeyForm.expire = true
importKeyForm.reload = true importKeyForm.reload = true
importKeyForm.file = '' importKeyForm.file = ''
importKeyForm.type = 0 importKeyForm.type = 0
importKeyForm.conflict = 0 importKeyForm.conflict = 0
importKeyForm.ttlType = 0
importKeyForm.ttl = -1
importing.value = false importing.value = false
} }
}) })
@ -46,6 +50,21 @@ const conflictOption = computed(() => [
}, },
]) ])
const ttlOption = computed(() => [
{
value: 0,
label: i18n.t('dialogue.import.ttl_include'),
},
{
value: 1,
label: i18n.t('dialogue.import.ttl_ignore'),
},
{
value: 2,
label: i18n.t('dialogue.import.ttl_custom'),
},
])
const importEnable = computed(() => { const importEnable = computed(() => {
return !isEmpty(importKeyForm.file) return !isEmpty(importKeyForm.file)
}) })
@ -53,8 +72,19 @@ const importEnable = computed(() => {
const onConfirmImport = async () => { const onConfirmImport = async () => {
try { try {
importing.value = true importing.value = true
const { server, db, file, conflict, expire, reload } = importKeyForm const { server, db, file, conflict, ttlType, ttl, ttlUnit, reload } = importKeyForm
browserStore.importKeysFromCSVFile(server, db, file, conflict, expire, reload).catch((e) => {}) let ttlVal = 0
switch (ttlType) {
case 0:
ttlVal = -1
break
case 1:
ttlVal = 0
break
default:
ttlVal = ttl * (ttlUnit || 1)
}
browserStore.importKeysFromCSVFile(server, db, file, conflict, ttlVal, reload).catch((e) => {})
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
return return
@ -104,16 +134,22 @@ const onClose = () => {
:value="op.value" /> :value="op.value" />
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.import.import_expire_title')" :show-label="false"> <n-form-item :label="$t('dialogue.import.import_expire_title')">
<n-space :wrap-item="false"> <n-space :wrap-item="false">
<n-checkbox v-model:checked="importKeyForm.expire" :autofocus="false"> <n-radio-group v-model:value="importKeyForm.ttlType">
{{ $t('dialogue.import.import_expire') }} <n-radio-button v-for="(op, i) in ttlOption" :key="i" :label="op.label" :value="op.value" />
</n-checkbox> </n-radio-group>
<n-checkbox v-model:checked="importKeyForm.reload" :autofocus="false"> <ttl-input
{{ $t('dialogue.import.reload') }} v-if="importKeyForm.ttlType === 2"
</n-checkbox> v-model:unit="importKeyForm.ttlUnit"
v-model:value="importKeyForm.ttl" />
</n-space> </n-space>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.import.import_expire_title')" :show-label="false">
<n-checkbox v-model:checked="importKeyForm.reload" :autofocus="false">
{{ $t('dialogue.import.reload') }}
</n-checkbox>
</n-form-item>
</n-form> </n-form>
</n-spin> </n-spin>

View File

@ -214,7 +214,7 @@ const onClose = () => {
</template> </template>
</n-input-number> </n-input-number>
<n-button :focusable="false" secondary type="primary" @click="() => (newForm.ttl = -1)"> <n-button :focusable="false" secondary type="primary" @click="() => (newForm.ttl = -1)">
{{ $t('dialogue.key.persist_key') }} {{ $t('interface.forever') }}
</n-button> </n-button>
</n-input-group> </n-input-group>
</n-form-item> </n-form-item>

View File

@ -1,62 +1,48 @@
<script setup> <script setup>
import { computed, reactive, watchEffect } from 'vue' import { computed, reactive, ref, watchEffect } from 'vue'
import useDialog from 'stores/dialog' import useDialog from 'stores/dialog'
import useTabStore from 'stores/tab.js'
import Binary from '@/components/icons/Binary.vue'
import { isEmpty } from 'lodash'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { isEmpty, size } from 'lodash'
import TtlInput from '@/components/common/TtlInput.vue'
const ttlForm = reactive({ const ttlForm = reactive({
server: '', server: '',
db: 0, db: 0,
key: '', key: '',
keyCode: null, keys: [],
ttl: -1, ttl: -1,
unit: 1, unit: 1,
}) })
const dialogStore = useDialog() const dialogStore = useDialog()
const browserStore = useBrowserStore() const browserStore = useBrowserStore()
const tabStore = useTabStore()
watchEffect(() => { watchEffect(() => {
if (dialogStore.ttlDialogVisible) { if (dialogStore.ttlDialogVisible) {
// get ttl from current tab // get ttl from current tab
const tab = tabStore.currentTab const { server, db, key, keys, ttl } = dialogStore.ttlParam
if (tab != null) { ttlForm.server = server
ttlForm.server = tab.name ttlForm.db = db
ttlForm.db = tab.db ttlForm.key = key
ttlForm.key = tab.key ttlForm.keys = keys
ttlForm.keyCode = tab.keyCode ttlForm.unit = 1
ttlForm.unit = 1 if (ttl < 0) {
if (tab.ttl < 0) { // forever
// forever ttlForm.ttl = -1
ttlForm.ttl = -1 } else {
} else { ttlForm.ttl = ttl
ttlForm.ttl = tab.ttl
}
} }
procssing.value = false
} }
}) })
const i18n = useI18n() const procssing = ref(false)
const unit = computed(() => [ const isBatchAction = computed(() => {
{ value: 1, label: i18n.t('common.second') }, return !isEmpty(ttlForm.keys)
{ })
value: 60,
label: i18n.t('common.minute'),
},
{
value: 3600,
label: i18n.t('common.hour'),
},
{
value: 86400,
label: i18n.t('common.day'),
},
])
const i18n = useI18n()
const quickOption = computed(() => [ const quickOption = computed(() => [
{ value: -1, unit: 1, label: i18n.t('interface.forever') }, { value: -1, unit: 1, label: i18n.t('interface.forever') },
{ value: 10, unit: 1, label: `10 ${i18n.t('common.second')}` }, { value: 10, unit: 1, label: `10 ${i18n.t('common.second')}` },
@ -76,24 +62,20 @@ const onClose = () => {
const onConfirm = async () => { const onConfirm = async () => {
try { try {
const tab = tabStore.currentTab procssing.value = true
if (tab == null) {
return
}
const key = isEmpty(ttlForm.keyCode) ? ttlForm.key : ttlForm.keyCode
const ttl = ttlForm.ttl * (ttlForm.unit || 1) const ttl = ttlForm.ttl * (ttlForm.unit || 1)
const success = await browserStore.setTTL(tab.name, tab.db, key, ttl) let success = false
if (isBatchAction.value) {
success = await browserStore.setTTLs(ttlForm.server, ttlForm.db, ttlForm.keys, ttl)
} else {
success = await browserStore.setTTL(ttlForm.server, ttlForm.db, ttlForm.key, ttl)
}
if (success) { if (success) {
tabStore.updateTTL({
server: ttlForm.server,
db: ttlForm.db,
key: ttlForm.key,
ttl: ttl,
})
} }
} catch (e) { } catch (e) {
$message.error(e.message || 'set ttl fail') $message.error(e.message || 'set ttl fail')
} finally { } finally {
procssing.value = false
dialogStore.closeTTLDialog() dialogStore.closeTTLDialog()
} }
} }
@ -109,30 +91,20 @@ const onConfirm = async () => {
:negative-text="$t('common.cancel')" :negative-text="$t('common.cancel')"
:on-negative-click="onClose" :on-negative-click="onClose"
:on-positive-click="onConfirm" :on-positive-click="onConfirm"
:positive-button-props="{ focusable: false, size: 'medium' }" :positive-button-props="{ focusable: false, size: 'medium', loading: procssing }"
:positive-text="$t('common.save')" :positive-text="$t('common.save')"
:show-icon="false" :show-icon="false"
:title="$t('dialogue.ttl.title')" :title="
isBatchAction ? $t('dialogue.ttl.title_batch', { count: size(ttlForm.keys) }) : $t('dialogue.ttl.title')
"
preset="dialog" preset="dialog"
transform-origin="center"> transform-origin="center">
<n-form :model="ttlForm" :show-require-mark="false" label-placement="top"> <n-form :model="ttlForm" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('common.key')"> <n-form-item v-if="!isBatchAction" :label="$t('common.key')">
<n-input :value="ttlForm.key" readonly> <n-input :value="ttlForm.key" readonly />
<template #prefix>
<n-icon v-if="!!ttlForm.keyCode" :component="Binary" size="20" />
</template>
</n-input>
</n-form-item> </n-form-item>
<n-form-item :label="$t('interface.ttl')" required> <n-form-item :label="$t('interface.ttl')" required>
<n-input-group> <ttl-input v-model:unit="ttlForm.unit" v-model:value="ttlForm.ttl" />
<n-input-number
v-model:value="ttlForm.ttl"
:max="Number.MAX_SAFE_INTEGER"
:min="-1"
:show-button="false"
class="flex-item-expand" />
<n-select v-model:value="ttlForm.unit" :options="unit" style="max-width: 150px" />
</n-input-group>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.ttl.quick_set')" :show-feedback="false"> <n-form-item :label="$t('dialogue.ttl.quick_set')" :show-feedback="false">
<n-space :wrap="true" :wrap-item="false"> <n-space :wrap="true" :wrap-item="false">

View File

@ -27,6 +27,7 @@ import { ConnectionType } from '@/consts/connection_type.js'
import Import from '@/components/icons/Import.vue' import Import from '@/components/icons/Import.vue'
import Down from '@/components/icons/Down.vue' import Down from '@/components/icons/Down.vue'
import Checkbox from '@/components/icons/Checkbox.vue' import Checkbox from '@/components/icons/Checkbox.vue'
import Timer from '@/components/icons/Timer.vue'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -127,11 +128,13 @@ const onAddKey = () => {
const selectedKey = get(browserTreeRef.value?.getSelectedKey(), 0) const selectedKey = get(browserTreeRef.value?.getSelectedKey(), 0)
if (selectedKey != null) { if (selectedKey != null) {
const node = browserStore.getNode(selectedKey) const node = browserStore.getNode(selectedKey)
const { type = ConnectionType.RedisValue, redisKey } = node if (node != null) {
if (type === ConnectionType.RedisKey) { const { type = ConnectionType.RedisValue, redisKey } = node
// has prefix if (type === ConnectionType.RedisKey) {
dialogStore.openNewKeyDialog(redisKey, props.server, props.db) // has prefix
return dialogStore.openNewKeyDialog(redisKey, props.server, props.db)
return
}
} }
} }
dialogStore.openNewKeyDialog('', props.server, props.db) dialogStore.openNewKeyDialog('', props.server, props.db)
@ -168,6 +171,10 @@ const onExportChecked = () => {
browserTreeRef.value?.exportCheckedItems() browserTreeRef.value?.exportCheckedItems()
} }
const onUpdateTTLChecked = () => {
browserTreeRef.value?.updateTTLCheckedItems()
}
const onImportData = () => { const onImportData = () => {
dialogStore.openImportKeyDialog(props.server, props.db) dialogStore.openImportKeyDialog(props.server, props.db)
} }
@ -346,7 +353,7 @@ watch(
:icon="LoadList" :icon="LoadList"
:loading="loading" :loading="loading"
:stroke-width="3.5" :stroke-width="3.5"
size="20" size="21"
t-tooltip="interface.load_more" t-tooltip="interface.load_more"
@click="onLoadMore" /> @click="onLoadMore" />
<icon-button <icon-button
@ -355,7 +362,7 @@ watch(
:icon="LoadAll" :icon="LoadAll"
:loading="loading" :loading="loading"
:stroke-width="3.5" :stroke-width="3.5"
size="20" size="21"
t-tooltip="interface.load_all" t-tooltip="interface.load_all"
@click="onLoadAll" /> @click="onLoadAll" />
<div class="flex-item-expand" style="min-width: 10px" /> <div class="flex-item-expand" style="min-width: 10px" />
@ -363,7 +370,7 @@ watch(
:button-class="['nav-pane-func-btn']" :button-class="['nav-pane-func-btn']"
:icon="Checkbox" :icon="Checkbox"
:stroke-width="3.5" :stroke-width="3.5"
size="20" size="19"
t-tooltip="interface.check_mode" t-tooltip="interface.check_mode"
@click="inCheckState = true" /> @click="inCheckState = true" />
<n-dropdown <n-dropdown
@ -385,6 +392,14 @@ watch(
size="20" size="20"
t-tooltip="interface.export_checked" t-tooltip="interface.export_checked"
@click="onExportChecked" /> @click="onExportChecked" />
<icon-button
:button-class="['nav-pane-func-btn']"
:disabled="checkedCount <= 0"
:icon="Timer"
:stroke-width="3.5"
size="20"
t-tooltip="interface.ttl_checked"
@click="onUpdateTTLChecked" />
<icon-button <icon-button
:button-class="['nav-pane-func-btn']" :button-class="['nav-pane-func-btn']"
:disabled="checkedCount <= 0" :disabled="checkedCount <= 0"

View File

@ -233,7 +233,7 @@ const handleSelectContextMenu = (key) => {
$dialog.warning(i18n.t('dialogue.remove_tip', { name: redisKeyName }), () => { $dialog.warning(i18n.t('dialogue.remove_tip', { name: redisKeyName }), () => {
browserStore.deleteKey(props.server, db, redisKey).then((success) => { browserStore.deleteKey(props.server, db, redisKey).then((success) => {
if (success) { if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: redisKeyName })) $message.success(i18n.t('dialogue.delete.success', { key: redisKeyName }))
} }
}) })
}) })
@ -469,7 +469,7 @@ const calcLayerMenu = (loading) => {
return [ return [
// reload layer enable only full loaded // reload layer enable only full loaded
h(IconButton, { h(IconButton, {
tTooltip: props.fullLoaded ? 'interface.reload' : 'interface.reload', tTooltip: props.fullLoaded ? 'interface.reload' : 'interface.reload_disable',
icon: Refresh, icon: Refresh,
loading: loading === true, loading: loading === true,
disabled: !props.fullLoaded, disabled: !props.fullLoaded,
@ -565,7 +565,7 @@ watchEffect(
) )
// the NTree node may get incorrect height after change data // the NTree node may get incorrect height after change data
// add key property to force refresh the component and then everything back to normal // add key property for force refresh the component so that everything back to normal
const treeKey = ref(0) const treeKey = ref(0)
defineExpose({ defineExpose({
handleSelectContextMenu, handleSelectContextMenu,
@ -589,6 +589,17 @@ defineExpose({
dialogStore.openExportKeyDialog(props.server, props.db, redisKeys) dialogStore.openExportKeyDialog(props.server, props.db, redisKeys)
} }
}, },
updateTTLCheckedItems: () => {
const checkedKeys = tabStore.currentCheckedKeys
const redisKeys = map(checkedKeys, 'redisKey')
if (!isEmpty(redisKeys)) {
dialogStore.openTTLDialog({
server: props.server,
db: props.db,
keys: redisKeys,
})
}
},
getSelectedKey: () => { getSelectedKey: () => {
return selectedKeys.value || [] return selectedKeys.value || []
}, },

View File

@ -33,6 +33,7 @@ const filterPattern = ref('')
stroke-width="4" stroke-width="4"
t-tooltip="interface.new_group" t-tooltip="interface.new_group"
@click="dialogStore.openNewGroupDialog()" /> @click="dialogStore.openNewGroupDialog()" />
<n-divider vertical />
<n-input v-model:value="filterPattern" :placeholder="$t('interface.filter')" clearable> <n-input v-model:value="filterPattern" :placeholder="$t('interface.filter')" clearable>
<template #prefix> <template #prefix>
<n-icon :component="Filter" size="20" /> <n-icon :component="Filter" size="20" />

View File

@ -447,6 +447,9 @@ const handleDrop = ({ node, dragNode, dropPosition }) => {
if (dragNodeSiblings === null || dragNodeIndex === null) { if (dragNodeSiblings === null || dragNodeIndex === null) {
return return
} }
if (node.type === ConnectionType.Group && dragNode.type === ConnectionType.Group) {
return
}
dragNodeSiblings.splice(dragNodeIndex, 1) dragNodeSiblings.splice(dragNodeIndex, 1)
if (dropPosition === 'inside') { if (dropPosition === 'inside') {
if (node.children) { if (node.children) {

View File

@ -84,6 +84,7 @@
"quit_check_mode": "Quit Check Mode", "quit_check_mode": "Quit Check Mode",
"delete_checked": "Delete Checked Items", "delete_checked": "Delete Checked Items",
"export_checked": "Export Checked Items", "export_checked": "Export Checked Items",
"ttl_checked": "Update TTL for Checked Items",
"copy_value": "Copy Value", "copy_value": "Copy Value",
"edit_value": "Edit Value", "edit_value": "Edit Value",
"save_update": "Save Update", "save_update": "Save Update",
@ -106,6 +107,7 @@
"view_as": "View As", "view_as": "View As",
"decode_with": "Decode / Decompression", "decode_with": "Decode / Decompression",
"reload": "Reload", "reload": "Reload",
"reload_disable": "Reload will enable after full loaded",
"open_connection": "Open Connection", "open_connection": "Open Connection",
"copy_path": "Copy Path", "copy_path": "Copy Path",
"copy_key": "Copy Key", "copy_key": "Copy Key",
@ -146,9 +148,6 @@
"interrupt_connection": "Cancel", "interrupt_connection": "Cancel",
"remove_tip": "{type} \"{name}\" will be deleted", "remove_tip": "{type} \"{name}\" will be deleted",
"remove_group_tip": "Group \"{name}\" and all connections in it 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({index}/{count})",
"delete_completed": "Deletion process has been completed, {success} successed, {fail} failed",
"rename_binary_key_fail": "Rename binary key name is unsupported", "rename_binary_key_fail": "Rename binary key name is unsupported",
"handle_succ": "Success!", "handle_succ": "Success!",
"handle_cancel": "The operation has been canceled.", "handle_cancel": "The operation has been canceled.",
@ -240,7 +239,6 @@
"key": { "key": {
"new": "New Key", "new": "New Key",
"new_name": "New Key Name", "new_name": "New Key Name",
"persist_key": "Persist Key",
"server": "Connection", "server": "Connection",
"db_index": "Database Index", "db_index": "Database Index",
"key_expression": "Key Expression", "key_expression": "Key Expression",
@ -252,6 +250,11 @@
"confirm_flush": "I know what I'm doing!", "confirm_flush": "I know what I'm doing!",
"confirm_flush_db": "Confirm Flush Database" "confirm_flush_db": "Confirm Flush Database"
}, },
"delete": {
"success": "\"{key}\" has been deleted",
"doing": "Deleting key({index}/{count})",
"completed": "Deletion process has been completed, {success} successed, {fail} failed"
},
"field": { "field": {
"new": "Add New Field", "new": "Add New Field",
"new_item": "Add New Item", "new_item": "Add New Item",
@ -288,7 +291,6 @@
"import": { "import": {
"name": "Import Data", "name": "Import Data",
"import_expire_title": "Expiration", "import_expire_title": "Expiration",
"import_expire": "Import Expiration Time",
"import": "Import", "import": "Import",
"reload": "Reload Keys After Imported", "reload": "Reload Keys After Imported",
"open_csv_file": "Import File", "open_csv_file": "Import File",
@ -296,17 +298,20 @@
"conflict_handle": "Key Conflict Resolution", "conflict_handle": "Key Conflict Resolution",
"conflict_overwrite": "Overwrite", "conflict_overwrite": "Overwrite",
"conflict_ignore": "Ignore", "conflict_ignore": "Ignore",
"ttl_include": "Import From File",
"ttl_ignore": "Do Not Set",
"ttl_custom": "Custom",
"importing": "Importing Keys imported/overwrite:{imported} conflict/fail:{conflict}", "importing": "Importing Keys imported/overwrite:{imported} conflict/fail:{conflict}",
"import_completed": "Import completed, {success} successes, {ignored} failed" "import_completed": "Import completed, {success} successes, {ignored} failed"
}, },
"ttl": { "ttl": {
"title": "Set Key TTL", "title": "Update TTL",
"quick_set": "Quick Settings" "title_batch": "Batch Update TTL({count})",
"quick_set": "Quick Settings",
"success": "All TTL of keys have been updated"
}, },
"upgrade": { "upgrade": {
"title": "New Version Available", "title": "New Version Available",
"import_expire_title": "Expiration",
"import_expire": "Include Expiration Time",
"new_version_tip": "A new version({ver}) is available. Download now?", "new_version_tip": "A new version({ver}) is available. Download now?",
"no_update": "You're update to date", "no_update": "You're update to date",
"download_now": "Download", "download_now": "Download",
@ -364,6 +369,7 @@
"monitor": { "monitor": {
"title": "Monitor Commands", "title": "Monitor Commands",
"actions": "Actions", "actions": "Actions",
"warning": "Monitor command may lead to server congestion. Please use it cautiously on production servers.",
"start": "Start", "start": "Start",
"stop": "Stop", "stop": "Stop",
"search": "Search", "search": "Search",

View File

@ -215,7 +215,6 @@
"key": { "key": {
"new": "Nova Chave", "new": "Nova Chave",
"new_name": "Novo Nome da Chave", "new_name": "Novo Nome da Chave",
"persist_key": "Persistir Chave",
"server": "Conexão", "server": "Conexão",
"db_index": "Índice do Banco de Dados", "db_index": "Índice do Banco de Dados",
"key_expression": "Expressão da Chave", "key_expression": "Expressão da Chave",

View File

@ -84,6 +84,7 @@
"quit_check_mode": "退出勾选模式", "quit_check_mode": "退出勾选模式",
"delete_checked": "删除所选项", "delete_checked": "删除所选项",
"export_checked": "导出所选项", "export_checked": "导出所选项",
"ttl_checked": "为所选项更新TTL",
"copy_value": "复制值", "copy_value": "复制值",
"edit_value": "修改值", "edit_value": "修改值",
"save_update": "保存修改", "save_update": "保存修改",
@ -106,6 +107,7 @@
"view_as": "查看方式", "view_as": "查看方式",
"decode_with": "解码/解压方式", "decode_with": "解码/解压方式",
"reload": "重新载入", "reload": "重新载入",
"reload_disable": "全量加载后可重新载入",
"open_connection": "打开连接", "open_connection": "打开连接",
"copy_path": "复制路径", "copy_path": "复制路径",
"copy_key": "复制键名", "copy_key": "复制键名",
@ -146,9 +148,6 @@
"interrupt_connection": "中断连接", "interrupt_connection": "中断连接",
"remove_tip": "{type} \"{name}\" 将会被删除", "remove_tip": "{type} \"{name}\" 将会被删除",
"remove_group_tip": "分组 \"{name}\"及其所有连接将会被删除", "remove_group_tip": "分组 \"{name}\"及其所有连接将会被删除",
"delete_key_succ": "{key} 已被删除",
"deleting_key": "正在删除键({index}/{count})",
"delete_completed": "已完成删除操作,成功{success}个,失败{fail}个",
"rename_binary_key_fail": "不支持重命名二进制键名", "rename_binary_key_fail": "不支持重命名二进制键名",
"handle_succ": "操作成功", "handle_succ": "操作成功",
"handle_cancel": "操作已取消", "handle_cancel": "操作已取消",
@ -240,7 +239,6 @@
"key": { "key": {
"new": "添加新键", "new": "添加新键",
"new_name": "新键名", "new_name": "新键名",
"persist_key": "持久化键",
"server": "所属连接", "server": "所属连接",
"db_index": "数据库编号", "db_index": "数据库编号",
"key_expression": "键名表达式", "key_expression": "键名表达式",
@ -252,6 +250,11 @@
"confirm_flush": "我知道我正在执行的操作!", "confirm_flush": "我知道我正在执行的操作!",
"confirm_flush_db": "确认清空数据库" "confirm_flush_db": "确认清空数据库"
}, },
"delete": {
"success": "{key} 已被删除",
"doing": "正在删除键({index}/{count})",
"completed": "已完成删除操作,成功{success}个,失败{fail}个"
},
"field": { "field": {
"new": "添加新字段", "new": "添加新字段",
"new_item": "添加新元素", "new_item": "添加新元素",
@ -288,7 +291,6 @@
"import": { "import": {
"name": "导入数据", "name": "导入数据",
"import_expire_title": "过期时间", "import_expire_title": "过期时间",
"import_expire": "包含键过期时间",
"reload": "导入完成后重新载入", "reload": "导入完成后重新载入",
"import": "确认导入", "import": "确认导入",
"open_csv_file": "导入文件路径", "open_csv_file": "导入文件路径",
@ -296,12 +298,17 @@
"conflict_handle": "键冲突处理", "conflict_handle": "键冲突处理",
"conflict_overwrite": "覆盖", "conflict_overwrite": "覆盖",
"conflict_ignore": "忽略", "conflict_ignore": "忽略",
"ttl_include": "尝试导入",
"ttl_ignore": "不设置",
"ttl_custom": "自定义",
"importing": "正在导入数据 已导入/覆盖:{imported} 冲突/失败:{conflict}", "importing": "正在导入数据 已导入/覆盖:{imported} 冲突/失败:{conflict}",
"import_completed": "已完成导入操作,成功{success}个,忽略{ignored}个" "import_completed": "已完成导入操作,成功{success}个,忽略{ignored}个"
}, },
"ttl": { "ttl": {
"title": "设置键存活时间", "title": "设置键存活时间",
"quick_set": "快捷设置" "title_batch": "批量设置键存活时间({count})",
"quick_set": "快捷设置",
"success": "已全部更新TTL"
}, },
"upgrade": { "upgrade": {
"title": "有可用新版本", "title": "有可用新版本",
@ -362,6 +369,7 @@
"monitor": { "monitor": {
"title": "监控命令", "title": "监控命令",
"actions": "操作", "actions": "操作",
"warning": "命令监控可能会造成服务端堵塞,请谨慎在生产环境的服务器使用",
"start": "开启监控", "start": "开启监控",
"stop": "停止监控", "stop": "停止监控",
"search": "搜索", "search": "搜索",

View File

@ -5,6 +5,7 @@ import {
AddListItem, AddListItem,
AddStreamValue, AddStreamValue,
AddZSetValue, AddZSetValue,
BatchSetTTL,
CleanCmdHistory, CleanCmdHistory,
CloseConnection, CloseConnection,
ConvertValue, ConvertValue,
@ -1484,19 +1485,81 @@ const useBrowserStore = defineStore('browser', {
* reset key's ttl * reset key's ttl
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string} key * @param {string|number[]} key
* @param {number} ttl * @param {number} ttl
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
async setTTL(server, db, key, ttl) { async setTTL(server, db, key, ttl) {
try { try {
const { success, msg } = await SetKeyTTL(server, db, key, ttl) const { success, msg } = await SetKeyTTL(server, db, key, ttl)
if (success) {
const tabStore = useTabStore()
tabStore.updateTTL({
server,
db,
key,
ttl,
})
}
return success === true return success === true
} catch (e) { } catch (e) {
return false return false
} }
}, },
async setTTLs(server, db, keys, ttl) {
// const msgRef = $message.loading('', { duration: 0, closable: true })
// let updated = []
// let failCount = 0
// let canceled = false
const serialNo = Date.now().valueOf().toString()
// const eventName = 'ttling:' + serialNo
// const cancelEvent = 'ttling:stop:' + serialNo
try {
// let maxProgress = 0
// EventsOn(eventName, ({ total, progress, processing }) => {
// // update delete progress
// if (progress > maxProgress) {
// maxProgress = progress
// }
// const k = decodeRedisKey(processing)
// msgRef.content = i18nGlobal.t('dialogue.delete.doing', {
// key: k,
// index: maxProgress,
// count: total,
// })
// })
// msgRef.onClose = () => {
// EventsEmit(cancelEvent)
// }
const { data, success, msg } = await BatchSetTTL(server, db, keys, ttl, serialNo)
if (success) {
// canceled = get(data, 'canceled', false)
// updated = get(data, 'updated', [])
// failCount = get(data, 'failed', 0)
} else {
$message.error(msg)
}
} finally {
// msgRef.destroy()
// EventsOff(eventName)
}
$message.success(i18nGlobal.t('dialogue.ttl.success'))
// const deletedCount = size(updated)
// if (canceled) {
// $message.info(i18nGlobal.t('dialogue.handle_cancel'))
// } else if (failCount <= 0) {
// // no fail
// $message.success(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
// } else if (failCount >= deletedCount) {
// // all fail
// $message.error(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
// } else {
// // some fail
// $message.warn(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
// }
},
/** /**
* delete redis key * delete redis key
* @param {string} server * @param {string} server
@ -1526,6 +1589,8 @@ const useBrowserStore = defineStore('browser', {
// set tab content empty // set tab content empty
const tab = useTabStore() const tab = useTabStore()
tab.emptyTab(server) tab.emptyTab(server)
tab.setSelectedKeys(server)
tab.setCheckedKeys(server)
return true return true
} finally { } finally {
} }
@ -1555,7 +1620,7 @@ const useBrowserStore = defineStore('browser', {
maxProgress = progress maxProgress = progress
} }
const k = decodeRedisKey(processing) const k = decodeRedisKey(processing)
msgRef.content = i18nGlobal.t('dialogue.deleting_key', { msgRef.content = i18nGlobal.t('dialogue.delete.doing', {
key: k, key: k,
index: maxProgress, index: maxProgress,
count: total, count: total,
@ -1585,13 +1650,13 @@ const useBrowserStore = defineStore('browser', {
$message.info(i18nGlobal.t('dialogue.handle_cancel')) $message.info(i18nGlobal.t('dialogue.handle_cancel'))
} else if (failCount <= 0) { } else if (failCount <= 0) {
// no fail // no fail
$message.success(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount })) $message.success(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
} else if (failCount >= deletedCount) { } else if (failCount >= deletedCount) {
// all fail // all fail
$message.error(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount })) $message.error(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
} else { } else {
// some fail // some fail
$message.warn(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount })) $message.warn(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
} }
// update ui // update ui
timeout(100).then(async () => { timeout(100).then(async () => {
@ -1673,11 +1738,11 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db * @param {number} db
* @param {string} path * @param {string} path
* @param {number} conflict * @param {number} conflict
* @param {boolean} [expire] * @param {number} [ttl] <0:use previous; ==0: persist; >0: custom ttl
* @param {boolean} [reload] * @param {boolean} [reload]
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async importKeysFromCSVFile(server, db, path, conflict, expire, reload) { async importKeysFromCSVFile(server, db, path, conflict, ttl, reload) {
const msgRef = $message.loading('', { duration: 0, closable: true }) const msgRef = $message.loading('', { duration: 0, closable: true })
let imported = 0 let imported = 0
let ignored = 0 let ignored = 0
@ -1695,7 +1760,7 @@ const useBrowserStore = defineStore('browser', {
msgRef.onClose = () => { msgRef.onClose = () => {
EventsEmit('import:stop:' + path) EventsEmit('import:stop:' + path)
} }
const { data, success, msg } = await ImportCSV(server, db, path, conflict, expire) const { data, success, msg } = await ImportCSV(server, db, path, conflict, ttl)
if (success) { if (success) {
canceled = get(data, 'canceled', false) canceled = get(data, 'canceled', false)
imported = get(data, 'imported', 0) imported = get(data, 'imported', 0)
@ -1720,9 +1785,9 @@ const useBrowserStore = defineStore('browser', {
/** /**
* flush database * flush database
* @param server * @param {string} server
* @param db * @param {number} db
* @param async * @param {boolean} async
* @return {Promise<boolean>} * @return {Promise<boolean>}
*/ */
async flushDatabase(server, db, async) { async flushDatabase(server, db, async) {

View File

@ -82,8 +82,14 @@ const useDialogStore = defineStore('dialog', {
}, },
flushDBDialogVisible: false, flushDBDialogVisible: false,
selectTTL: -1,
ttlDialogVisible: false, ttlDialogVisible: false,
ttlParam: {
server: '',
db: 0,
key: '',
keys: [],
ttl: 0,
},
preferencesDialogVisible: false, preferencesDialogVisible: false,
aboutDialogVisible: false, aboutDialogVisible: false,
@ -264,12 +270,23 @@ const useDialogStore = defineStore('dialog', {
this.addFieldsDialogVisible = false this.addFieldsDialogVisible = false
}, },
openTTLDialog(ttl) { /**
this.selectTTL = ttl *
* @param {string} server
* @param {number} db
* @param {string|number[]} [key]
* @param {string[]|number[][]} [keys]
* @param {number} [ttl]
*/
openTTLDialog({ server, db, key, keys, ttl = -1 }) {
this.ttlDialogVisible = true this.ttlDialogVisible = true
this.ttlParam.server = server
this.ttlParam.db = db
this.ttlParam.key = key
this.ttlParam.keys = keys
this.ttlParam.ttl = ttl
}, },
closeTTLDialog() { closeTTLDialog() {
this.selectTTL = -1
this.ttlDialogVisible = false this.ttlDialogVisible = false
}, },

View File

@ -1,6 +1,7 @@
import { assign, find, findIndex, get, isEmpty, pullAt, remove, set, size } from 'lodash' import { assign, find, findIndex, get, isEmpty, pullAt, remove, set, size } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { TabItem } from '@/objects/tabItem.js' import { TabItem } from '@/objects/tabItem.js'
import { decodeRedisKey } from '@/utils/key_convert.js'
const useTabStore = defineStore('tab', { const useTabStore = defineStore('tab', {
/** /**
@ -546,7 +547,7 @@ const useTabStore = defineStore('tab', {
* @param {number} ttl * @param {number} ttl
*/ */
updateTTL({ server, db, key, ttl }) { updateTTL({ server, db, key, ttl }) {
let tab = find(this.tabList, { name: server, db, key }) let tab = find(this.tabList, { name: server, db, key: decodeRedisKey(key) })
if (tab == null) { if (tab == null) {
return return
} }
@ -673,7 +674,7 @@ const useTabStore = defineStore('tab', {
* @param {string} server * @param {string} server
* @param {CheckedKey[]} [keys] * @param {CheckedKey[]} [keys]
*/ */
setCheckedKeys(server, keys) { setCheckedKeys(server, keys = null) {
let tab = find(this.tabList, { name: server }) let tab = find(this.tabList, { name: server })
if (tab != null) { if (tab != null) {
if (isEmpty(keys)) { if (isEmpty(keys)) {