860 lines
26 KiB
Vue
860 lines
26 KiB
Vue
<script setup>
|
|
import { computed, h, markRaw, nextTick, reactive, ref, watchEffect } from 'vue'
|
|
import { ConnectionType } from '@/consts/connection_type.js'
|
|
import { NIcon, NSpace, NText, useThemeVars } from 'naive-ui'
|
|
import Key from '@/components/icons/Key.vue'
|
|
import Binary from '@/components/icons/Binary.vue'
|
|
import Database from '@/components/icons/Database.vue'
|
|
import { filter, find, first, get, includes, isEmpty, last, map, size, toUpper } from 'lodash'
|
|
import { useI18n } from 'vue-i18n'
|
|
import Refresh from '@/components/icons/Refresh.vue'
|
|
import CopyLink from '@/components/icons/CopyLink.vue'
|
|
import Add from '@/components/icons/Add.vue'
|
|
import Layer from '@/components/icons/Layer.vue'
|
|
import Delete from '@/components/icons/Delete.vue'
|
|
import useDialogStore from 'stores/dialog.js'
|
|
import useConnectionStore from 'stores/connections.js'
|
|
import useTabStore from 'stores/tab.js'
|
|
import IconButton from '@/components/common/IconButton.vue'
|
|
import { parseHexColor } from '@/utils/rgb.js'
|
|
import LoadList from '@/components/icons/LoadList.vue'
|
|
import LoadAll from '@/components/icons/LoadAll.vue'
|
|
import useBrowserStore from 'stores/browser.js'
|
|
import { useRender } from '@/utils/render.js'
|
|
import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
|
import usePreferencesStore from 'stores/preferences.js'
|
|
import { typesIconStyle } from '@/consts/support_redis_type.js'
|
|
import { nativeRedisKey } from '@/utils/key_convert.js'
|
|
import copy from 'copy-text-to-clipboard'
|
|
import { isMacOS } from '@/utils/platform.js'
|
|
|
|
const props = defineProps({
|
|
server: String,
|
|
db: Number,
|
|
keyView: String,
|
|
loading: Boolean,
|
|
pattern: String,
|
|
fullLoaded: Boolean,
|
|
checkMode: Boolean,
|
|
})
|
|
|
|
const themeVars = useThemeVars()
|
|
const render = useRender()
|
|
const i18n = useI18n()
|
|
const connectionStore = useConnectionStore()
|
|
const browserStore = useBrowserStore()
|
|
const prefStore = usePreferencesStore()
|
|
const tabStore = useTabStore()
|
|
const dialogStore = useDialogStore()
|
|
|
|
/**
|
|
*
|
|
* @type {ComputedRef<string[]>}
|
|
*/
|
|
const expandedKeys = computed(() => {
|
|
const tab = find(tabStore.tabList, { name: props.server })
|
|
if (tab != null) {
|
|
return get(tab, 'expandedKeys', [])
|
|
}
|
|
return []
|
|
})
|
|
|
|
/**
|
|
*
|
|
* @type {ComputedRef<string[]>}
|
|
*/
|
|
const selectedKeys = computed(() => {
|
|
const tab = find(tabStore.tabList, { name: props.server })
|
|
if (tab != null) {
|
|
return get(tab, 'selectedKeys', [])
|
|
}
|
|
return []
|
|
})
|
|
|
|
/**
|
|
*
|
|
* @type {ComputedRef<string[]>}
|
|
*/
|
|
const checkedKeys = computed(() => {
|
|
const tab = find(tabStore.tabList, { name: props.server })
|
|
if (tab != null) {
|
|
const checkedKeys = get(tab, 'checkedKeys', [])
|
|
return map(checkedKeys, 'key')
|
|
}
|
|
return []
|
|
})
|
|
|
|
const data = computed(() => {
|
|
return browserStore.getKeyStruct(props.server, props.checkMode)
|
|
})
|
|
|
|
const backgroundColor = computed(() => {
|
|
const { markColor: hex = '' } = connectionStore.serverProfile[props.server] || {}
|
|
if (isEmpty(hex)) {
|
|
return ''
|
|
}
|
|
const { r, g, b } = parseHexColor(hex)
|
|
return `rgba(${r}, ${g}, ${b}, 0.2)`
|
|
})
|
|
|
|
const contextMenuParam = reactive({
|
|
show: false,
|
|
x: 0,
|
|
y: 0,
|
|
options: null,
|
|
})
|
|
|
|
const menuOptions = {
|
|
[ConnectionType.RedisKey]: [
|
|
// {
|
|
// key: 'key_reload',
|
|
// label: 'interface.reload'),
|
|
// icon: Refresh,
|
|
// },
|
|
{
|
|
key: 'key_newkey',
|
|
label: 'interface.new_key',
|
|
icon: Add,
|
|
},
|
|
{
|
|
key: 'key_copy',
|
|
label: 'interface.copy_path',
|
|
icon: CopyLink,
|
|
},
|
|
{
|
|
type: 'divider',
|
|
key: 'd1',
|
|
},
|
|
{
|
|
key: 'key_remove',
|
|
label: 'interface.batch_delete_key',
|
|
icon: Delete,
|
|
},
|
|
],
|
|
[ConnectionType.RedisValue]: [
|
|
{
|
|
key: 'value_reload',
|
|
label: 'interface.reload',
|
|
icon: Refresh,
|
|
},
|
|
{
|
|
key: 'value_copy',
|
|
label: 'interface.copy_key',
|
|
icon: CopyLink,
|
|
},
|
|
{
|
|
type: 'divider',
|
|
key: 'd1',
|
|
},
|
|
{
|
|
key: 'value_remove',
|
|
label: 'interface.remove_key',
|
|
icon: Delete,
|
|
},
|
|
],
|
|
}
|
|
|
|
const handleKeyUp = () => {
|
|
const selectedKey = get(selectedKeys.value, 0)
|
|
if (selectedKey == null) {
|
|
return
|
|
}
|
|
let node = browserStore.getNode(selectedKey)
|
|
if (node == null) {
|
|
return
|
|
}
|
|
|
|
let parentNode = browserStore.getParentNode(selectedKey)
|
|
if (parentNode == null) {
|
|
return
|
|
}
|
|
const nodeIndex = parentNode.children.indexOf(node)
|
|
if (nodeIndex <= 0) {
|
|
if (parentNode.type === ConnectionType.RedisKey || parentNode.type === ConnectionType.RedisValue) {
|
|
onUpdateSelectedKeys([parentNode.key])
|
|
updateKeyDetail(parentNode)
|
|
}
|
|
return
|
|
}
|
|
|
|
// try select pre node
|
|
let preNode = parentNode.children[nodeIndex - 1]
|
|
while (preNode.expanded && !isEmpty(preNode.children)) {
|
|
preNode = last(preNode.children)
|
|
}
|
|
onUpdateSelectedKeys([preNode.key])
|
|
updateKeyDetail(preNode)
|
|
}
|
|
|
|
const handleKeyDown = () => {
|
|
const selectedKey = get(selectedKeys.value, 0)
|
|
if (selectedKey == null) {
|
|
return
|
|
}
|
|
let node = browserStore.getNode(selectedKey)
|
|
if (node == null) {
|
|
return
|
|
}
|
|
// try select first child if expanded
|
|
if (node.expanded && !isEmpty(node.children)) {
|
|
const childNode = get(node.children, 0)
|
|
onUpdateSelectedKeys([childNode.key])
|
|
updateKeyDetail(childNode)
|
|
return
|
|
}
|
|
|
|
let travelCount = 0
|
|
let childKey = selectedKey
|
|
do {
|
|
if (travelCount++ > 20) {
|
|
return
|
|
}
|
|
// find out parent node
|
|
const parentNode = browserStore.getParentNode(childKey)
|
|
if (parentNode == null) {
|
|
return
|
|
}
|
|
const nodeIndex = parentNode.children.indexOf(node)
|
|
if (nodeIndex < 0 || nodeIndex >= parentNode.children.length - 1) {
|
|
// last child, try select parent's neighbor node
|
|
childKey = parentNode.key
|
|
node = parentNode
|
|
} else {
|
|
// select next node
|
|
const childNode = parentNode.children[nodeIndex + 1]
|
|
onUpdateSelectedKeys([childNode.key])
|
|
updateKeyDetail(childNode)
|
|
return
|
|
}
|
|
} while (true)
|
|
}
|
|
|
|
const handleKeyLeft = () => {
|
|
const selectedKey = get(selectedKeys.value, 0)
|
|
if (selectedKey == null) {
|
|
return
|
|
}
|
|
let node = browserStore.getNode(selectedKey)
|
|
if (node == null) {
|
|
return
|
|
}
|
|
|
|
if (node.type === ConnectionType.RedisKey) {
|
|
if (node.expanded) {
|
|
// try collapse
|
|
onUpdateExpanded([node.key], null, { node, action: 'collapse' })
|
|
return
|
|
}
|
|
}
|
|
|
|
// try select parent node
|
|
let parentNode = browserStore.getParentNode(selectedKey)
|
|
if (parentNode == null || parentNode.type !== ConnectionType.RedisKey) {
|
|
return
|
|
}
|
|
onUpdateSelectedKeys([parentNode.key])
|
|
updateKeyDetail(parentNode)
|
|
}
|
|
|
|
const handleKeyRight = () => {
|
|
const selectedKey = get(selectedKeys.value, 0)
|
|
if (selectedKey == null) {
|
|
return
|
|
}
|
|
let node = browserStore.getNode(selectedKey)
|
|
if (node == null) {
|
|
return
|
|
}
|
|
|
|
if (node.type === ConnectionType.RedisKey) {
|
|
if (!node.expanded) {
|
|
// try expand
|
|
onUpdateExpanded([node.key], null, { node, action: 'expand' })
|
|
} else if (!isEmpty(node.children)) {
|
|
// try select first child
|
|
const childNode = first(node.children)
|
|
onUpdateSelectedKeys([childNode.key])
|
|
updateKeyDetail(childNode)
|
|
}
|
|
} else if (node.type === ConnectionType.RedisValue) {
|
|
handleKeyDown()
|
|
}
|
|
}
|
|
|
|
const handleKeyDelete = () => {
|
|
const selectedKey = get(selectedKeys.value, 0)
|
|
if (selectedKey == null) {
|
|
return
|
|
}
|
|
let node = browserStore.getNode(selectedKey)
|
|
if (node == null) {
|
|
return
|
|
}
|
|
|
|
const { db = 0, key: nodeKey, redisKey: rk = '', redisKeyCode: rkc, label } = node || {}
|
|
const redisKey = rkc || rk
|
|
const redisKeyName = !!rkc ? label : redisKey
|
|
switch (node.type) {
|
|
case ConnectionType.RedisKey:
|
|
dialogStore.openDeleteKeyDialog(props.server, db, isEmpty(redisKey) ? '*' : redisKey + ':*')
|
|
break
|
|
case ConnectionType.RedisValue:
|
|
$dialog.warning(i18n.t('dialogue.remove_tip', { name: redisKeyName }), () => {
|
|
browserStore.deleteKey(props.server, db, redisKey).then((success) => {
|
|
if (success) {
|
|
$message.success(i18n.t('dialogue.delete.success', { key: redisKeyName }))
|
|
}
|
|
})
|
|
})
|
|
break
|
|
}
|
|
}
|
|
|
|
const handleKeyCopy = () => {
|
|
const selectedKey = get(selectedKeys.value, 0)
|
|
if (selectedKey == null) {
|
|
return
|
|
}
|
|
let node = browserStore.getNode(selectedKey)
|
|
if (node == null) {
|
|
return
|
|
}
|
|
|
|
if (node.type === ConnectionType.RedisValue) {
|
|
copy(nativeRedisKey(node.redisKeyCode || node.redisKey))
|
|
$message.success(i18n.t('interface.copy_succ'))
|
|
}
|
|
}
|
|
|
|
const onKeyShortcut = (e) => {
|
|
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
|
|
switch (e.key) {
|
|
case 'ArrowUp':
|
|
handleKeyUp()
|
|
break
|
|
case 'ArrowDown':
|
|
handleKeyDown()
|
|
break
|
|
case 'ArrowLeft':
|
|
handleKeyLeft()
|
|
break
|
|
case 'ArrowRight':
|
|
handleKeyRight()
|
|
break
|
|
case 'c':
|
|
if (isCtrlOn) {
|
|
handleKeyCopy()
|
|
}
|
|
break
|
|
case 'Delete':
|
|
handleKeyDelete()
|
|
break
|
|
case 'F5':
|
|
handleSelectContextMenu('value_reload')
|
|
break
|
|
case 'r':
|
|
if (isCtrlOn) {
|
|
handleSelectContextMenu('value_reload')
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
const handleSelectContextMenu = (action) => {
|
|
contextMenuParam.show = false
|
|
const selectedKey = get(selectedKeys.value, 0)
|
|
if (selectedKey == null) {
|
|
return
|
|
}
|
|
const node = browserStore.getNode(selectedKey)
|
|
const { db = 0, key: nodeKey, redisKey: rk = '', redisKeyCode: rkc, label } = node || {}
|
|
const redisKey = rkc || rk
|
|
const redisKeyName = !!rkc ? label : redisKey
|
|
switch (action) {
|
|
case 'key_newkey':
|
|
dialogStore.openNewKeyDialog(redisKey, props.server, db)
|
|
break
|
|
case 'db_filter':
|
|
// const { match: pattern, type } = browserStore.getKeyFilter(props.server)
|
|
// dialogStore.openKeyFilterDialog(props.server, db, pattern, type)
|
|
break
|
|
case 'key_reload':
|
|
if (node != null && !!!node.loading) {
|
|
node.loading = true
|
|
browserStore.reloadLayer(props.server, db, redisKey).finally(() => {
|
|
delete node.loading
|
|
})
|
|
}
|
|
break
|
|
case 'value_reload':
|
|
browserStore.reloadKey({
|
|
server: props.server,
|
|
db,
|
|
key: redisKey,
|
|
})
|
|
break
|
|
case 'key_remove':
|
|
dialogStore.openDeleteKeyDialog(props.server, db, isEmpty(redisKey) ? '*' : redisKey + ':*')
|
|
break
|
|
case 'value_remove':
|
|
$dialog.warning(i18n.t('dialogue.remove_tip', { name: redisKeyName }), () => {
|
|
browserStore.deleteKey(props.server, db, redisKey).then((success) => {
|
|
if (success) {
|
|
$message.success(i18n.t('dialogue.delete.success', { key: redisKeyName }))
|
|
}
|
|
})
|
|
})
|
|
break
|
|
case 'key_copy':
|
|
case 'value_copy':
|
|
copy(nativeRedisKey(redisKey))
|
|
$message.success(i18n.t('interface.copy_succ'))
|
|
break
|
|
case 'db_loadall':
|
|
if (node != null && !!!node.loading) {
|
|
node.loading = true
|
|
browserStore
|
|
.loadAllKeys(props.server, db)
|
|
.catch((e) => {
|
|
$message.error(e.message)
|
|
})
|
|
.finally(() => {
|
|
delete node.loading
|
|
node.fullLoaded = true
|
|
})
|
|
}
|
|
break
|
|
case 'more_action':
|
|
default:
|
|
console.warn('TODO: handle context menu:' + action)
|
|
}
|
|
}
|
|
|
|
const onUpdateSelectedKeys = (keys, options) => {
|
|
if (!isEmpty(keys)) {
|
|
tabStore.setSelectedKeys(props.server, keys)
|
|
} else {
|
|
// default is load blank key to display server status
|
|
// tabStore.openBlank(props.server)
|
|
}
|
|
}
|
|
|
|
const onUpdateExpanded = (value, option, meta) => {
|
|
const expand = meta.action === 'expand'
|
|
if (expand) {
|
|
tabStore.addExpandedKey(props.server, value)
|
|
} else {
|
|
tabStore.removeExpandedKey(props.server, value)
|
|
}
|
|
let node = meta.node
|
|
if (!node) {
|
|
return
|
|
}
|
|
|
|
// keep expand or collapse children while they own more than 1 child
|
|
do {
|
|
const key = node.key
|
|
if (expand) {
|
|
if (node.type === ConnectionType.RedisKey) {
|
|
node.expanded = true
|
|
tabStore.addExpandedKey(props.server, key)
|
|
}
|
|
} else {
|
|
node.expanded = false
|
|
tabStore.removeExpandedKey(props.server, key)
|
|
}
|
|
if (size(node.children) === 1) {
|
|
node = node.children[0]
|
|
} else {
|
|
break
|
|
}
|
|
} while (true)
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string[]} keys
|
|
* @param {TreeOption[]} options
|
|
*/
|
|
const onUpdateCheckedKeys = (keys, options) => {
|
|
const checkedKeys = map(
|
|
filter(options, (o) => o.type === ConnectionType.RedisValue),
|
|
(o) => ({ key: o.key, redisKey: o.redisKeyCode || o.redisKey }),
|
|
)
|
|
tabStore.setCheckedKeys(props.server, checkedKeys)
|
|
}
|
|
|
|
const renderPrefix = ({ option }) => {
|
|
switch (option.type) {
|
|
// case ConnectionType.Server:
|
|
// const icon = option.cluster === true ? ToggleCluster : ToggleServer
|
|
// return h(
|
|
// NIcon,
|
|
// { size: 20 },
|
|
// {
|
|
// default: () => h(icon, { modelValue: false }),
|
|
// },
|
|
// )
|
|
case ConnectionType.RedisDB:
|
|
return h(
|
|
NIcon,
|
|
{ size: 20, color: option.opened === true ? '#dc423c' : undefined },
|
|
{
|
|
default: () => h(Database, { inverse: option.opened === true }),
|
|
},
|
|
)
|
|
|
|
case ConnectionType.RedisKey:
|
|
return h(
|
|
NIcon,
|
|
{ size: 20 },
|
|
{
|
|
default: () => h(Layer),
|
|
},
|
|
)
|
|
|
|
case ConnectionType.RedisValue:
|
|
if (prefStore.keyIconType === typesIconStyle.ICON) {
|
|
return h(NIcon, { size: 20 }, () => h(Key))
|
|
}
|
|
const loading = isEmpty(option.redisType) || option.redisType === 'loading'
|
|
if (loading) {
|
|
browserStore.loadKeyType({
|
|
server: props.server,
|
|
db: option.db,
|
|
key: option.redisKeyCode || option.redisKey,
|
|
})
|
|
}
|
|
switch (prefStore.keyIconType) {
|
|
case typesIconStyle.FULL:
|
|
return h(RedisTypeTag, {
|
|
type: toUpper(option.redisType),
|
|
short: false,
|
|
size: 'small',
|
|
inverse: includes(selectedKeys.value, option.key),
|
|
loading,
|
|
})
|
|
|
|
case typesIconStyle.POINT:
|
|
return h(RedisTypeTag, {
|
|
type: toUpper(option.redisType),
|
|
point: true,
|
|
loading,
|
|
})
|
|
|
|
case typesIconStyle.SHORT:
|
|
default:
|
|
return h(RedisTypeTag, {
|
|
type: toUpper(option.redisType),
|
|
short: true,
|
|
size: 'small',
|
|
loading,
|
|
inverse: includes(selectedKeys.value, option.key),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// render tree item label
|
|
const renderLabel = ({ option }) => {
|
|
switch (option.type) {
|
|
case ConnectionType.RedisKey:
|
|
if (option.label === '') {
|
|
// blank label name
|
|
return h('div', [
|
|
h(NText, { italic: true, depth: 3 }, () => '[NO NAME]'),
|
|
h('span', () => ` (${option.keyCount || 0})`),
|
|
])
|
|
}
|
|
return `${option.label} (${option.keyCount || 0})`
|
|
// case ConnectionType.RedisValue:
|
|
// return `[${option.keyType}]${option.label}`
|
|
}
|
|
return option.label
|
|
}
|
|
|
|
// render horizontal item
|
|
const renderIconMenu = (items) => {
|
|
return h(
|
|
NSpace,
|
|
{
|
|
align: 'center',
|
|
inline: true,
|
|
size: 3,
|
|
wrapItem: false,
|
|
wrap: false,
|
|
style: 'margin-right: 5px',
|
|
},
|
|
() => items,
|
|
)
|
|
}
|
|
|
|
const calcDBMenu = (opened, loading, end) => {
|
|
const btns = []
|
|
if (opened) {
|
|
btns.push(
|
|
h(IconButton, {
|
|
tTooltip: 'interface.load_more',
|
|
icon: LoadList,
|
|
disabled: end === true,
|
|
loading: loading === true,
|
|
color: loading === true ? themeVars.value.primaryColor : '',
|
|
onClick: () => handleSelectContextMenu('db_loadmore'),
|
|
}),
|
|
h(IconButton, {
|
|
tTooltip: 'interface.load_all',
|
|
icon: LoadAll,
|
|
disabled: end === true,
|
|
loading: loading === true,
|
|
color: loading === true ? themeVars.value.primaryColor : '',
|
|
onClick: () => handleSelectContextMenu('db_loadall'),
|
|
}),
|
|
// h(IconButton, {
|
|
// tTooltip: 'interface.more_action',
|
|
// icon: More,
|
|
// onClick: () => handleSelectContextMenu('more_action'),
|
|
// }),
|
|
)
|
|
}
|
|
return btns
|
|
}
|
|
|
|
const calcLayerMenu = (loading) => {
|
|
return [
|
|
// reload layer enable only full loaded
|
|
h(IconButton, {
|
|
tTooltip: props.fullLoaded ? 'interface.reload' : 'interface.reload_disable',
|
|
icon: Refresh,
|
|
loading: loading === true,
|
|
disabled: !props.fullLoaded,
|
|
onClick: () => handleSelectContextMenu('key_reload'),
|
|
}),
|
|
h(IconButton, {
|
|
tTooltip: 'interface.new_key',
|
|
icon: Add,
|
|
onClick: () => handleSelectContextMenu('key_newkey'),
|
|
}),
|
|
h(IconButton, {
|
|
tTooltip: 'interface.batch_delete_key',
|
|
icon: Delete,
|
|
onClick: () => handleSelectContextMenu('key_remove'),
|
|
}),
|
|
]
|
|
}
|
|
|
|
const calcValueMenu = () => {
|
|
return [
|
|
h(IconButton, {
|
|
tTooltip: 'interface.remove_key',
|
|
icon: Delete,
|
|
onClick: () => handleSelectContextMenu('value_remove'),
|
|
}),
|
|
]
|
|
}
|
|
|
|
// render menu function icon
|
|
const renderSuffix = ({ option }) => {
|
|
const selected = includes(selectedKeys.value, option.key)
|
|
if (selected && !props.checkMode) {
|
|
switch (option.type) {
|
|
case ConnectionType.RedisDB:
|
|
return renderIconMenu(calcDBMenu(option.opened, option.loading, option.fullLoaded))
|
|
case ConnectionType.RedisKey:
|
|
return renderIconMenu(calcLayerMenu(option.loading))
|
|
case ConnectionType.RedisValue:
|
|
return renderIconMenu(calcValueMenu())
|
|
}
|
|
} else if (!selected && !!option.redisKeyCode && option.type === ConnectionType.RedisValue) {
|
|
// render binary icon
|
|
return renderIconMenu(h(NIcon, { size: 20 }, () => h(Binary)))
|
|
}
|
|
return null
|
|
}
|
|
|
|
const lastLoadKey = ref(0)
|
|
|
|
/**
|
|
*
|
|
* @param {RedisNodeItem} node
|
|
*/
|
|
const updateKeyDetail = (node) => {
|
|
if (node.type === ConnectionType.RedisValue) {
|
|
const preK = tabStore.getActivatedKey(props.server)
|
|
if (!isEmpty(preK) && preK === node.key && Date.now() - lastLoadKey.value > 1000) {
|
|
// reload key already activated
|
|
lastLoadKey.value = Date.now()
|
|
const { db, redisKey, redisKeyCode } = node
|
|
browserStore.reloadKey({
|
|
server: props.server,
|
|
db,
|
|
key: redisKeyCode || redisKey,
|
|
})
|
|
} else if (tabStore.setActivatedKey(props.server, node.key)) {
|
|
const { db, redisKey, redisKeyCode } = node
|
|
browserStore.loadKeySummary({
|
|
server: props.server,
|
|
db,
|
|
key: redisKeyCode || redisKey,
|
|
clearValue: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const nodeProps = ({ option }) => {
|
|
return {
|
|
onClick: () => {
|
|
updateKeyDetail(option)
|
|
},
|
|
onDblclick: () => {
|
|
if (props.loading) {
|
|
console.warn('TODO: alert to ignore double click when loading')
|
|
return
|
|
}
|
|
if (!props.checkMode) {
|
|
// default handle is toggle expand current node
|
|
nextTick().then(() => tabStore.toggleExpandKey(props.server, option.key))
|
|
}
|
|
},
|
|
onContextmenu(e) {
|
|
e.preventDefault()
|
|
if (!menuOptions.hasOwnProperty(option.type)) {
|
|
return
|
|
}
|
|
contextMenuParam.show = false
|
|
nextTick().then(() => {
|
|
contextMenuParam.options = markRaw(menuOptions[option.type] || [])
|
|
contextMenuParam.x = e.clientX
|
|
contextMenuParam.y = e.clientY
|
|
contextMenuParam.show = true
|
|
onUpdateSelectedKeys([option.key], [option])
|
|
})
|
|
},
|
|
// onMouseover() {
|
|
// console.log('mouse over')
|
|
// }
|
|
}
|
|
}
|
|
|
|
const handleOutsideContextMenu = () => {
|
|
contextMenuParam.show = false
|
|
}
|
|
|
|
watchEffect(
|
|
() => {
|
|
if (!props.checkMode) {
|
|
tabStore.setCheckedKeys(props.server)
|
|
} else {
|
|
tabStore.addExpandedKey(props.server, `${ConnectionType.RedisDB}`)
|
|
}
|
|
},
|
|
{ flush: 'post' },
|
|
)
|
|
|
|
// the NTree node may get incorrect height after change data
|
|
// add key property for force refresh the component so that everything back to normal
|
|
const treeKey = ref(0)
|
|
defineExpose({
|
|
handleSelectContextMenu,
|
|
refreshTree: () => {
|
|
treeKey.value = Date.now()
|
|
tabStore.setExpandedKeys(props.server)
|
|
tabStore.setCheckedKeys(props.server)
|
|
},
|
|
deleteCheckedItems: () => {
|
|
const checkedKeys = tabStore.currentCheckedKeys
|
|
const redisKeys = map(checkedKeys, 'redisKey')
|
|
if (!isEmpty(redisKeys)) {
|
|
dialogStore.openDeleteKeyDialog(props.server, props.db, redisKeys)
|
|
}
|
|
},
|
|
exportCheckedItems: () => {
|
|
const checkedKeys = tabStore.currentCheckedKeys
|
|
const redisKeys = map(checkedKeys, 'redisKey')
|
|
if (!isEmpty(redisKeys)) {
|
|
dialogStore.openExportKeyDialog(props.server, props.db, redisKeys)
|
|
}
|
|
},
|
|
updateTTLCheckedItems: () => {
|
|
const checkedKeys = tabStore.currentCheckedKeys
|
|
const redisKeys = map(checkedKeys, 'redisKey')
|
|
if (!isEmpty(redisKeys)) {
|
|
dialogStore.openTTLDialog({
|
|
server: props.server,
|
|
db: props.db,
|
|
keys: redisKeys,
|
|
})
|
|
}
|
|
},
|
|
getSelectedKey: () => {
|
|
return selectedKeys.value || []
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
:style="{ backgroundColor }"
|
|
class="flex-box-v browser-tree-wrapper"
|
|
@contextmenu="(e) => e.preventDefault()"
|
|
@keydown.esc="contextMenuParam.show = false">
|
|
<n-spin v-if="props.loading" class="fill-height" />
|
|
<n-empty v-else-if="!props.loading && isEmpty(data)" class="empty-content" />
|
|
<n-tree
|
|
v-show="!props.loading && !isEmpty(data)"
|
|
:key="treeKey"
|
|
:animated="false"
|
|
:block-line="true"
|
|
:block-node="true"
|
|
:cancelable="false"
|
|
:cascade="true"
|
|
:checkable="props.checkMode"
|
|
:checked-keys="checkedKeys"
|
|
:data="data"
|
|
:expand-on-click="false"
|
|
:expanded-keys="expandedKeys"
|
|
:filter="(pattern, node) => includes(node.redisKey, pattern)"
|
|
:keyboard="false"
|
|
:node-props="nodeProps"
|
|
:pattern="props.pattern"
|
|
:render-label="renderLabel"
|
|
:render-prefix="renderPrefix"
|
|
:render-suffix="renderSuffix"
|
|
:selected-keys="selectedKeys"
|
|
:show-irrelevant-nodes="false"
|
|
check-strategy="child"
|
|
class="fill-height"
|
|
virtual-scroll
|
|
@keydown="onKeyShortcut"
|
|
@update:selected-keys="onUpdateSelectedKeys"
|
|
@update:expanded-keys="onUpdateExpanded"
|
|
@update:checked-keys="onUpdateCheckedKeys">
|
|
<template #empty>
|
|
<n-empty class="empty-content" />
|
|
</template>
|
|
</n-tree>
|
|
|
|
<!-- context menu -->
|
|
<n-dropdown
|
|
:options="contextMenuParam.options"
|
|
:render-icon="({ icon }) => render.renderIcon(icon)"
|
|
:render-label="({ label }) => render.renderLabel($t(label), { class: 'context-menu-item' })"
|
|
:show="contextMenuParam.show"
|
|
:x="contextMenuParam.x"
|
|
:y="contextMenuParam.y"
|
|
placement="bottom-start"
|
|
trigger="manual"
|
|
@clickoutside="handleOutsideContextMenu"
|
|
@select="handleSelectContextMenu" />
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
@use '@/styles/content';
|
|
|
|
.browser-tree-wrapper {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|