feat: add partial entries loading for complex type(list/hash/set/zset/stream) #70

refactor: split "key value loading" into "key summary loading" and "key detail loading"
This commit is contained in:
tiny-craft 2023-11-08 23:45:33 +08:00
parent f2ebd7f358
commit 9618990de8
22 changed files with 1255 additions and 285 deletions

View File

@ -29,12 +29,22 @@ type slowLogItem struct {
Cost int64 `json:"cost"`
}
type entryCursor struct {
DB int
Type string
Key string
Pattern string
Cursor uint64
XLast string // last stream pos
}
type connectionItem struct {
client redis.UniversalClient
ctx context.Context
cancelFunc context.CancelFunc
cursor map[int]uint64 // current cursor of databases
stepSize int64
client redis.UniversalClient
ctx context.Context
cancelFunc context.CancelFunc
cursor map[int]uint64 // current cursor of databases
entryCursor map[int]entryCursor // current entry cursor of databases
stepSize int64
}
type browserService struct {
@ -261,11 +271,12 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
}
ctx, cancelFunc := context.WithCancel(b.ctx)
item = connectionItem{
client: client,
ctx: ctx,
cancelFunc: cancelFunc,
cursor: map[int]uint64{},
stepSize: int64(selConn.LoadSize),
client: client,
ctx: ctx,
cancelFunc: cancelFunc,
cursor: map[int]uint64{},
entryCursor: map[int]entryCursor{},
stepSize: int64(selConn.LoadSize),
}
if item.stepSize <= 0 {
item.stepSize = consts.DEFAULT_LOAD_SIZE
@ -487,6 +498,345 @@ func (b *browserService) LoadAllKeys(connName string, db int, match, keyType str
return
}
// GetKeySummary get key summary info
func (b *browserService) GetKeySummary(param types.KeySummaryParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB)
if err != nil {
resp.Msg = err.Error()
return
}
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key)
var keyType string
var dur time.Duration
keyType, err = client.Type(ctx, key).Result()
if err != nil {
resp.Msg = err.Error()
return
}
if keyType == "none" {
resp.Msg = "key not exists"
return
}
var data types.KeySummary
data.Type = strings.ToLower(keyType)
if dur, err = client.TTL(ctx, key).Result(); err != nil {
data.TTL = -1
} else {
if dur < 0 {
data.TTL = -1
} else {
data.TTL = int64(dur.Seconds())
}
}
data.Size, _ = client.MemoryUsage(ctx, key, 0).Result()
switch data.Type {
case "string":
data.Length, _ = client.StrLen(ctx, key).Result()
case "list":
data.Length, _ = client.LLen(ctx, key).Result()
case "hash":
data.Length, _ = client.HLen(ctx, key).Result()
case "set":
data.Length, _ = client.SCard(ctx, key).Result()
case "zset":
data.Length, _ = client.ZCard(ctx, key).Result()
case "stream":
data.Length, _ = client.XLen(ctx, key).Result()
default:
resp.Msg = "unknown key type"
return
}
resp.Success = true
resp.Data = data
return
}
// GetKeyDetail get key detail
func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB)
if err != nil {
resp.Msg = err.Error()
return
}
client, ctx, entryCors := item.client, item.ctx, item.entryCursor
key := strutil.DecodeRedisKey(param.Key)
var keyType string
keyType, err = client.Type(ctx, key).Result()
if err != nil {
resp.Msg = err.Error()
return
}
if keyType == "none" {
resp.Msg = "key not exists"
return
}
var data types.KeyDetail
//var cursor uint64
matchPattern := param.MatchPattern
if len(matchPattern) <= 0 {
matchPattern = "*"
}
// define get entry cursor function
getEntryCursor := func() (uint64, string) {
if entry, ok := entryCors[param.DB]; !ok || entry.Key != key || entry.Pattern != matchPattern {
// not the same key or match pattern, reset cursor
entry = entryCursor{
DB: param.DB,
Key: key,
Pattern: matchPattern,
Cursor: 0,
}
entryCors[param.DB] = entry
return 0, ""
} else {
return entry.Cursor, entry.XLast
}
}
// define set entry cursor function
setEntryCursor := func(cursor uint64) {
entryCors[param.DB] = entryCursor{
DB: param.DB,
Type: "",
Key: key,
Pattern: matchPattern,
Cursor: cursor,
}
}
// define set last stream pos function
setEntryXLast := func(last string) {
entryCors[param.DB] = entryCursor{
DB: param.DB,
Type: "",
Key: key,
Pattern: matchPattern,
XLast: last,
}
}
switch strings.ToLower(keyType) {
case "string":
var str string
str, err = client.Get(ctx, key).Result()
data.Value, data.DecodeType, data.ViewAs = strutil.ConvertTo(str, param.DecodeType, param.ViewAs)
case "list":
loadListHandle := func() ([]string, bool, error) {
var items []string
var cursor uint64
if param.Full {
// load all
cursor = 0
items, err = client.LRange(ctx, key, 0, -1).Result()
} else {
cursor, _ = getEntryCursor()
scanSize := int64(Preferences().GetScanSize())
items, err = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result()
cursor = cursor + uint64(scanSize)
if len(items) < int(scanSize) {
cursor = 0
}
}
setEntryCursor(cursor)
if err != nil {
return items, false, err
}
return items, cursor == 0, nil
}
data.Value, data.End, err = loadListHandle()
case "hash":
loadHashHandle := func() (map[string]string, bool, error) {
items := map[string]string{}
scanSize := int64(Preferences().GetScanSize())
var loadedVal []string
var cursor uint64
if param.Full {
// load all
cursor = 0
for {
loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", scanSize).Result()
if err != nil {
return nil, false, err
}
for i := 0; i < len(loadedVal); i += 2 {
items[loadedVal[i]] = loadedVal[i+1]
}
if cursor == 0 {
break
}
}
} else {
cursor, _ = getEntryCursor()
loadedVal, cursor, err = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result()
if err != nil {
return nil, false, err
}
for i := 0; i < len(loadedVal); i += 2 {
items[loadedVal[i]] = loadedVal[i+1]
}
}
setEntryCursor(cursor)
return items, cursor == 0, nil
}
data.Value, data.End, err = loadHashHandle()
if err != nil {
resp.Msg = err.Error()
return
}
case "set":
loadSetHandle := func() ([]string, bool, error) {
var items []string
var cursor uint64
scanSize := int64(Preferences().GetScanSize())
var loadedKey []string
if param.Full {
// load all
cursor = 0
for {
loadedKey, cursor, err = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
if err != nil {
return items, false, err
}
items = append(items, loadedKey...)
if cursor == 0 {
break
}
}
} else {
cursor, _ = getEntryCursor()
loadedKey, cursor, err = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
items = append(items, loadedKey...)
}
setEntryCursor(cursor)
return items, cursor == 0, nil
}
data.Value, data.End, err = loadSetHandle()
if err != nil {
resp.Msg = err.Error()
return
}
case "zset":
loadZSetHandle := func() ([]types.ZSetItem, bool, error) {
var items []types.ZSetItem
var cursor uint64
scanSize := int64(Preferences().GetScanSize())
var loadedVal []string
if param.Full {
// load all
cursor = 0
for {
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
if err != nil {
return items, false, err
}
var score float64
for i := 0; i < len(loadedVal); i += 2 {
if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
items = append(items, types.ZSetItem{
Value: loadedVal[i],
Score: score,
})
}
}
if cursor == 0 {
break
}
}
} else {
cursor, _ = getEntryCursor()
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
var score float64
for i := 0; i < len(loadedVal); i += 2 {
if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
items = append(items, types.ZSetItem{
Value: loadedVal[i],
Score: score,
})
}
}
}
setEntryCursor(cursor)
return items, cursor == 0, nil
}
data.Value, data.End, err = loadZSetHandle()
if err != nil {
resp.Msg = err.Error()
return
}
case "stream":
loadStreamHandle := func() ([]types.StreamItem, bool, error) {
var msgs []redis.XMessage
var items []types.StreamItem
var last string
if param.Full {
// load all
last = ""
msgs, err = client.XRevRange(ctx, key, "+", "-").Result()
} else {
scanSize := int64(Preferences().GetScanSize())
_, last = getEntryCursor()
if len(last) <= 0 {
last = "+"
}
if last != "+" {
// add 1 more item when continue scan
msgs, err = client.XRevRangeN(ctx, key, last, "-", scanSize+1).Result()
msgs = msgs[1:]
} else {
msgs, err = client.XRevRangeN(ctx, key, last, "-", scanSize).Result()
}
scanCount := len(msgs)
if scanCount <= 0 || scanCount < int(scanSize) {
last = ""
} else if scanCount > 0 {
last = msgs[scanCount-1].ID
}
}
setEntryXLast(last)
for _, msg := range msgs {
items = append(items, types.StreamItem{
ID: msg.ID,
Value: msg.Values,
})
}
if err != nil {
return items, false, err
}
return items, last == "", nil
}
data.Value, data.End, err = loadStreamHandle()
if err != nil {
resp.Msg = err.Error()
return
}
}
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true
resp.Data = data
return
}
// GetKeyValue get value by key
func (b *browserService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db)
@ -774,6 +1124,7 @@ func (b *browserService) SetHashValue(connName string, db int, k any, field, new
key := strutil.DecodeRedisKey(k)
var removedField []string
updatedField := map[string]string{}
replacedField := map[string]string{}
if len(field) <= 0 {
// old filed is empty, add new field
_, err = client.HSet(ctx, key, newField, value).Result()
@ -795,6 +1146,7 @@ func (b *browserService) SetHashValue(connName string, db int, k any, field, new
_, err = client.HSet(ctx, key, newField, value).Result()
removedField = append(removedField, field)
updatedField[newField] = value
replacedField[field] = newField
}
if err != nil {
resp.Msg = err.Error()
@ -803,8 +1155,9 @@ func (b *browserService) SetHashValue(connName string, db int, k any, field, new
resp.Success = true
resp.Data = map[string]any{
"removed": removedField,
"updated": updatedField,
"removed": removedField,
"updated": updatedField,
"replaced": replacedField,
}
return
}
@ -944,10 +1297,11 @@ func (b *browserService) SetSetItem(connName string, db int, k any, remove bool,
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k)
var affected int64
if remove {
_, err = client.SRem(ctx, key, members...).Result()
affected, err = client.SRem(ctx, key, members...).Result()
} else {
_, err = client.SAdd(ctx, key, members...).Result()
affected, err = client.SAdd(ctx, key, members...).Result()
}
if err != nil {
resp.Msg = err.Error()
@ -955,6 +1309,9 @@ func (b *browserService) SetSetItem(connName string, db int, k any, remove bool,
}
resp.Success = true
resp.Data = map[string]any{
"affected": affected,
}
return
}
@ -1003,6 +1360,9 @@ func (b *browserService) UpdateZSetValue(connName string, db int, k any, value,
Score: score,
Member: value,
}).Result()
if err == nil {
updated[value] = score
}
} else {
// remove old value and add new one
_, err = client.ZRem(ctx, key, value).Result()
@ -1075,7 +1435,8 @@ func (b *browserService) AddStreamValue(connName string, db int, k any, ID strin
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k)
_, err = client.XAdd(ctx, &redis.XAddArgs{
var updateID string
updateID, err = client.XAdd(ctx, &redis.XAddArgs{
Stream: key,
ID: ID,
Values: fieldItems,
@ -1086,6 +1447,9 @@ func (b *browserService) AddStreamValue(connName string, db int, k any, ID strin
}
resp.Success = true
resp.Data = map[string]any{
"updateID": updateID,
}
return
}
@ -1099,8 +1463,18 @@ func (b *browserService) RemoveStreamValues(connName string, db int, k any, IDs
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(k)
_, err = client.XDel(ctx, key, IDs...).Result()
var affected int64
affected, err = client.XDel(ctx, key, IDs...).Result()
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true
resp.Data = map[string]any{
"affected": affected,
}
return
}

View File

@ -5,3 +5,35 @@ type JSResp struct {
Msg string `json:"msg"`
Data any `json:"data,omitempty"`
}
type KeySummaryParam struct {
Server string `json:"server"`
DB int `json:"db"`
Key any `json:"key"`
}
type KeySummary struct {
Type string `json:"type"`
TTL int64 `json:"ttl"`
Size int64 `json:"size"`
Length int64 `json:"length"`
}
type KeyDetailParam struct {
Server string `json:"server"`
DB int `json:"db"`
Key any `json:"key"`
ViewAs string `json:"viewAs,omitempty"`
DecodeType string `json:"decodeType,omitempty"`
MatchPattern string `json:"matchPattern,omitempty"`
Reset bool `json:"reset"`
Full bool `json:"full"`
}
type KeyDetail struct {
Value any `json:"value"`
Length int64 `json:"length,omitempty"`
ViewAs string `json:"viewAs,omitempty"`
DecodeType string `json:"decodeType,omitempty"`
End bool `json:"end"`
}

View File

@ -26,7 +26,7 @@ const props = defineProps({
const data = reactive({
navMenuWidth: 60,
toolbarHeight: 45,
toolbarHeight: 38,
})
const tabStore = useTabStore()

View File

@ -56,11 +56,13 @@ const tabContent = computed(() => {
length: tab.length || 0,
viewAs: tab.viewAs,
decode: tab.decode,
end: tab.end,
loading: tab.loading === true,
}
})
const isBlankValue = computed(() => {
return tabContent.value.value == null
return tabContent.value?.keyPath == null
})
const selectedSubTab = computed(() => {
@ -133,19 +135,7 @@ watch(
<span>{{ $t('interface.sub_tab.key_detail') }}</span>
</n-space>
</template>
<content-value-wrapper
:blank="isBlankValue"
:db="tabContent.db"
:decode="tabContent.decode"
:key-code="tabContent.keyCode"
:key-path="tabContent.keyPath"
:length="tabContent.length"
:name="tabContent.name"
:size="tabContent.size"
:ttl="tabContent.ttl"
:type="tabContent.type"
:value="tabContent.value"
:view-as="tabContent.viewAs" />
<content-value-wrapper :blank="isBlankValue" :content="tabContent" />
</n-tab-pane>
<!-- cli pane -->

View File

@ -1,9 +1,8 @@
<script setup>
import { h, onMounted, onUnmounted, reactive, ref } from 'vue'
import Refresh from '@/components/icons/Refresh.vue'
import { debounce, isEmpty, map, size, split } from 'lodash'
import { debounce, isEmpty, map, size } from 'lodash'
import { useI18n } from 'vue-i18n'
import dayjs from 'dayjs'
import { useThemeVars } from 'naive-ui'
import useBrowserStore from 'stores/browser.js'

View File

@ -11,9 +11,7 @@ import IconButton from '@/components/common/IconButton.vue'
import Copy from '@/components/icons/Copy.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { computed } from 'vue'
import { isEmpty, padStart } from 'lodash'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
import { padStart } from 'lodash'
const props = defineProps({
server: String,
@ -34,32 +32,18 @@ const props = defineProps({
type: Number,
default: -1,
},
viewAs: {
type: String,
default: formatTypes.PLAIN_TEXT,
},
decode: {
type: String,
default: decodeTypes.NONE,
},
loading: Boolean,
})
const emit = defineEmits(['reload', 'rename', 'delete'])
const dialogStore = useDialog()
const browserStore = useBrowserStore()
const i18n = useI18n()
const binaryKey = computed(() => {
return !!props.keyCode
})
/**
*
* @type {ComputedRef<string|number[]>}
*/
const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
})
const ttlString = computed(() => {
let s = ''
if (props.ttl > 0) {
@ -77,10 +61,6 @@ const ttlString = computed(() => {
return s
})
const onReloadKey = () => {
browserStore.loadKeyValue(props.server, props.db, keyName.value, props.viewAs, props.decode)
}
const onCopyKey = () => {
ClipboardSetText(props.keyPath)
.then((succ) => {
@ -92,33 +72,20 @@ const onCopyKey = () => {
$message.error(e.message)
})
}
const onRenameKey = () => {
if (binaryKey.value) {
$message.error(i18n.t('dialogue.rename_binary_key_fail'))
} else {
dialogStore.openRenameKeyDialog(props.server, props.db, props.keyPath)
}
}
const onDeleteKey = () => {
$dialog.warning(i18n.t('dialogue.remove_tip', { name: props.keyPath }), () => {
browserStore.deleteKey(props.server, props.db, keyName.value).then((success) => {
if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: props.keyPath }))
}
})
})
}
</script>
<template>
<div class="content-toolbar flex-box-h">
<n-input-group>
<redis-type-tag :binary-key="binaryKey" :type="props.keyType" size="large" />
<n-input v-model:value="props.keyPath">
<n-input v-model:value="props.keyPath" readonly>
<template #suffix>
<icon-button :icon="Refresh" size="18" t-tooltip="interface.reload" @click="onReloadKey" />
<icon-button
:icon="Refresh"
:loading="props.loading"
size="18"
t-tooltip="interface.reload"
@click="emit('reload')" />
</template>
</n-input>
<icon-button :icon="Copy" border size="18" t-tooltip="interface.copy_key" @click="onCopyKey" />
@ -135,11 +102,11 @@ const onDeleteKey = () => {
</template>
TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }}
</n-tooltip>
<icon-button :icon="Edit" border size="18" t-tooltip="interface.rename_key" @click="onRenameKey" />
<icon-button :icon="Edit" border size="18" t-tooltip="interface.rename_key" @click="emit('rename')" />
</n-button-group>
<n-tooltip :show-arrow="false">
<template #trigger>
<n-button :focusable="false" @click="onDeleteKey">
<n-button :focusable="false" @click="emit('delete')">
<template #icon>
<n-icon :component="Delete" size="18" />
</template>

View File

@ -7,10 +7,13 @@ import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import useDialogStore from 'stores/dialog.js'
import { isEmpty } from 'lodash'
import { isEmpty, size } from 'lodash'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -149,14 +156,7 @@ const actionColumn = {
row.key,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.key }))
// update display value
// if (!isEmpty(removed)) {
// for (const elem of removed) {
// delete props.value[elem]
// }
// }
} else {
$message.error(msg)
}
@ -175,14 +175,7 @@ const actionColumn = {
currentEditRow.value.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ'))
// update display value
// if (!isEmpty(updated)) {
// for (const key in updated) {
// props.value[key] = updated[key]
// }
// }
} else {
$message.error(msg)
}
@ -223,6 +216,12 @@ const tableData = computed(() => {
}
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.HASH)
}
@ -268,14 +267,16 @@ const onUpdateFilter = (filters, sourceColumn) => {
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
:ttl="ttl"
:view-as="props.viewAs"
class="value-item-part" />
class="value-item-part"
@delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h">
<n-input-group>
@ -294,6 +295,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group>
</div>
<div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddRow">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -301,24 +318,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</div>
<div class="value-wrapper value-item-part fill-height flex-box-h">
<div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table
:key="(row) => row.no"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@update:filters="onUpdateFilter" />
</div>
<div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text>
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div>

View File

@ -11,6 +11,9 @@ import useDialogStore from 'stores/dialog.js'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -106,12 +113,7 @@ const actionColumn = {
row.no - 1,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: '#' + row.no }))
// update display value
// if (!isEmpty(removed)) {
// props.value.splice(removed[0], 1)
// }
} else {
$message.error(msg)
}
@ -129,14 +131,7 @@ const actionColumn = {
currentEditRow.value.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ'))
// update display value
// if (!isEmpty(updated)) {
// for (const key in updated) {
// props.value[key] = updated[key]
// }
// }
} else {
$message.error(msg)
}
@ -178,6 +173,11 @@ const tableData = computed(() => {
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddValue = (value) => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.LIST)
}
@ -200,14 +200,16 @@ const onUpdateFilter = (filters, sourceColumn) => {
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
:ttl="ttl"
:view-as="props.viewAs"
class="value-item-part" />
class="value-item-part"
@delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h">
<n-input
@ -218,6 +220,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
@update:value="onFilterInput" />
</div>
<div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddValue">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -225,24 +243,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</div>
<div class="value-wrapper value-item-part fill-height flex-box-h">
<div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table
:key="(row) => row.no"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@update:filters="onUpdateFilter" />
</div>
<div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text>
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div>

View File

@ -11,6 +11,9 @@ import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -107,10 +114,7 @@ const actionColumn = {
row.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.value }))
// update display value
// props.value.splice(row.no - 1, 1)
} else {
$message.error(msg)
}
@ -128,10 +132,7 @@ const actionColumn = {
currentEditRow.value.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ'))
// update display value
// props.value[row.no - 1] = currentEditRow.value.value
} else {
$message.error(msg)
}
@ -173,6 +174,11 @@ const tableData = computed(() => {
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddValue = (value) => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET)
}
@ -195,14 +201,16 @@ const onUpdateFilter = (filters, sourceColumn) => {
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
:ttl="ttl"
:view-as="props.viewAs"
class="value-item-part" />
class="value-item-part"
@delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h">
<n-input
@ -213,6 +221,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
@update:value="onFilterInput" />
</div>
<div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddValue">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -220,24 +244,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</div>
<div class="value-wrapper value-item-part fill-height flex-box-h">
<div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table
:key="(row) => row.no"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@update:filters="onUpdateFilter" />
</div>
<div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text>
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div>

View File

@ -7,10 +7,13 @@ import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import useDialogStore from 'stores/dialog.js'
import { includes, isEmpty, keys, some, values } from 'lodash'
import { includes, isEmpty, keys, size, some, values } from 'lodash'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -27,7 +30,10 @@ const props = defineProps({
type: Number,
default: -1,
},
value: Object,
value: {
type: Array,
default: () => [],
},
size: Number,
length: Number,
viewAs: {
@ -38,8 +44,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -115,14 +125,7 @@ const actionColumn = {
row.id,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.id }))
// update display value
// if (!isEmpty(removed)) {
// for (const elem of removed) {
// delete props.value[elem]
// }
// }
} else {
$message.error(msg)
}
@ -137,15 +140,22 @@ const columns = reactive([idColumn, valueColumn, actionColumn])
const tableData = computed(() => {
const data = []
for (const elem of props.value) {
data.push({
id: elem.id,
value: elem.value,
})
if (!isEmpty(props.value)) {
for (const elem of props.value) {
data.push({
id: elem.id,
value: elem.value,
})
}
}
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.STREAM)
}
@ -180,14 +190,16 @@ const onUpdateFilter = (filters, sourceColumn) => {
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
:ttl="ttl"
:view-as="props.viewAs"
class="value-item-part" />
class="value-item-part"
@delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h">
<n-input-group>
@ -206,6 +218,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group>
</div>
<div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddRow">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -213,17 +241,18 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</div>
<div class="value-wrapper value-item-part fill-height flex-box-h">
<div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table
:key="(row) => row.id"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@ -231,7 +260,7 @@ const onUpdateFilter = (filters, sourceColumn) => {
</div>
<div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text>
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div>

View File

@ -43,8 +43,11 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
loading: Boolean,
})
const emit = defineEmits(['reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -53,16 +56,6 @@ const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
})
// const viewOption = computed(() =>
// map(types, (t) => {
// return {
// value: t,
// label: t,
// key: t,
// }
// }),
// )
const keyType = redisTypes.STRING
const viewLanguage = computed(() => {
switch (props.viewAs) {
@ -74,11 +67,23 @@ const viewLanguage = computed(() => {
})
const onViewTypeUpdate = (viewType) => {
browserStore.loadKeyValue(props.name, props.db, keyName.value, viewType, props.decode)
browserStore.loadKeyDetail({
server: props.name,
db: props.db,
key: keyName.value,
viewType,
decodeType: props.decode,
})
}
const onDecodeTypeUpdate = (decodeType) => {
browserStore.loadKeyValue(props.name, props.db, keyName.value, props.viewAs, decodeType)
browserStore.loadKeyDetail({
server: props.name,
db: props.db,
key: keyName.value,
viewType: props.viewAs,
decodeType,
})
}
/**
@ -126,7 +131,7 @@ const onSaveValue = async () => {
props.decode,
)
if (success) {
await browserStore.loadKeyValue(props.name, props.db, keyName.value)
await browserStore.loadKeyDetail({ server: props.name, db: props.db, key: keyName.value })
$message.success(i18n.t('dialogue.save_value_succ'))
} else {
$message.error(msg)
@ -144,14 +149,16 @@ const onSaveValue = async () => {
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="keyCode"
:key-path="keyPath"
:key-type="keyType"
:loading="loading"
:server="props.name"
:ttl="ttl"
:view-as="props.viewAs"
class="value-item-part" />
class="value-item-part"
@delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h">
<div class="flex-item-expand"></div>
<n-button-group v-if="!inEdit">

View File

@ -1,5 +1,4 @@
<script setup>
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { types as redisTypes } from '@/consts/support_redis_type.js'
import ContentValueString from '@/components/content_value/ContentValueString.vue'
import ContentValueHash from '@/components/content_value/ContentValueHash.vue'
@ -9,37 +8,48 @@ import ContentValueZset from '@/components/content_value/ContentValueZSet.vue'
import ContentValueStream from '@/components/content_value/ContentValueStream.vue'
import { useThemeVars } from 'naive-ui'
import useBrowserStore from 'stores/browser.js'
import { computed, onMounted, watch } from 'vue'
import { isEmpty } from 'lodash'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useDialogStore from 'stores/dialog.js'
const themeVars = useThemeVars()
const browserStore = useBrowserStore()
const dialogStore = useDialogStore()
const props = defineProps({
blank: Boolean,
type: String,
name: String,
db: Number,
keyPath: String,
keyCode: {
type: Array,
default: null,
},
ttl: {
type: Number,
default: -1,
},
value: [String, Object],
size: Number,
length: Number,
viewAs: {
type: String,
default: formatTypes.PLAIN_TEXT,
},
decode: {
type: String,
default: decodeTypes.NONE,
content: {
type: Object,
default: {},
},
})
/**
*
* @type {ComputedRef<{
* type:
* String, name: String,
* db: Number,
* keyPath: String,
* keyCode: Array,
* ttl: Number,
* value: [String, Object],
* size: Number,
* length: Number,
* viewAs: String,
* decode: String,
* end: Boolean
* }>}
*/
const data = computed(() => {
return props.content
})
const binaryKey = computed(() => {
return !!data.value.keyCode
})
const valueComponents = {
[redisTypes.STRING]: ContentValueString,
[redisTypes.HASH]: ContentValueHash,
@ -49,34 +59,103 @@ const valueComponents = {
[redisTypes.STREAM]: ContentValueStream,
}
/**
* reload current selection key
* @returns {Promise<null>}
*/
const onReloadKey = async () => {
await browserStore.loadKeyValue(props.name, props.db, props.key, props.viewAs)
const keyName = computed(() => {
return !isEmpty(data.value.keyCode) ? data.value.keyCode : data.value.keyPath
})
const loadData = async (reset, full) => {
try {
const { name, db, view, decodeType, matchPattern } = data.value
await browserStore.loadKeyDetail({
server: name,
db: db,
key: keyName.value,
viewType: view,
decodeType: decodeType,
matchPattern: matchPattern,
reset: reset === true,
full: full === true,
})
} finally {
}
}
const onReload = async () => {
try {
const { name, db, keyCode, keyPath } = data.value
await browserStore.reloadKey({ server: name, db, key: keyCode || keyPath })
} finally {
}
}
const onRename = () => {
const { name, db, keyPath } = data.value
if (binaryKey.value) {
$message.error(i18n.t('dialogue.rename_binary_key_fail'))
} else {
dialogStore.openRenameKeyDialog(name, db, keyPath)
}
}
const onDelete = () => {
$dialog.warning(i18n.t('dialogue.remove_tip', { name: props.keyPath }), () => {
const { name, db } = data.value
browserStore.deleteKey(name, db, keyName.value).then((success) => {
if (success) {
$message.success(i18n.t('dialogue.delete_key_succ', { key: props.keyPath }))
}
})
})
}
const onLoadMore = () => {
loadData(false, false)
}
const onLoadAll = () => {
loadData(false, true)
}
onMounted(() => {
// onReload()
loadData(false, false)
})
watch(
() => data.value?.keyPath,
() => {
// onReload()
loadData(false, false)
},
)
</script>
<template>
<n-empty v-if="props.blank" :description="$t('interface.nonexist_tab_content')" class="empty-content">
<template #extra>
<n-button :focusable="false" @click="onReloadKey">{{ $t('interface.reload') }}</n-button>
<n-button :focusable="false" @click="onReload">{{ $t('interface.reload') }}</n-button>
</template>
</n-empty>
<keep-alive v-else>
<component
:is="valueComponents[props.type]"
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:length="props.length"
:name="props.name"
:size="props.size"
:ttl="props.ttl"
:value="props.value"
:view-as="props.viewAs" />
:is="valueComponents[data.type]"
:db="data.db"
:decode="data.decode || decodeTypes.NONE"
:end="data.end"
:key-code="data.keyCode"
:key-path="data.keyPath"
:length="data.length"
:loading="data.loading === true"
:name="data.name"
:size="data.size"
:ttl="data.ttl"
:value="data.value"
:view-as="data.viewAs || formatTypes.PLAIN_TEXT"
@delete="onDelete"
@loadall="onLoadAll"
@loadmore="onLoadMore"
@reload="onReload"
@rename="onRename" />
</keep-alive>
</template>

View File

@ -6,11 +6,14 @@ import AddLink from '@/components/icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, NInputNumber, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import { isEmpty } from 'lodash'
import { isEmpty, size } from 'lodash'
import useDialogStore from 'stores/dialog.js'
import bytes from 'bytes'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useBrowserStore from 'stores/browser.js'
import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import IconButton from '@/components/common/IconButton.vue'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -38,8 +41,12 @@ const props = defineProps({
type: String,
default: decodeTypes.NONE,
},
end: Boolean,
loading: Boolean,
})
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
/**
*
* @type {ComputedRef<string|number[]>}
@ -178,7 +185,6 @@ const actionColumn = {
row.value,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.delete_key_succ', { key: row.value }))
} else {
$message.error(msg)
@ -203,7 +209,6 @@ const actionColumn = {
currentEditRow.value.score,
)
if (success) {
browserStore.loadKeyValue(props.name, props.db, keyName.value).then((r) => {})
$message.success(i18n.t('dialogue.save_value_succ'))
} else {
$message.error(msg)
@ -235,17 +240,24 @@ const columns = reactive([
const tableData = computed(() => {
const data = []
let index = 0
for (const elem of props.value) {
data.push({
no: ++index,
value: elem.value,
score: elem.score,
})
if (!isEmpty(props.value)) {
let index = 0
for (const elem of props.value) {
data.push({
no: ++index,
value: elem.value,
score: elem.score,
})
}
}
return data
})
const entries = computed(() => {
const len = size(tableData.value)
return `${len} / ${Math.max(len, props.length)}`
})
const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET)
}
@ -293,14 +305,16 @@ const onUpdateFilter = (filters, sourceColumn) => {
<div class="content-wrapper flex-box-v">
<content-toolbar
:db="props.db"
:decode="props.decode"
:key-code="props.keyCode"
:key-path="props.keyPath"
:key-type="keyType"
:loading="props.loading"
:server="props.name"
:ttl="ttl"
:view-as="props.viewAs"
class="value-item-part" />
class="value-item-part"
@delete="emit('delete')"
@reload="emit('reload')"
@rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h">
<n-input-group>
@ -324,6 +338,22 @@ const onUpdateFilter = (filters, sourceColumn) => {
</n-input-group>
</div>
<div class="flex-item-expand"></div>
<n-button-group>
<icon-button
:disabled="props.end || props.loading"
:icon="LoadList"
border
size="18"
t-tooltip="interface.load_more_entries"
@click="emit('loadmore')" />
<icon-button
:disabled="props.end || props.loading"
:icon="LoadAll"
border
size="18"
t-tooltip="interface.load_all_entries"
@click="emit('loadall')" />
</n-button-group>
<n-button :focusable="false" plain @click="onAddRow">
<template #icon>
<n-icon :component="AddLink" size="18" />
@ -331,24 +361,25 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('interface.add_row') }}
</n-button>
</div>
<div class="value-wrapper value-item-part fill-height flex-box-h">
<div class="value-wrapper value-item-part flex-box-v flex-item-expand">
<n-data-table
:key="(row) => row.no"
:bordered="false"
:bottom-bordered="false"
:columns="columns"
:data="tableData"
:loading="props.loading"
:single-column="true"
:single-line="false"
class="flex-item-expand"
flex-height
max-height="100%"
size="small"
striped
virtual-scroll
@update:filters="onUpdateFilter" />
</div>
<div class="value-footer flex-box-h">
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ props.length }}</n-text>
<n-text v-if="!isNaN(props.length)">{{ $t('interface.entries') }}: {{ entries }}</n-text>
<n-divider v-if="!isNaN(props.length)" vertical />
<n-text v-if="!isNaN(props.size)">{{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}</n-text>
<div class="flex-item-expand"></div>

View File

@ -11,6 +11,7 @@ import AddZSetValue from '@/components/new_value/AddZSetValue.vue'
import NewStreamValue from '@/components/new_value/NewStreamValue.vue'
import { isEmpty, size, slice } from 'lodash'
import useBrowserStore from 'stores/browser.js'
import useTabStore from 'stores/tab.js'
const i18n = useI18n()
const newForm = reactive({
@ -79,6 +80,7 @@ watch(
)
const browserStore = useBrowserStore()
const tab = useTabStore()
const onAdd = async () => {
try {
const { server, db, key, keyCode, type } = newForm
@ -87,6 +89,7 @@ const onAdd = async () => {
value = defaultValue[type]
}
const keyName = isEmpty(keyCode) ? key : keyCode
let updated = false
switch (type) {
case types.LIST:
{
@ -98,9 +101,7 @@ const onAdd = async () => {
}
const { success, msg } = data
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -112,9 +113,7 @@ const onAdd = async () => {
{
const { success, msg } = await browserStore.addHashField(server, db, keyName, newForm.opType, value)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -126,9 +125,7 @@ const onAdd = async () => {
{
const { success, msg } = await browserStore.addSetItem(server, db, keyName, value)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -140,9 +137,7 @@ const onAdd = async () => {
{
const { success, msg } = await browserStore.addZSetItem(server, db, keyName, newForm.opType, value)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -161,9 +156,7 @@ const onAdd = async () => {
slice(value, 1),
)
if (success) {
if (newForm.reload) {
browserStore.loadKeyValue(server, db, keyName).then(() => {})
}
updated = true
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)
@ -172,6 +165,12 @@ const onAdd = async () => {
}
break
}
if (updated) {
if (newForm.reload) {
browserStore.reloadKey({ server, db, key: keyName })
}
}
dialogStore.closeAddFieldsDialog()
} catch (e) {
$message.error(e.message)

View File

@ -130,7 +130,7 @@ const onAdd = async () => {
if (success) {
// select current key
tabStore.setSelectedKeys(server, nodeKey)
browserStore.loadKeyValue(server, db, key).then(() => {})
browserStore.loadKeySummary({ server, db, key })
} else if (!isEmpty(msg)) {
$message.error(msg)
}

View File

@ -3,6 +3,7 @@ import { reactive, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js'
import useTabStore from 'stores/tab.js'
const renameForm = reactive({
server: '',
@ -13,6 +14,7 @@ const renameForm = reactive({
const dialogStore = useDialog()
const browserStore = useBrowserStore()
const tab = useTabStore()
watch(
() => dialogStore.renameDialogVisible,
(visible) => {
@ -30,9 +32,10 @@ const i18n = useI18n()
const onRename = async () => {
try {
const { server, db, key, newKey } = renameForm
const { success, msg } = await browserStore.renameKey(server, db, key, newKey)
const { success, msg, nodeKey } = await browserStore.renameKey(server, db, key, newKey)
if (success) {
await browserStore.loadKeyValue(server, db, newKey)
tab.setSelectedKeys(server, nodeKey)
browserStore.loadKeySummary({ server, db, key: newKey })
$message.success(i18n.t('dialogue.handle_succ'))
} else {
$message.error(msg)

View File

@ -276,7 +276,11 @@ const handleSelectContextMenu = (key) => {
// browserStore.loadKeys(props.server, db, redisKey)
// break
case 'value_reload':
browserStore.loadKeyValue(props.server, db, redisKey)
browserStore.reloadKey({
server: props.server,
db,
key: redisKey,
})
break
case 'key_remove':
dialogStore.openDeleteKeyDialog(props.server, db, isEmpty(redisKey) ? '*' : redisKey + ':*')
@ -378,14 +382,18 @@ const onUpdateSelectedKeys = (keys, options) => {
const { key, db } = node
const redisKey = node.redisKeyCode || node.redisKey
if (!includes(selectedKeys.value, key)) {
browserStore.loadKeyValue(props.server, db, redisKey)
browserStore.loadKeySummary({
server: props.server,
db,
key: redisKey,
})
}
return
}
}
}
// default is load blank key to display server status
browserStore.loadKeyValue(props.server, 0)
tabStore.openBlank(props.server)
} finally {
tabStore.setSelectedKeys(props.server, keys)
}
@ -640,7 +648,7 @@ const nodeProps = ({ option }) => {
contextMenuParam.x = e.clientX
contextMenuParam.y = e.clientY
contextMenuParam.show = true
tabStore.setSelectedKeys(props.server, option.key)
onUpdateSelectedKeys([option.key], [option])
})
},
// onMouseover() {

View File

@ -90,6 +90,8 @@
"new_key": "Add Key",
"load_more": "Load More Keys",
"load_all": "Load All Left Keys",
"load_more_entries": "Load More",
"load_all_entries": "Load All",
"more_action": "More Action",
"nonexist_tab_content": "Selected key does not exist or no key is selected. Please retry",
"empty_server_content": "Select and open a connection from the left",

View File

@ -90,6 +90,8 @@
"new_key": "添加新键",
"load_more": "加载更多键",
"load_all": "加载剩余所有键",
"load_more_entries": "加载更多",
"load_all_entries": "加载全部",
"more_action": "更多操作",
"nonexist_tab_content": "所选键不存在或未选中任何键,请尝试刷新重试",
"empty_server_content": "可以从左边选择并打开连接",

View File

@ -24,7 +24,8 @@ import {
DeleteKey,
FlushDB,
GetCmdHistory,
GetKeyValue,
GetKeyDetail,
GetKeySummary,
GetSlowLogs,
LoadAllKeys,
LoadNextKeys,
@ -58,7 +59,7 @@ const useBrowserStore = defineStore('browser', {
* @property {number} type
* @property {number} [db] - database index, type == ConnectionType.RedisDB only
* @property {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue only
* @property {string} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
* @property {number[]} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data
* @property {number} [keys] - children key count
* @property {number} [maxKeys] - max key count for database
* @property {boolean} [isLeaf]
@ -344,20 +345,23 @@ const useBrowserStore = defineStore('browser', {
},
/**
* load redis key
* load key summary info
* @param {string} server
* @param {number} db
* @param {string|number[]} [key] when key is null or blank, update tab to display normal content (blank content or server status)
* @param {string} [viewType]
* @param {string} [decodeType]
* @param {string|number[]} [key] null or blank indicate that update tab to display normal content (blank content or server status)
* @return {Promise<void>}
*/
async loadKeyValue(server, db, key, viewType, decodeType) {
async loadKeySummary({ server, db, key }) {
try {
const tab = useTabStore()
if (!isEmpty(key)) {
const { data, success, msg } = await GetKeyValue(server, db, key, viewType, decodeType)
const { data, success, msg } = await GetKeySummary({
server,
db,
key,
})
if (success) {
const { type, ttl, value, size, length, viewAs, decode } = data
const { type, ttl, size, length } = data
const k = decodeRedisKey(key)
const binaryKey = k !== key
tab.upsertTab({
@ -368,16 +372,13 @@ const useBrowserStore = defineStore('browser', {
ttl,
keyCode: binaryKey ? key : undefined,
key: k,
value,
size,
length,
viewAs,
decode,
})
return
} else {
if (!isEmpty(msg)) {
$message.error('load key fail: ' + msg)
$message.error('load key summary fail: ' + msg)
}
// its danger to delete "non-exists" key, just remove from tree view
await this.deleteKey(server, db, key, true)
@ -397,10 +398,75 @@ const useBrowserStore = defineStore('browser', {
size: 0,
length: 0,
})
} catch (e) {
$message.error('')
} finally {
}
},
/**
* reload key
* @param server
* @param db
* @param key
* @return {Promise<void>}
*/
async reloadKey({ server, db, key }) {
const tab = useTabStore()
try {
tab.updateLoading({ server, db, loading: true })
await this.loadKeySummary({ server, db, key })
await this.loadKeyDetail({ server, db, key, reset: true })
} finally {
tab.updateLoading({ server, db, loading: false })
}
},
/**
* load key content
* @param {string} server
* @param {number} db
* @param {string|number[]} key
* @param {string} [viewType]
* @param {string} [decodeType]
* @param {string} [matchPattern]
* @param {boolean} [reset]
* @param {boolean} [full]
* @return {Promise<void>}
*/
async loadKeyDetail({ server, db, key, viewType, decodeType, matchPattern, reset, full }) {
const tab = useTabStore()
try {
tab.updateLoading({ server, db, loading: true })
const { data, success, msg } = await GetKeyDetail({
server,
db,
key,
viewAs: viewType,
decodeType,
matchPattern,
full: full === true,
reset,
lite: true,
})
if (success) {
const { value, viewAs, decodeType: decode, end } = data
tab.updateValue({
server,
db,
key: decodeRedisKey(key),
value,
viewAs,
decode,
reset: reset || full === true,
end,
})
}
} finally {
tab.updateLoading({ server, db, loading: false })
}
},
/**
* scan keys with prefix
* @param {string} connName
@ -857,7 +923,14 @@ const useBrowserStore = defineStore('browser', {
try {
const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '')
if (success) {
const { updated = {} } = data
const { updated = {}, removed = [], replaced = {} } = data
const tab = useTabStore()
if (!isEmpty(removed)) {
tab.removeValueEntries({ server: connName, db, key, type: 'hash', entries: removed })
}
if (!isEmpty(updated)) {
tab.upsertValueEntries({ server: connName, db, key, type: 'hash', entries: updated })
}
return { success, updated }
} else {
return { success, msg }
@ -881,6 +954,8 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
if (success) {
const { updated = {} } = data
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'hash', entries: updated })
return { success, updated }
} else {
return { success: false, msg }
@ -903,6 +978,10 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '')
if (success) {
const { removed = [] } = data
if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'hash', entries: removed })
}
return { success, removed }
} else {
return { success, msg }
@ -935,13 +1014,24 @@ const useBrowserStore = defineStore('browser', {
* @param db
* @param key
* @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: []>}
* @returns {Promise<{[msg]: string, success: boolean, [item]: []}>}
*/
async prependListItem(connName, db, key, values) {
try {
const { data, success, msg } = await AddListItem(connName, db, key, 0, values)
if (success) {
const { left = [] } = data
if (!isEmpty(left)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: right,
prepend: true,
})
}
return { success, item: left }
} else {
return { success: false, msg }
@ -957,13 +1047,24 @@ const useBrowserStore = defineStore('browser', {
* @param db
* @param key
* @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: any[]>}
* @returns {Promise<{[msg]: string, success: boolean, [item]: any[]}>}
*/
async appendListItem(connName, db, key, values) {
try {
const { data, success, msg } = await AddListItem(connName, db, key, 1, values)
if (success) {
const { right = [] } = data
if (!isEmpty(right)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: right,
prepend: false,
})
}
return { success, item: right }
} else {
return { success: false, msg }
@ -987,6 +1088,16 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetListItem(connName, db, key, index, value)
if (success) {
const { updated = {} } = data
if (!isEmpty(updated)) {
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'list',
entries: updated,
})
}
return { success, updated }
} else {
return { success, msg }
@ -1009,6 +1120,16 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await SetListItem(connName, db, key, index, '')
if (success) {
const { removed = [] } = data
if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({
server: connName,
db,
key,
type: 'list',
entries: removed,
})
}
return { success, removed }
} else {
return { success, msg }
@ -1023,13 +1144,18 @@ const useBrowserStore = defineStore('browser', {
* @param {string} connName
* @param {number} db
* @param {string|number} key
* @param {string} value
* @param {string|string[]} value
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async addSetItem(connName, db, key, value) {
try {
const { success, msg } = await SetSetItem(connName, db, key, false, [value])
if (!value instanceof Array) {
value = [value]
}
const { data, success, msg } = await SetSetItem(connName, db, key, false, value)
if (success) {
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: value })
return { success }
} else {
return { success, msg }
@ -1052,6 +1178,8 @@ const useBrowserStore = defineStore('browser', {
try {
const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue)
if (success) {
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: { [value]: newValue } })
return { success: true }
} else {
return { success, msg }
@ -1073,6 +1201,8 @@ const useBrowserStore = defineStore('browser', {
try {
const { success, msg } = await SetSetItem(connName, db, key, true, [value])
if (success) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'set', entries: [value] })
return { success }
} else {
return { success, msg }
@ -1094,6 +1224,8 @@ const useBrowserStore = defineStore('browser', {
async addZSetItem(connName, db, key, action, vs) {
try {
const { success, msg } = await AddZSetValue(connName, db, key, action, vs)
const tab = useTabStore()
tab.upsertValueEntries({ server: connName, db, key, type: 'zset', entries: vs })
if (success) {
return { success }
} else {
@ -1119,6 +1251,13 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score)
if (success) {
const { updated, removed } = data
const tab = useTabStore()
if (!isEmpty(updated)) {
tab.upsertValueEntries({ server: connName, db, key, type: 'zset', entries: updated })
}
if (!isEmpty(removed)) {
tab.removeValueEntries({ server: connName, db, key, type: 'zset', entries: removed })
}
return { success, updated, removed }
} else {
return { success, msg }
@ -1141,7 +1280,11 @@ const useBrowserStore = defineStore('browser', {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0)
if (success) {
const { removed } = data
return { success, removed }
if (!isEmpty(removed)) {
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'zset', entries: removed })
}
return { success }
} else {
return { success, msg }
}
@ -1157,14 +1300,22 @@ const useBrowserStore = defineStore('browser', {
* @param {string|number[]} key
* @param {string} id
* @param {string[]} values field1, value1, filed2, value2...
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async addStreamValue(connName, db, key, id, values) {
try {
const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values)
if (success) {
const { updated = {} } = data
return { success, updated }
const { updateID } = data
const tab = useTabStore()
tab.upsertValueEntries({
server: connName,
db,
key,
type: 'stream',
entries: [{ id: updateID, value: values }],
})
return { success }
} else {
return { success: false, msg }
}
@ -1179,7 +1330,7 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db
* @param {string|number[]} key
* @param {string[]|string} ids
* @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
* @returns {Promise<{[msg]: {}, success: boolean}>}
*/
async removeStreamValues(connName, db, key, ids) {
if (typeof ids === 'string') {
@ -1188,8 +1339,9 @@ const useBrowserStore = defineStore('browser', {
try {
const { data = {}, success, msg } = await RemoveStreamValues(connName, db, key, ids)
if (success) {
const { removed = [] } = data
return { success, removed }
const tab = useTabStore()
tab.removeValueEntries({ server: connName, db, key, type: 'stream', entries: ids })
return { success }
} else {
return { success, msg }
}
@ -1424,7 +1576,7 @@ const useBrowserStore = defineStore('browser', {
* @param {number} db
* @param {string} key
* @param {string} newKey
* @returns {Promise<{[msg]: string, success: boolean}>}
* @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: string}>}
*/
async renameKey(connName, db, key, newKey) {
const { success = false, msg } = await RenameKey(connName, db, key, newKey)
@ -1432,7 +1584,7 @@ const useBrowserStore = defineStore('browser', {
// delete old key and add new key struct
this._deleteKeyNode(connName, db, key)
this._addKeyNodes(connName, db, [newKey])
return { success: true }
return { success: true, nodeKey: `${connName}/db${db}#${ConnectionType.RedisValue}/${newKey}` }
} else {
return { success: false, msg }
}

View File

@ -1,4 +1,4 @@
import { find, findIndex, get, isEmpty, set, size } from 'lodash'
import { assign, find, findIndex, get, indexOf, isEmpty, pullAt, remove, set, size } from 'lodash'
import { defineStore } from 'pinia'
const useTabStore = defineStore('tab', {
@ -11,12 +11,18 @@ const useTabStore = defineStore('tab', {
* @property {string} [icon] tab icon
* @property {string[]} selectedKeys
* @property {string} [type] key type
* @property {Object|Array} [value] key value
* @property {*} [value] key value
* @property {string} [server] server name
* @property {int} [db] database index
* @property {string} [key] current key name
* @property {number[]|null|undefined} [keyCode] current key name as char array
* @param {number} [size] memory usage
* @param {number} [length] length of content or entries
* @property {int} [ttl] ttl of current key
* @param {string} [viewAs]
* @param {string} [decode]
* @param {boolean} [end]
* @param {boolean} [loading]
*/
/**
@ -82,6 +88,10 @@ const useTabStore = defineStore('tab', {
}
},
openBlank(server) {
this.upsertTab({ server, db: 0 })
},
/**
* update or insert a new tab if not exists with the same name
* @param {string} subTab
@ -94,10 +104,8 @@ const useTabStore = defineStore('tab', {
* @param {number} [size]
* @param {number} [length]
* @param {*} [value]
* @param {string} [viewAs]
* @param {string} [decode]
*/
upsertTab({ subTab, server, db, type, ttl, key, keyCode, size, length, value, viewAs, decode }) {
upsertTab({ subTab, server, db, type, ttl, key, keyCode, size, length }) {
let tabIndex = findIndex(this.tabList, { name: server })
if (tabIndex === -1) {
this.tabList.push({
@ -112,9 +120,7 @@ const useTabStore = defineStore('tab', {
keyCode,
size,
length,
value,
viewAs,
decode,
value: undefined,
})
tabIndex = this.tabList.length - 1
} else {
@ -131,16 +137,239 @@ const useTabStore = defineStore('tab', {
tab.keyCode = keyCode
tab.size = size
tab.length = length
tab.value = value
tab.viewAs = viewAs
tab.decode = decode
tab.value = undefined
}
this._setActivatedIndex(tabIndex, true, subTab)
// this.activatedTab = tab.name
},
/**
* update ttl by tag
* keep update value in tab
* @param {string} server
* @param {number} db
* @param {string} key
* @param {*} value
* @param {string} [viewAs]
* @param {string] [decode]
* @param {boolean} reset
* @param {boolean} [end] keep end status if not set
*/
updateValue({ server, db, key, value, viewAs, decode, reset, end }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
tab.viewAs = viewAs || tab.viewAs
tab.decode = decode || tab.decode
if (typeof end === 'boolean') {
tab.end = end
}
if (!reset && typeof value === 'object') {
if (value instanceof Array) {
tab.value = tab.value || []
tab.value.push(...value)
} else {
tab.value = assign(value, tab.value || {})
}
} else {
tab.value = value
}
},
/**
* update or insert value entries
* @param {string} server
* @param {number} db
* @param {string} key
* @param {string} type
* @param {string[]|Object.<string, number>|Object.<number, string>} entries
* @param {boolean} [prepend] for list only
* @param {boolean} [reset]
* @param {boolean} [nocheck] ignore conflict checking for hash/set/zset
*/
upsertValueEntries({ server, db, key, type, entries, prepend, reset, nocheck }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'list': // string[] | Object.<number, string>
if (entries instanceof Array) {
// append or prepend items
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
if (prepend === true) {
tab.value = [...entries, ...tab.value]
} else {
tab.value.push(...entries)
}
tab.length += size(entries)
}
} else {
// replace by index
tab.value = tab.value || []
for (const idx in entries) {
set(tab.value, idx, entries[idx])
}
}
break
case 'hash': // Object.<string, string>
if (reset === true) {
tab.value = {}
tab.length = 0
} else {
tab.value = tab.value || {}
}
for (const k in entries) {
if (nocheck !== true && !tab.value.hasOwnProperty(k)) {
tab.length += 1
}
tab.value[k] = entries[k]
}
break
case 'set': // string[] | Object.{string, string}
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
if (entries instanceof Array) {
// add items
for (const elem of entries) {
if (nocheck !== true && indexOf(tab.value, elem) === -1) {
tab.value.push(elem)
tab.length += 1
}
}
} else {
// replace items
for (const k in entries) {
const idx = indexOf(tab.value, k)
if (idx !== -1) {
tab.value[idx] = entries[k]
} else {
tab.value.push(entries[k])
tab.length += 1
}
}
}
}
break
case 'zset': // {value: string, score: number}
if (reset === true) {
tab.value = Object.entries(entries).map(([value, score]) => ({ value, score }))
} else {
tab.value = tab.value || []
for (const val in entries) {
if (nocheck !== true) {
const ent = find(tab.value, (e) => e.value === val)
if (ent != null) {
ent.score = entries[val]
} else {
tab.value.push({ value: val, score: entries[val] })
tab.length += 1
}
} else {
tab.value.push({ value: val, score: entries[val] })
tab.length += 1
}
}
}
break
case 'stream': // [{id: string, value: []any}]
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
tab.value = [...entries, ...tab.value]
}
break
}
},
/**
* remove value entries
* @param {string} server
* @param {number} db
* @param {string} key
* @param {string} type
* @param {string[] | number[]} entries
*/
removeValueEntries({ server, db, key, type, entries }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'list': // string[] | number[]
tab.value = tab.value || []
if (typeof entries[0] === 'number') {
// remove by index、
entries.sort((a, b) => b - a)
const removed = pullAt(tab.value, ...entries)
tab.length -= size(removed)
} else {
// append or prepend items
for (const elem of entries) {
if (!isEmpty(remove(tab.value, elem))) {
tab.length -= 1
}
}
}
break
case 'hash': // string[]
tab.value = tab.value || {}
for (const k of entries) {
if (tab.value.hasOwnProperty(k)) {
delete tab.value[k]
tab.length -= 1
}
}
break
case 'set': // []string
tab.value = tab.value || []
tab.length -= size(remove(tab.value, (v) => entries.indexOf(v) >= 0))
break
case 'zset': // string[]
tab.value = tab.value || []
tab.length -= size(remove(tab.value, (v) => entries.indexOf(v.value) >= 0))
break
case 'stream': // string[]
tab.value = tab.value || []
tab.length -= size(remove(tab.value, (v) => entries.indexOf(v.id) >= 0))
break
}
},
/**
* update loading status of content in tab
* @param {string} server
* @param {number} db
* @param {boolean} loading
*/
updateLoading({ server, db, loading }) {
const tab = find(this.tabList, { name: server, db })
if (tab == null) {
return
}
tab.loading = loading
},
/**
* update ttl in tab
* @param {string} server
* @param {number} db
* @param {string|number[]} key

View File

@ -92,7 +92,8 @@ body {
.value-wrapper {
//border-top: v-bind('themeVars.borderColor') 1px solid;
user-select: text;
height: 100%;
//height: 100%;
box-sizing: border-box;
}
.value-item-part {
@ -136,3 +137,7 @@ body {
.n-modal-mask {
--wails-draggable: drag;
}
.n-tabs .n-tabs-nav {
line-height: 1.3;
}