Compare commits

...

6 Commits

Author SHA1 Message Date
tiny-craft acd3fa9304 style: update the tab style 2023-10-23 01:01:29 +08:00
tiny-craft f9fe74a6b4 perf: move key view switch to connection preferences 2023-10-22 18:32:47 +08:00
tiny-craft 92ffd6c605 perf: optimized keyboard support for context menus 2023-10-22 18:04:17 +08:00
tiny-craft c4e4ed7e79 perf: change "load all keys" to "load all left keys" 2023-10-22 02:18:38 +08:00
tiny-craft 5bbd87cc31 perf: reset expand key record when reload or close database 2023-10-22 02:09:11 +08:00
tiny-craft a669f3dfcb feat: add "tree view" and "list view" switch for keys browser 2023-10-22 01:54:22 +08:00
16 changed files with 483 additions and 169 deletions

View File

@ -507,7 +507,8 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
"db": dbs, "db": dbs,
"view": selConn.KeyView,
} }
return return
} }
@ -770,7 +771,8 @@ func (c *connectionService) LoadAllKeys(connName string, db int, match, keyType
} }
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
keys, _, err := c.scanKeys(ctx, client, match, keyType, 0, 0) cursor := item.cursor[db]
keys, _, err := c.scanKeys(ctx, client, match, keyType, cursor, 0)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return

View File

@ -15,6 +15,7 @@ type ConnectionConfig struct {
ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"` ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"`
DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"` DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"`
DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"` DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"`
KeyView int `json:"keyView,omitempty" yaml:"key_view,omitempty"`
LoadSize int `json:"loadSize,omitempty" yaml:"load_size,omitempty"` LoadSize int `json:"loadSize,omitempty" yaml:"load_size,omitempty"`
MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"` MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"`
SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"` SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"`

View File

@ -24,6 +24,10 @@ const props = defineProps({
type: [Number, String], type: [Number, String],
default: 3, default: 3,
}, },
unselectStrokeWidth: {
type: [Number, String],
default: 3,
},
}) })
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
@ -36,24 +40,26 @@ const handleSwitch = (idx) => {
</script> </script>
<template> <template>
<n-button-group> <n-button-group>
<n-tooltip <n-tooltip
:show-arrow="false" :show-arrow="false"
v-for="(icon, i) in props.icons" v-for="(icon, i) in props.icons"
:key="i" :key="i"
:disabled="!(props.tTooltips && props.tTooltips[i])"> :disabled="!(props.tTooltips && props.tTooltips[i])">
<template #trigger> <template #trigger>
<n-button :tertiary="i !== props.value" :focusable="false" :size="props.size" @click="handleSwitch(i)"> <n-button :tertiary="i !== props.value" :focusable="false" :size="props.size" @click="handleSwitch(i)">
<template #icon> <template #icon>
<n-icon :size="props.iconSize"> <n-icon :size="props.iconSize">
<component :is="icon" :stroke-width="props.strokeWidth" /> <component
</n-icon> :is="icon"
</template> :stroke-width="i !== props.value ? props.unselectStrokeWidth : props.strokeWidth" />
</n-button> </n-icon>
</template> </template>
{{ props.tTooltips ? $t(props.tTooltips[i]) : '' }} </n-button>
</n-tooltip> </template>
</n-button-group> {{ props.tTooltips ? $t(props.tTooltips[i]) : '' }}
</n-tooltip>
</n-button-group>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -7,6 +7,9 @@ import { map, uniqBy } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Delete from '@/components/icons/Delete.vue' import Delete from '@/components/icons/Delete.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useThemeVars } from 'naive-ui'
const themeVars = useThemeVars()
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
const i18n = useI18n() const i18n = useI18n()
@ -72,7 +75,8 @@ defineExpose({
:bordered="false" :bordered="false"
:title="$t('log.launch_log')" :title="$t('log.launch_log')"
class="content-container flex-box-v" class="content-container flex-box-v"
content-style="display: flex;flex-direction: column; overflow: hidden;"> content-style="display: flex;flex-direction: column; overflow: hidden; backgroundColor: gray"
:theme-overrides="{ borderRadius: '0px' }">
<n-form :disabled="data.loading" class="flex-item" inline> <n-form :disabled="data.loading" class="flex-item" inline>
<n-form-item :label="$t('log.filter_server')"> <n-form-item :label="$t('log.filter_server')">
<n-select <n-select
@ -158,5 +162,6 @@ defineExpose({
.content-container { .content-container {
padding: 5px; padding: 5px;
box-sizing: border-box; box-sizing: border-box;
border-left: 1px solid v-bind('themeVars.borderColor');
} }
</style> </style>

View File

@ -25,20 +25,20 @@ const activeTabStyle = computed(() => {
const { name } = tabStore.currentTab const { name } = tabStore.currentTab
const { markColor = '' } = connectionStore.serverProfile[name] || {} const { markColor = '' } = connectionStore.serverProfile[name] || {}
return { return {
backgroundColor: themeVars.value.bodyColor,
borderTopWidth: markColor ? '3px' : '1px', borderTopWidth: markColor ? '3px' : '1px',
borderTopColor: markColor || themeVars.value.borderColor, borderTopColor: markColor || themeVars.value.borderColor,
borderBottomColor: themeVars.value.bodyColor,
borderTopLeftRadius: themeVars.value.borderRadius,
borderTopRightRadius: themeVars.value.borderRadius,
} }
}) })
const inactiveTabStyle = computed(() => ({
borderWidth: '0 0 1px', const tabClass = (idx) => {
// borderBottomColor: themeVars.value.borderColor, if (tabStore.activatedIndex === idx) {
borderTopLeftRadius: themeVars.value.borderRadius, return ['value-tab', 'value-tab-active']
borderTopRightRadius: themeVars.value.borderRadius, } else if (tabStore.activatedIndex - 1 === idx) {
})) return ['value-tab', 'value-tab-inactive']
} else {
return ['value-tab', 'value-tab-inactive', 'value-tab-inactive2']
}
}
const tab = computed(() => const tab = computed(() =>
map(tabStore.tabs, (item) => ({ map(tabStore.tabs, (item) => ({
@ -52,6 +52,7 @@ const tab = computed(() =>
<n-tabs <n-tabs
v-model:value="tabStore.activatedIndex" v-model:value="tabStore.activatedIndex"
:closable="true" :closable="true"
:tabs-padding="2"
:tab-style="{ :tab-style="{
borderStyle: 'solid', borderStyle: 'solid',
borderWidth: '1px', borderWidth: '1px',
@ -60,7 +61,6 @@ const tab = computed(() =>
}" }"
:theme-overrides="{ :theme-overrides="{
tabFontWeightActive: 800, tabFontWeightActive: 800,
tabBorderRadius: 0,
tabGapSmallCard: 0, tabGapSmallCard: 0,
tabGapMediumCard: 0, tabGapMediumCard: 0,
tabGapLargeCard: 0, tabGapLargeCard: 0,
@ -76,10 +76,10 @@ const tab = computed(() =>
<n-tab <n-tab
v-for="(t, i) in tab" v-for="(t, i) in tab"
:key="i" :key="i"
:closable="tabStore.activatedIndex === i" :closable="true"
:name="i" :name="i"
:style="tabStore.activatedIndex === i ? activeTabStyle : inactiveTabStyle" :style="tabStore.activatedIndex === i ? activeTabStyle : undefined"
style="--wails-draggable: none" :class="tabClass(i)"
@dblclick.stop="() => {}"> @dblclick.stop="() => {}">
<n-space :size="5" :wrap-item="false" align="center" inline justify="center"> <n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon :component="ToggleServer" size="18" /> <n-icon :component="ToggleServer" size="18" />
@ -89,4 +89,38 @@ const tab = computed(() =>
</n-tabs> </n-tabs>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss">
.value-tab {
--wails-draggable: none;
position: relative;
}
.value-tab-active {
background-color: v-bind('themeVars.bodyColor') !important;
border-bottom-color: v-bind('themeVars.bodyColor') !important;
}
.value-tab-inactive {
border-color: #0000 !important;
&:hover {
background-color: v-bind('themeVars.borderColor') !important;
}
}
.value-tab-inactive2 {
&:after {
content: '';
position: absolute;
top: 25%;
height: 50%;
width: 1px;
background-color: v-bind('themeVars.borderColor');
right: -2px;
}
&:hover::after {
background-color: #0000;
}
}
</style>

View File

@ -7,6 +7,7 @@ import useDialog, { ConnDialogType } from 'stores/dialog'
import Close from '@/components/icons/Close.vue' import Close from '@/components/icons/Close.vue'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import FileOpenInput from '@/components/common/FileOpenInput.vue' import FileOpenInput from '@/components/common/FileOpenInput.vue'
import { KeyViewType } from '@/consts/key_view_type.js'
/** /**
* Dialog for new or edit connection * Dialog for new or edit connection
@ -258,37 +259,47 @@ const onClose = () => {
:rules="generalFormRules()" :rules="generalFormRules()"
:show-require-mark="false" :show-require-mark="false"
label-placement="top"> label-placement="top">
<n-form-item :label="$t('dialogue.connection.conn_name')" path="name" required> <n-grid :x-gap="10">
<n-input <n-form-item-gi
v-model:value="generalForm.name" :label="$t('dialogue.connection.conn_name')"
:placeholder="$t('dialogue.connection.name_tip')" /> :span="24"
</n-form-item> path="name"
<n-form-item v-if="!isEditMode" :label="$t('dialogue.connection.group')" required> required>
<n-select v-model:value="generalForm.group" :options="groupOptions" /> <n-input
</n-form-item> v-model:value="generalForm.name"
<n-form-item :label="$t('dialogue.connection.addr')" path="addr" required> :placeholder="$t('dialogue.connection.name_tip')" />
<n-input </n-form-item-gi>
v-model:value="generalForm.addr" <n-form-item-gi
:placeholder="$t('dialogue.connection.addr_tip')" /> v-if="!isEditMode"
<n-text style="width: 40px; text-align: center">:</n-text> :label="$t('dialogue.connection.group')"
<n-input-number :span="24"
v-model:value="generalForm.port" required>
:max="65535" <n-select v-model:value="generalForm.group" :options="groupOptions" />
:min="1" </n-form-item-gi>
style="width: 200px" /> <n-form-item-gi :label="$t('dialogue.connection.addr')" :span="24" path="addr" required>
</n-form-item> <n-input
<n-form-item :label="$t('dialogue.connection.pwd')" path="password"> v-model:value="generalForm.addr"
<n-input :placeholder="$t('dialogue.connection.addr_tip')" />
v-model:value="generalForm.password" <n-text style="width: 40px; text-align: center">:</n-text>
:placeholder="$t('dialogue.connection.pwd_tip')" <n-input-number
show-password-on="click" v-model:value="generalForm.port"
type="password" /> :max="65535"
</n-form-item> :min="1"
<n-form-item :label="$t('dialogue.connection.usr')" path="username"> style="width: 200px" />
<n-input </n-form-item-gi>
v-model:value="generalForm.username" <n-form-item-gi :label="$t('dialogue.connection.pwd')" :span="12" path="password">
:placeholder="$t('dialogue.connection.usr_tip')" /> <n-input
</n-form-item> v-model:value="generalForm.password"
:placeholder="$t('dialogue.connection.pwd_tip')"
show-password-on="click"
type="password" />
</n-form-item-gi>
<n-form-item-gi :label="$t('dialogue.connection.usr')" :span="12" path="username">
<n-input
v-model:value="generalForm.username"
:placeholder="$t('dialogue.connection.usr_tip')" />
</n-form-item-gi>
</n-grid>
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
@ -337,6 +348,16 @@ const onClose = () => {
</template> </template>
</n-input-number> </n-input-number>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :label="$t('dialogue.connection.advn.key_view')" :span="12">
<n-radio-group v-model:value="generalForm.keyView">
<n-radio-button
:label="$t('dialogue.connection.advn.key_view_tree')"
:value="KeyViewType.Tree" />
<n-radio-button
:label="$t('dialogue.connection.advn.key_view_list')"
:value="KeyViewType.List" />
</n-radio-group>
</n-form-item-gi>
<n-form-item-gi :label="$t('dialogue.connection.advn.load_size')" :span="12"> <n-form-item-gi :label="$t('dialogue.connection.advn.load_size')" :span="12">
<n-input-number v-model:value="generalForm.loadSize" :min="0" /> <n-input-number v-model:value="generalForm.loadSize" :min="0" />
</n-form-item-gi> </n-form-item-gi>
@ -403,20 +424,20 @@ const onClose = () => {
<n-form-item :label="$t('dialogue.connection.ssl.cert_file')"> <n-form-item :label="$t('dialogue.connection.ssl.cert_file')">
<file-open-input <file-open-input
v-model:value="generalForm.ssl.certFile" v-model:value="generalForm.ssl.certFile"
:placeholder="$t('dialogue.connection.ssl.cert_file_tip')" :disabled="!generalForm.ssl.enable"
:disabled="!generalForm.ssl.enable" /> :placeholder="$t('dialogue.connection.ssl.cert_file_tip')" />
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.connection.ssl.key_file')"> <n-form-item :label="$t('dialogue.connection.ssl.key_file')">
<file-open-input <file-open-input
v-model:value="generalForm.ssl.keyFile" v-model:value="generalForm.ssl.keyFile"
:placeholder="$t('dialogue.connection.ssl.key_file_tip')" :disabled="!generalForm.ssl.enable"
:disabled="!generalForm.ssl.enable" /> :placeholder="$t('dialogue.connection.ssl.key_file_tip')" />
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.connection.ssl.ca_file')"> <n-form-item :label="$t('dialogue.connection.ssl.ca_file')">
<file-open-input <file-open-input
v-model:value="generalForm.ssl.caFile" v-model:value="generalForm.ssl.caFile"
:placeholder="$t('dialogue.connection.ssl.ca_file_tip')" :disabled="!generalForm.ssl.enable"
:disabled="!generalForm.ssl.enable" /> :placeholder="$t('dialogue.connection.ssl.ca_file_tip')" />
</n-form-item> </n-form-item>
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
@ -467,8 +488,8 @@ const onClose = () => {
<n-form-item v-if="sshLoginType === 'pkfile'" :label="$t('dialogue.connection.ssh.pkfile')"> <n-form-item v-if="sshLoginType === 'pkfile'" :label="$t('dialogue.connection.ssh.pkfile')">
<file-open-input <file-open-input
v-model:value="generalForm.ssh.pkFile" v-model:value="generalForm.ssh.pkFile"
:placeholder="$t('dialogue.connection.ssh.pkfile_tip')" :disabled="!generalForm.ssh.enable"
:disabled="!generalForm.ssh.enable" /> :placeholder="$t('dialogue.connection.ssh.pkfile_tip')" />
</n-form-item> </n-form-item>
<n-form-item v-if="sshLoginType === 'pkfile'" :label="$t('dialogue.connection.ssh.passphrase')"> <n-form-item v-if="sshLoginType === 'pkfile'" :label="$t('dialogue.connection.ssh.passphrase')">
<n-input <n-input

View File

@ -0,0 +1,48 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M9 42C11.2091 42 13 40.2091 13 38C13 35.7909 11.2091 34 9 34C6.79086 34 5 35.7909 5 38C5 40.2091 6.79086 42 9 42Z"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M9 14C11.2091 14 13 12.2092 13 10C13 7.79086 11.2091 6 9 6C6.79086 6 5 7.79086 5 10C5 12.2092 6.79086 14 9 14Z"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M9 28C11.2091 28 13 26.2092 13 24C13 21.7908 11.2091 20 9 20C6.79086 20 5 21.7908 5 24C5 26.2092 6.79086 28 9 28Z"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M21 24H43"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M21 38H43"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M21 10H43"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,57 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M38 20H18V28H38V20Z"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M32 6H18V14H32V6Z"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M44 34H18V42H44V34Z"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M17 10H5"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M17 24H5"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M17 38H5"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M5 44V4"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { useThemeVars } from 'naive-ui' import { NIcon, useThemeVars } from 'naive-ui'
import BrowserTree from './BrowserTree.vue' import BrowserTree from './BrowserTree.vue'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import useTabStore from 'stores/tab.js' import useTabStore from 'stores/tab.js'
@ -13,6 +13,9 @@ import { types } from '@/consts/support_redis_type.js'
import Search from '@/components/icons/Search.vue' import Search from '@/components/icons/Search.vue'
import Unlink from '@/components/icons/Unlink.vue' import Unlink from '@/components/icons/Unlink.vue'
import Status from '@/components/icons/Status.vue' import Status from '@/components/icons/Status.vue'
import SwitchButton from '@/components/common/SwitchButton.vue'
import ListView from '@/components/icons/ListView.vue'
import TreeView from '@/components/icons/TreeView.vue'
const themeVars = useThemeVars() const themeVars = useThemeVars()
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
@ -59,6 +62,13 @@ const filterTypeOptions = computed(() => {
}) })
return options return options
}) })
// forbid dynamic switch key view due to performance issues
// const viewType = ref(0)
// const onSwitchView = (selectView) => {
// const { server } = tabStore.currentTab
// connectionStore.switchKeyView(server, selectView)
// }
</script> </script>
<template> <template>
@ -82,9 +92,16 @@ const filterTypeOptions = computed(() => {
</div> </div>
<!-- bottom function bar --> <!-- bottom function bar -->
<div class="nav-pane-bottom flex-box-h"> <div class="nav-pane-bottom flex-box-h">
<!-- <switch-button-->
<!-- v-model:value="viewType"-->
<!-- :icons="[TreeView, ListView]"-->
<!-- :t-tooltips="['interface.tree_view', 'interface.list_view']"-->
<!-- stroke-width="4"-->
<!-- unselect-stroke-width="3"-->
<!-- @update:value="onSwitchView" />-->
<icon-button :icon="Status" size="20" stroke-width="4" t-tooltip="interface.status" @click="onInfo" /> <icon-button :icon="Status" size="20" stroke-width="4" t-tooltip="interface.status" @click="onInfo" />
<icon-button :icon="Refresh" size="20" stroke-width="4" t-tooltip="interface.reload" @click="onRefresh" /> <icon-button :icon="Refresh" size="20" stroke-width="4" t-tooltip="interface.reload" @click="onRefresh" />
<div class="flex-item-expand"></div> <div class="flex-item-expand" />
<icon-button <icon-button
:icon="Unlink" :icon="Unlink"
size="20" size="20"

View File

@ -5,7 +5,7 @@ import { NIcon, NSpace, NTag } from 'naive-ui'
import Key from '@/components/icons/Key.vue' import Key from '@/components/icons/Key.vue'
import Binary from '@/components/icons/Binary.vue' import Binary from '@/components/icons/Binary.vue'
import ToggleDb from '@/components/icons/ToggleDb.vue' import ToggleDb from '@/components/icons/ToggleDb.vue'
import { find, get, includes, indexOf, isEmpty, remove, size } from 'lodash' import { find, get, includes, indexOf, isEmpty, remove, size, startsWith } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Refresh from '@/components/icons/Refresh.vue' import Refresh from '@/components/icons/Refresh.vue'
import CopyLink from '@/components/icons/CopyLink.vue' import CopyLink from '@/components/icons/CopyLink.vue'
@ -28,6 +28,7 @@ import LoadAll from '@/components/icons/LoadAll.vue'
const props = defineProps({ const props = defineProps({
server: String, server: String,
keyView: String,
}) })
const i18n = useI18n() const i18n = useI18n()
@ -53,14 +54,6 @@ const selectedKeys = computed(() => {
const data = computed(() => { const data = computed(() => {
const dbs = get(connectionStore.databases, props.server, []) const dbs = get(connectionStore.databases, props.server, [])
return dbs return dbs
// return [
// {
// key: `${props.server}`,
// label: props.server,
// type: ConnectionType.Server,
// children: dbs,
// },
// ]
}) })
const backgroundColor = computed(() => { const backgroundColor = computed(() => {
@ -218,6 +211,17 @@ const expandKey = (key) => {
} }
} }
const resetExpandKey = (server, db, includeDB) => {
const prefix = `${server}/db${db}`
remove(expandedKeys.value, (k) => {
if (!!!includeDB) {
return k !== prefix && startsWith(k, prefix)
} else {
return startsWith(k, prefix)
}
})
}
const handleSelectContextMenu = (key) => { const handleSelectContextMenu = (key) => {
contextMenuParam.show = false contextMenuParam.show = false
const selectedKey = get(selectedKeys.value, 0) const selectedKey = get(selectedKeys.value, 0)
@ -247,10 +251,11 @@ const handleSelectContextMenu = (key) => {
nextTick().then(() => expandKey(nodeKey)) nextTick().then(() => expandKey(nodeKey))
break break
case 'db_reload': case 'db_reload':
resetExpandKey(props.server, db)
connectionStore.reopenDatabase(props.server, db) connectionStore.reopenDatabase(props.server, db)
break break
case 'db_close': case 'db_close':
remove(expandedKeys.value, (k) => k === `${props.server}/db${db}`) resetExpandKey(props.server, db, true)
connectionStore.closeDatabase(props.server, db) connectionStore.closeDatabase(props.server, db)
break break
case 'db_newkey': case 'db_newkey':
@ -327,6 +332,7 @@ const handleSelectContextMenu = (key) => {
console.warn('TODO: handle context menu:' + key) console.warn('TODO: handle context menu:' + key)
} }
} }
defineExpose({ defineExpose({
handleSelectContextMenu, handleSelectContextMenu,
}) })
@ -490,7 +496,7 @@ const renderIconMenu = (items) => {
{ {
align: 'center', align: 'center',
inline: true, inline: true,
size: 2, size: 3,
wrapItem: false, wrapItem: false,
wrap: false, wrap: false,
style: 'margin-right: 5px', style: 'margin-right: 5px',
@ -684,7 +690,6 @@ const handleOutsideContextMenu = () => {
@update:selected-keys="onUpdateSelectedKeys" @update:selected-keys="onUpdateSelectedKeys"
@update:expanded-keys="onUpdateExpanded" /> @update:expanded-keys="onUpdateExpanded" />
<n-dropdown <n-dropdown
:animated="false"
:options="contextMenuParam.options" :options="contextMenuParam.options"
:render-label="renderContextLabel" :render-label="renderContextLabel"
:show="contextMenuParam.show" :show="contextMenuParam.show"

View File

@ -166,7 +166,7 @@ const renderIconMenu = (items) => {
{ {
align: 'center', align: 'center',
inline: true, inline: true,
size: 2, size: 3,
wrapItem: false, wrapItem: false,
wrap: false, wrap: false,
style: 'margin-right: 5px', style: 'margin-right: 5px',
@ -530,10 +530,10 @@ const onCancelOpen = () => {
<!-- context menu --> <!-- context menu -->
<n-dropdown <n-dropdown
:animated="false"
:options="contextMenuParam.options" :options="contextMenuParam.options"
:render-label="renderContextLabel" :render-label="renderContextLabel"
:show="contextMenuParam.show" :show="contextMenuParam.show"
:keyboard="true"
:x="contextMenuParam.x" :x="contextMenuParam.x"
:y="contextMenuParam.y" :y="contextMenuParam.y"
placement="bottom-start" placement="bottom-start"

View File

@ -125,8 +125,6 @@ const openGithub = () => {
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<div class="nav-menu-item flex-box-v"> <div class="nav-menu-item flex-box-v">
<n-dropdown <n-dropdown
:animated="false"
:keyboard="false"
:options="preferencesOptions" :options="preferencesOptions"
:render-label="renderContextLabel" :render-label="renderContextLabel"
trigger="click" trigger="click"

View File

@ -0,0 +1,8 @@
/**
* all types of redis key viewing
* @enum {number}
*/
export const KeyViewType = {
Tree: 0,
List: 1,
}

View File

@ -85,7 +85,7 @@
"remove_key": "Remove Key", "remove_key": "Remove Key",
"new_key": "Add Key", "new_key": "Add Key",
"load_more": "Load More Keys", "load_more": "Load More Keys",
"load_all": "Load All Keys", "load_all": "Load All Left Keys",
"more_action": "More Action", "more_action": "More Action",
"nonexist_tab_content": "Selected key does not exist. Please retry", "nonexist_tab_content": "Selected key does not exist. Please retry",
"empty_server_content": "Select and open a connection from the left", "empty_server_content": "Select and open a connection from the left",
@ -149,6 +149,9 @@
"dbfilter_hide_title": "Select the Databases to Hide", "dbfilter_hide_title": "Select the Databases to Hide",
"dbfilter_input": "Input Database Index", "dbfilter_input": "Input Database Index",
"dbfilter_input_tip": "Press Enter to confirm", "dbfilter_input_tip": "Press Enter to confirm",
"key_view": "Default Key View",
"key_view_tree": "Tree View",
"key_view_list": "List View",
"load_size": "Size of Keys Per Load", "load_size": "Size of Keys Per Load",
"mark_color": "Mark Color" "mark_color": "Mark Color"
}, },

View File

@ -85,7 +85,7 @@
"remove_key": "删除键", "remove_key": "删除键",
"new_key": "添加新键", "new_key": "添加新键",
"load_more": "加载更多键", "load_more": "加载更多键",
"load_all": "加载所有键", "load_all": "加载剩余所有键",
"more_action": "更多操作", "more_action": "更多操作",
"nonexist_tab_content": "所选键不存在,请尝试刷新重试", "nonexist_tab_content": "所选键不存在,请尝试刷新重试",
"empty_server_content": "可以从左边选择并打开连接", "empty_server_content": "可以从左边选择并打开连接",
@ -149,6 +149,9 @@
"dbfilter_hide_title": "需要隐藏的数据库", "dbfilter_hide_title": "需要隐藏的数据库",
"dbfilter_input": "输入数据库索引", "dbfilter_input": "输入数据库索引",
"dbfilter_input_tip": "按回车确认", "dbfilter_input_tip": "按回车确认",
"key_view": "默认键视图",
"key_view_tree": "树形列表",
"key_view_list": "平铺列表",
"load_size": "单次加载键数量", "load_size": "单次加载键数量",
"mark_color": "标记颜色" "mark_color": "标记颜色"
}, },

View File

@ -51,6 +51,8 @@ import { ConnectionType } from '@/consts/connection_type.js'
import useTabStore from './tab.js' import useTabStore from './tab.js'
import { types } from '@/consts/support_redis_type.js' import { types } from '@/consts/support_redis_type.js'
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js' import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
import { KeyViewType } from '@/consts/key_view_type.js'
import { nextTick } from 'vue'
const useConnectionStore = defineStore('connections', { const useConnectionStore = defineStore('connections', {
/** /**
@ -117,10 +119,12 @@ const useConnectionStore = defineStore('connections', {
connections: [], // all connections connections: [], // all connections
serverStats: {}, // current server status info serverStats: {}, // current server status info
serverProfile: {}, // all server profile serverProfile: {}, // all server profile
keyFilter: {}, // all key filters in opened connections group by server+db keyFilter: {}, // all key filters in opened connections group by 'server+db'
typeFilter: {}, // all key type filters in opened connections group by server+db typeFilter: {}, // all key type filters in opened connections group by 'server+db'
databases: {}, // all databases in opened connections group by server name viewType: {}, // view type selection for all opened connections group by 'server'
nodeMap: {}, // all nodes in opened connections group by server#db and type/key databases: {}, // all databases in opened connections group by 'server name'
nodeMap: {}, // all nodes in opened connections group by 'server#db' and 'type/key'
keySet: {}, // all keys set in opened connections group by 'server#db
}), }),
getters: { getters: {
anyConnectionOpened() { anyConnectionOpened() {
@ -232,6 +236,7 @@ const useConnectionStore = defineStore('connections', {
execTimeout: 60, execTimeout: 60,
dbFilterType: 'none', dbFilterType: 'none',
dbFilterList: [], dbFilterList: [],
keyView: KeyViewType.Tree,
loadSize: 10000, loadSize: 10000,
markColor: '', markColor: '',
ssl: { ssl: {
@ -320,6 +325,38 @@ const useConnectionStore = defineStore('connections', {
return null return null
}, },
/**
* switch key view
* @param {string} connName
* @param {number} viewType
*/
async switchKeyView(connName, viewType) {
if (viewType !== KeyViewType.Tree && viewType !== KeyViewType.List) {
return
}
const t = get(this.viewType, connName, KeyViewType.Tree)
if (t === viewType) {
return
}
this.viewType[connName] = viewType
const dbs = get(this.databases, connName, [])
for (const dbItem of dbs) {
if (!dbItem.opened) {
continue
}
dbItem.children = undefined
dbItem.keys = 0
const { db = 0 } = dbItem
this._getNodeMap(connName, db).clear()
const keys = this._getKeySet(connName, db)
this._addKeyNodes(connName, db, keys)
this._tidyNode(connName, db, '')
}
},
/** /**
* create a new connection or update current connection profile * create a new connection or update current connection profile
* @param {string} name set null if create a new connection * @param {string} name set null if create a new connection
@ -399,13 +436,14 @@ const useConnectionStore = defineStore('connections', {
// if (connNode == null) { // if (connNode == null) {
// throw new Error('no such connection') // throw new Error('no such connection')
// } // }
const { db } = data const { db, view = KeyViewType.Tree } = data
if (isEmpty(db)) { if (isEmpty(db)) {
throw new Error('no db loaded') throw new Error('no db loaded')
} }
const dbs = [] const dbs = []
for (let i = 0; i < db.length; i++) { for (let i = 0; i < db.length; i++) {
this._getNodeMap(name, i).clear() this._getNodeMap(name, i).clear()
this._getKeySet(name, i).clear()
dbs.push({ dbs.push({
key: `${name}/${db[i].name}`, key: `${name}/${db[i].name}`,
label: db[i].name, label: db[i].name,
@ -419,6 +457,7 @@ const useConnectionStore = defineStore('connections', {
}) })
} }
this.databases[name] = dbs this.databases[name] = dbs
this.viewType[name] = view
}, },
/** /**
@ -438,6 +477,7 @@ const useConnectionStore = defineStore('connections', {
for (const db of dbs) { for (const db of dbs) {
this.removeKeyFilter(name, db.db) this.removeKeyFilter(name, db.db)
this._getNodeMap(name, db.db).clear() this._getNodeMap(name, db.db).clear()
this._getKeySet(name, db.db).clear()
} }
} }
this.removeKeyFilter(name, -1) this.removeKeyFilter(name, -1)
@ -459,6 +499,8 @@ const useConnectionStore = defineStore('connections', {
} }
this.databases = {} this.databases = {}
this.nodeMap.clear()
this.keySet.clear()
this.serverStats = {} this.serverStats = {}
const tabStore = useTabStore() const tabStore = useTabStore()
tabStore.removeAllTab() tabStore.removeAllTab()
@ -534,8 +576,8 @@ const useConnectionStore = defineStore('connections', {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async openDatabase(connName, db) { async openDatabase(connName, db) {
const { match: filterPattern, type: keyType } = this.getKeyFilter(connName, db) const { match: filterPattern, type: filterType } = this.getKeyFilter(connName, db)
const { data, success, msg } = await OpenDatabase(connName, db, filterPattern, keyType) const { data, success, msg } = await OpenDatabase(connName, db, filterPattern, filterType)
if (!success) { if (!success) {
throw new Error(msg) throw new Error(msg)
} }
@ -571,6 +613,7 @@ const useConnectionStore = defineStore('connections', {
selDB.isLeaf = false selDB.isLeaf = false
this._getNodeMap(connName, db).clear() this._getNodeMap(connName, db).clear()
this._getKeySet(connName, db).clear()
}, },
/** /**
@ -588,6 +631,7 @@ const useConnectionStore = defineStore('connections', {
selDB.opened = false selDB.opened = false
this._getNodeMap(connName, db).clear() this._getNodeMap(connName, db).clear()
this._getKeySet(connName, db).clear()
}, },
/** /**
@ -730,8 +774,6 @@ const useConnectionStore = defineStore('connections', {
async loadAllKeys(connName, db) { async loadAllKeys(connName, db) {
const { match, type: keyType } = this.getKeyFilter(connName, db) const { match, type: keyType } = this.getKeyFilter(connName, db)
const { keys } = await this._loadKeys(connName, db, match, keyType, true) const { keys } = await this._loadKeys(connName, db, match, keyType, true)
// remove current keys below prefix
this._deleteKeyNode(connName, db, '', true)
this._addKeyNodes(connName, db, keys) this._addKeyNodes(connName, db, keys)
this._tidyNode(connName, db, '') this._tidyNode(connName, db, '')
}, },
@ -752,30 +794,50 @@ const useConnectionStore = defineStore('connections', {
/** /**
* get node map * get node map
* @param connName * @param {string} connName
* @param db * @param {number} db
* @returns {Map<string, DatabaseItem>} * @returns {Map<string, DatabaseItem>}
* @private * @private
*/ */
_getNodeMap(connName, db) { _getNodeMap(connName, db) {
if (this.nodeMap[`${connName}#${db}`] == null) { if (!this.nodeMap.hasOwnProperty(`${connName}#${db}`)) {
this.nodeMap[`${connName}#${db}`] = new Map() this.nodeMap[`${connName}#${db}`] = new Map()
} }
// construct a tree node list, the format of item key likes 'server/db#type/key' // construct a tree node list, the format of item key likes 'server/db#type/key'
return this.nodeMap[`${connName}#${db}`] return this.nodeMap[`${connName}#${db}`]
}, },
/**
* get all keys in a database
* @param {string} connName
* @param {number} db
* @return {Set<string|number[]>}
* @private
*/
_getKeySet(connName, db) {
if (!this.keySet.hasOwnProperty(`${connName}#${db}`)) {
this.keySet[`${connName}#${db}`] = new Set()
}
// construct a key set
return this.keySet[`${connName}#${db}`]
},
/** /**
* remove keys in db * remove keys in db
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {Array<string|number[]>} keys * @param {Array<string|number[]>|Set<string|number[]>} keys
* @param {boolean} [sortInsert] * @param {boolean} [sortInsert]
* @return {{success: boolean, newKey: number, newLayer: number, replaceKey: number}} * @return {{success: boolean, newKey: number, newLayer: number, replaceKey: number}}
* @private * @private
*/ */
_addKeyNodes(connName, db, keys, sortInsert) { _addKeyNodes(connName, db, keys, sortInsert) {
const result = { success: false, newLayer: 0, newKey: 0, replaceKey: 0 } const result = {
success: false,
newLayer: 0,
newKey: 0,
replaceKey: 0,
}
if (isEmpty(keys)) { if (isEmpty(keys)) {
return result return result
} }
@ -789,72 +851,105 @@ const useConnectionStore = defineStore('connections', {
selDB.children = [] selDB.children = []
} }
const nodeMap = this._getNodeMap(connName, db) const nodeMap = this._getNodeMap(connName, db)
const keySet = this._getKeySet(connName, db)
const rootChildren = selDB.children const rootChildren = selDB.children
for (const key of keys) { const viewType = get(this.viewType, connName, KeyViewType.Tree)
const k = decodeRedisKey(key) if (viewType === KeyViewType.List) {
const binaryKey = k !== key // construct list view data
const keyParts = binaryKey ? [nativeRedisKey(key)] : split(k, separator) for (const key of keys) {
const len = size(keyParts) const k = decodeRedisKey(key)
const lastIdx = len - 1 const isBinaryKey = k !== key
let handlePath = '' const nodeKey = `${ConnectionType.RedisValue}/${nativeRedisKey(key)}`
let children = rootChildren const replaceKey = nodeMap.has(nodeKey)
for (let i = 0; i < len; i++) { const selectedNode = {
handlePath += keyParts[i] key: `${connName}/db${db}#${nodeKey}`,
if (i !== lastIdx) { label: k,
// layer db,
const nodeKey = `${ConnectionType.RedisKey}/${handlePath}` keys: 0,
let selectedNode = nodeMap.get(nodeKey) redisKey: k,
if (selectedNode == null) { redisKeyCode: isBinaryKey ? key : undefined,
selectedNode = { type: ConnectionType.RedisValue,
isLeaf: true,
}
nodeMap.set(nodeKey, selectedNode)
keySet.add(key)
if (!replaceKey) {
if (sortInsert) {
const index = sortedIndexBy(rootChildren, selectedNode, 'key')
rootChildren.splice(index, 0, selectedNode)
} else {
rootChildren.push(selectedNode)
}
result.newKey += 1
} else {
result.replaceKey += 1
}
}
} else {
// construct tree view data
for (const key of keys) {
const k = decodeRedisKey(key)
const isBinaryKey = k !== key
const keyParts = isBinaryKey ? [nativeRedisKey(key)] : split(k, separator)
const len = size(keyParts)
const lastIdx = len - 1
let handlePath = ''
let children = rootChildren
for (let i = 0; i < len; i++) {
handlePath += keyParts[i]
if (i !== lastIdx) {
// layer
const nodeKey = `${ConnectionType.RedisKey}/${handlePath}`
let selectedNode = nodeMap.get(nodeKey)
if (selectedNode == null) {
selectedNode = {
key: `${connName}/db${db}#${nodeKey}`,
label: keyParts[i],
db,
keys: 0,
redisKey: handlePath,
type: ConnectionType.RedisKey,
isLeaf: false,
children: [],
}
nodeMap.set(nodeKey, selectedNode)
if (sortInsert) {
const index = sortedIndexBy(children, selectedNode, 'key')
children.splice(index, 0, selectedNode)
} else {
children.push(selectedNode)
}
result.newLayer += 1
}
children = selectedNode.children
handlePath += separator
} else {
// key
const nodeKey = `${ConnectionType.RedisValue}/${handlePath}`
const replaceKey = nodeMap.has(nodeKey)
const selectedNode = {
key: `${connName}/db${db}#${nodeKey}`, key: `${connName}/db${db}#${nodeKey}`,
label: keyParts[i], label: isBinaryKey ? k : keyParts[i],
db, db,
keys: 0, keys: 0,
redisKey: handlePath, redisKey: handlePath,
type: ConnectionType.RedisKey, redisKeyCode: isBinaryKey ? key : undefined,
isLeaf: false, type: ConnectionType.RedisValue,
children: [], isLeaf: true,
} }
nodeMap.set(nodeKey, selectedNode) nodeMap.set(nodeKey, selectedNode)
if (sortInsert) { keySet.add(key)
const index = sortedIndexBy(children, selectedNode, (elem) => { if (!replaceKey) {
return elem.key if (sortInsert) {
}) const index = sortedIndexBy(children, selectedNode, 'key')
children.splice(index, 0, selectedNode) children.splice(index, 0, selectedNode)
} else {
children.push(selectedNode)
}
result.newKey += 1
} else { } else {
children.push(selectedNode) result.replaceKey += 1
} }
result.newLayer += 1
}
children = selectedNode.children
handlePath += separator
} else {
// key
const nodeKey = `${ConnectionType.RedisValue}/${handlePath}`
const replaceKey = nodeMap.has(nodeKey)
const selectedNode = {
key: `${connName}/db${db}#${nodeKey}`,
label: binaryKey ? k : keyParts[i],
db,
keys: 0,
redisKey: handlePath,
redisKeyCode: binaryKey ? key : undefined,
type: ConnectionType.RedisValue,
isLeaf: true,
}
nodeMap.set(nodeKey, selectedNode)
if (!replaceKey) {
if (sortInsert) {
const index = sortedIndexBy(children, selectedNode, (elem) => {
return elem.key > selectedNode.key
})
children.splice(index, 0, selectedNode)
} else {
children.push(selectedNode)
}
result.newKey += 1
} else {
result.replaceKey += 1
} }
} }
} }
@ -1408,8 +1503,9 @@ const useConnectionStore = defineStore('connections', {
} }
const nodeMap = this._getNodeMap(connName, db) const nodeMap = this._getNodeMap(connName, db)
const keySet = this._getKeySet(connName, db)
if (isLayer === true) { if (isLayer === true) {
this._deleteChildrenKeyNodes(nodeMap, key) this._deleteChildrenKeyNodes(nodeMap, keySet, key)
} }
if (isEmpty(key)) { if (isEmpty(key)) {
// clear all key nodes // clear all key nodes
@ -1447,12 +1543,18 @@ const useConnectionStore = defineStore('connections', {
if (isEmpty(anceNode.children)) { if (isEmpty(anceNode.children)) {
nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`) nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`)
keySet.delete(anceNode.redisKeyCode || anceNode.redisKey)
} else { } else {
break break
} }
} else { } else {
// last one, remove from db node // last one, remove from db node
remove(dbRoot.children, { type: ConnectionType.RedisKey, redisKey: keyParts[0] }) remove(dbRoot.children, { type: ConnectionType.RedisKey, redisKey: keyParts[0] })
const node = nodeMap.get(`${ConnectionType.RedisValue}/${keyParts[0]}`)
if (node != null) {
nodeMap.delete(`${ConnectionType.RedisValue}/${keyParts[0]}`)
keySet.delete(node.redisKeyCode || node.redisKey)
}
} }
} }
} }
@ -1463,12 +1565,14 @@ const useConnectionStore = defineStore('connections', {
/** /**
* delete node and all it's children from nodeMap * delete node and all it's children from nodeMap
* @param {Map<string, DatabaseItem>} nodeMap * @param {Map<string, DatabaseItem>} nodeMap
* @param {Set<string|number[]>} keySet
* @param {string} [key] clean nodeMap if key is empty * @param {string} [key] clean nodeMap if key is empty
* @private * @private
*/ */
_deleteChildrenKeyNodes(nodeMap, key) { _deleteChildrenKeyNodes(nodeMap, keySet, key) {
if (isEmpty(key)) { if (isEmpty(key)) {
nodeMap.clear() nodeMap.clear()
keySet.clear()
} else { } else {
const mapKey = `${ConnectionType.RedisKey}/${key}` const mapKey = `${ConnectionType.RedisKey}/${key}`
const node = nodeMap.get(mapKey) const node = nodeMap.get(mapKey)
@ -1477,13 +1581,15 @@ const useConnectionStore = defineStore('connections', {
if (!nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) { if (!nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) {
console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`) console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`)
} }
keySet.delete(child.redisKeyCode || child.redisKey)
} else if (child.type === ConnectionType.RedisKey) { } else if (child.type === ConnectionType.RedisKey) {
this._deleteChildrenKeyNodes(nodeMap, child.redisKey) this._deleteChildrenKeyNodes(nodeMap, keySet, child.redisKey)
} }
} }
if (!nodeMap.delete(mapKey)) { if (!nodeMap.delete(mapKey)) {
console.warn('delete map key', mapKey) console.warn('delete map key', mapKey)
} }
keySet.delete(node.redisKeyCode || node.redisKey)
} }
}, },