From 455a91115423ed96bea66faed6ea9fe67c7a4d49 Mon Sep 17 00:00:00 2001 From: Lykin <137850705+tiny-craft@users.noreply.github.com> Date: Tue, 14 May 2024 10:09:50 +0800 Subject: [PATCH] perf: support keyboard navigation in key tree view (#238) --- .../src/components/sidebar/BrowserTree.vue | 164 ++++++++++++++++-- frontend/src/stores/browser.js | 33 +++- 2 files changed, 184 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/sidebar/BrowserTree.vue b/frontend/src/components/sidebar/BrowserTree.vue index 3987e93..5c37fe2 100644 --- a/frontend/src/components/sidebar/BrowserTree.vue +++ b/frontend/src/components/sidebar/BrowserTree.vue @@ -5,7 +5,7 @@ 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, 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 Refresh from '@/components/icons/Refresh.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) => { contextMenuParam.show = false const selectedKey = get(selectedKeys.value, 0) @@ -471,20 +598,28 @@ const renderSuffix = ({ option }) => { return null } +/** + * + * @param {RedisNodeItem} node + */ +const updateKeyDetail = (node) => { + if (node.type === ConnectionType.RedisValue) { + 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: () => { - if (option.type === ConnectionType.RedisValue) { - if (tabStore.setActivatedKey(props.server, option.key)) { - const { db, redisKey, redisKeyCode } = option - browserStore.loadKeySummary({ - server: props.server, - db, - key: redisKeyCode || redisKey, - clearValue: true, - }) - } - } + updateKeyDetail(option) }, onDblclick: () => { if (props.loading) { @@ -604,6 +739,11 @@ defineExpose({ check-strategy="child" class="fill-height" virtual-scroll + :keyboard="false" + @keydown.up="handleKeyUp" + @keydown.down="handleKeyDown" + @keydown.left="handleKeyLeft" + @keydown.right="handleKeyRight" @keydown.delete="handleSelectContextMenu('value_remove')" @update:selected-keys="onUpdateSelectedKeys" @update:expanded-keys="onUpdateExpanded" diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js index 83d7957..837d112 100644 --- a/frontend/src/stores/browser.js +++ b/frontend/src/stores/browser.js @@ -1,5 +1,5 @@ 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 { AddHashField, AddListItem, @@ -773,6 +773,37 @@ const useBrowserStore = defineStore('browser', { 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 * @param {string} server