perf: support keyboard navigation in key tree view (#238)

This commit is contained in:
Lykin 2024-05-14 10:09:50 +08:00
parent e2264b33b0
commit 455a911154
2 changed files with 184 additions and 13 deletions

View File

@ -5,7 +5,7 @@ import { NIcon, NSpace, NText, useThemeVars } 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 Database from '@/components/icons/Database.vue' import Database from '@/components/icons/Database.vue'
import { filter, find, get, includes, isEmpty, map, size, toUpper } from 'lodash' import { filter, find, first, get, includes, isEmpty, last, map, size, toUpper } 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'
@ -153,6 +153,133 @@ const menuOptions = {
], ],
} }
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 handleSelectContextMenu = (action) => { const handleSelectContextMenu = (action) => {
contextMenuParam.show = false contextMenuParam.show = false
const selectedKey = get(selectedKeys.value, 0) const selectedKey = get(selectedKeys.value, 0)
@ -471,12 +598,14 @@ const renderSuffix = ({ option }) => {
return null return null
} }
const nodeProps = ({ option }) => { /**
return { *
onClick: () => { * @param {RedisNodeItem} node
if (option.type === ConnectionType.RedisValue) { */
if (tabStore.setActivatedKey(props.server, option.key)) { const updateKeyDetail = (node) => {
const { db, redisKey, redisKeyCode } = option if (node.type === ConnectionType.RedisValue) {
if (tabStore.setActivatedKey(props.server, node.key)) {
const { db, redisKey, redisKeyCode } = node
browserStore.loadKeySummary({ browserStore.loadKeySummary({
server: props.server, server: props.server,
db, db,
@ -485,6 +614,12 @@ const nodeProps = ({ option }) => {
}) })
} }
} }
}
const nodeProps = ({ option }) => {
return {
onClick: () => {
updateKeyDetail(option)
}, },
onDblclick: () => { onDblclick: () => {
if (props.loading) { if (props.loading) {
@ -604,6 +739,11 @@ defineExpose({
check-strategy="child" check-strategy="child"
class="fill-height" class="fill-height"
virtual-scroll virtual-scroll
:keyboard="false"
@keydown.up="handleKeyUp"
@keydown.down="handleKeyDown"
@keydown.left="handleKeyLeft"
@keydown.right="handleKeyRight"
@keydown.delete="handleSelectContextMenu('value_remove')" @keydown.delete="handleSelectContextMenu('value_remove')"
@update:selected-keys="onUpdateSelectedKeys" @update:selected-keys="onUpdateSelectedKeys"
@update:expanded-keys="onUpdateExpanded" @update:expanded-keys="onUpdateExpanded"

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { endsWith, get, isEmpty, map, now, size } from 'lodash' import { endsWith, get, isEmpty, join, map, now, size, slice, split } from 'lodash'
import { import {
AddHashField, AddHashField,
AddListItem, AddListItem,
@ -773,6 +773,37 @@ const useBrowserStore = defineStore('browser', {
return serverInst.nodeMap.get(keyPart) return serverInst.nodeMap.get(keyPart)
}, },
/**
* get parent tree node by key name
* @param key
* @return {RedisNodeItem|null}
*/
getParentNode(key) {
const i = key.indexOf('#')
if (i < 0) {
return null
}
const [server, db] = split(key.substring(0, i), '/')
if (isEmpty(server) || isEmpty(db)) {
return null
}
/** @type {RedisServerState} **/
const serverInst = this.servers[server]
if (serverInst == null) {
return null
}
const separator = this.getSeparator(server)
const keyPart = key.substring(i)
const keyStartIdx = keyPart.indexOf('/')
const redisKey = keyPart.substring(keyStartIdx + 1)
const redisKeyParts = split(redisKey, separator)
const parentKey = slice(redisKeyParts, 0, size(redisKeyParts) - 1)
if (isEmpty(parentKey)) {
return serverInst.getRoot()
}
return serverInst.nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, separator)}`)
},
/** /**
* set redis key * set redis key
* @param {string} server * @param {string} server