diff --git a/frontend/src/AppContent.vue b/frontend/src/AppContent.vue index 6400395..5f8252f 100644 --- a/frontend/src/AppContent.vue +++ b/frontend/src/AppContent.vue @@ -130,7 +130,7 @@ const dragging = computed(() => { .resize-divider { //height: 100%; - width: 2px; + width: 5px; border-left-width: 5px; background-color: v-bind('themeVars.tabColor'); } diff --git a/frontend/src/components/dialogs/NewKeyDialog.vue b/frontend/src/components/dialogs/NewKeyDialog.vue index a4fa3c3..efeba40 100644 --- a/frontend/src/components/dialogs/NewKeyDialog.vue +++ b/frontend/src/components/dialogs/NewKeyDialog.vue @@ -68,6 +68,7 @@ watch( const { prefix, server, db } = dialogStore.newKeyParam newForm.server = server newForm.key = isEmpty(prefix) ? '' : prefix + newForm.db = db newForm.type = options.value[0].value newForm.ttl = -1 newForm.value = null diff --git a/frontend/src/components/sidebar/BrowserTree.vue b/frontend/src/components/sidebar/BrowserTree.vue index db4f537..3991ec0 100644 --- a/frontend/src/components/sidebar/BrowserTree.vue +++ b/frontend/src/components/sidebar/BrowserTree.vue @@ -384,10 +384,13 @@ const onLoadTree = async (node) => { loading.value = false } break - case ConnectionType.RedisKey: - // load all children - // node.children = [] - break + // case ConnectionType.RedisKey: + // console.warn('load redis key', node.redisKey) + // node.keys = sumBy(node.children, 'keys') + // break + // case ConnectionType.RedisValue: + // node.keys = 1 + // break } } diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js index 0946e16..1c8c7e2 100644 --- a/frontend/src/stores/connections.js +++ b/frontend/src/stores/connections.js @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { endsWith, findIndex, get, isEmpty, size, split, toUpper, uniq } from 'lodash' +import { endsWith, get, isEmpty, join, remove, size, slice, split, sumBy, toUpper, uniq } from 'lodash' import { AddHashField, AddListItem, @@ -45,15 +45,17 @@ const useConnectionStore = defineStore('connections', { /** * @typedef {Object} DatabaseItem - * @property {string} key + * @property {string} key - tree node unique key * @property {string} label - * @property {string} name - server name, type != ConnectionType.Group only + * @property {string} [name] - server name, type != ConnectionType.Group only * @property {number} type * @property {number} [db] - database index, type == ConnectionType.RedisDB only - * @property {number} keys + * @property {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue only + * @property {number} [keys] - children key count * @property {boolean} [isLeaf] * @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only * @property {boolean} [expanded] - current node is expanded + * @property {DatabaseItem[]} [children] */ /** @@ -453,7 +455,7 @@ const useConnectionStore = defineStore('connections', { // append db node to current connection's children this._addKeyNodes(connName, db, keys) - this._tidyNodeChildren(dbs[db]) + this._tidyNode(connName, db) }, /** @@ -577,40 +579,9 @@ const useConnectionStore = defineStore('connections', { } // remove current keys below prefix - this._deleteKeyNodes(connName, db, prefix) + this._deleteKeyNode(connName, db, prefix, true) this._addKeyNodes(connName, db, keys) - this._tidyNodeChildren(this.databases[connName][db]) - }, - - /** - * remove key with prefix - * @param {string} connName - * @param {number} db - * @param {string} prefix - * @returns {boolean} - * @private - */ - _deleteKeyNodes(connName, db, prefix) { - const separator = this._getSeparator(connName) - const dbs = this.databases[connName] - let node = dbs[db] - const prefixPart = split(prefix, separator) - const partLen = size(prefixPart) - for (let i = 0; i < partLen; i++) { - let idx = findIndex(node.children, { label: prefixPart[i] }) - if (idx === -1) { - node = null - break - } - if (i === partLen - 1) { - // remove last part from parent - node.children.splice(idx, 1) - return true - } else { - node = node.children[idx] - } - } - return false + this._tidyNode(connName, db, prefix) }, /** @@ -627,6 +598,21 @@ const useConnectionStore = defineStore('connections', { return keySeparator }, + /** + * get node map + * @param connName + * @param db + * @returns {Map} + * @private + */ + _getNodeMap(connName, db) { + if (this.nodeMap[`${connName}#${db}`] == null) { + this.nodeMap[`${connName}#${db}`] = new Map() + } + // construct a tree node list, the format of item key likes 'server/db#type/key' + return this.nodeMap[`${connName}#${db}`] + }, + /** * remove keys in db * @param {string} connName @@ -635,34 +621,32 @@ const useConnectionStore = defineStore('connections', { * @private */ _addKeyNodes(connName, db, keys) { + if (isEmpty(keys)) { + return + } const separator = this._getSeparator(connName) const dbs = this.databases[connName] if (dbs[db].children == null) { dbs[db].children = [] } - if (this.nodeMap[`${connName}#${db}`] == null) { - this.nodeMap[`${connName}#${db}`] = new Map() - } - // construct a tree node list, the format of item key likes 'server/db#type/key' - const nodeMap = this.nodeMap[`${connName}#${db}`] + const nodeMap = this._getNodeMap(connName, db) const rootChildren = dbs[db].children for (const key of keys) { - const keyPart = split(key, separator) - // const prefixLen = size(keyPart) - 1 - const len = size(keyPart) + const keyParts = split(key, separator) + const len = size(keyParts) const lastIdx = len - 1 let handlePath = '' let children = rootChildren for (let i = 0; i < len; i++) { - handlePath += keyPart[i] + handlePath += keyParts[i] if (i !== lastIdx) { // layer - const nodeKey = `#${ConnectionType.RedisKey}/${handlePath}` + const nodeKey = `${ConnectionType.RedisKey}/${handlePath}` let selectedNode = nodeMap.get(nodeKey) if (selectedNode == null) { selectedNode = { - key: `${connName}/db${db}${nodeKey}`, - label: keyPart[i], + key: `${connName}/db${db}#${nodeKey}`, + label: keyParts[i], db, keys: 0, redisKey: handlePath, @@ -677,11 +661,11 @@ const useConnectionStore = defineStore('connections', { handlePath += separator } else { // key - const nodeKey = `#${ConnectionType.RedisValue}/${handlePath}` + const nodeKey = `${ConnectionType.RedisValue}/${handlePath}` const replaceKey = nodeMap.has(nodeKey) const selectedNode = { - key: `${connName}/db${db}${nodeKey}`, - label: keyPart[i], + key: `${connName}/db${db}#${nodeKey}`, + label: keyParts[i], db, keys: 0, redisKey: handlePath, @@ -712,23 +696,78 @@ const useConnectionStore = defineStore('connections', { }, /** - * sort all node item's children and calculate keys count - * @param node + * tidy node by key + * @param {string} connName + * @param {number} db + * @param {string} [key] + * @param {boolean} [skipResort] * @private */ - _tidyNodeChildren(node) { + _tidyNode(connName, db, key, skipResort) { + const nodeMap = this._getNodeMap(connName, db) + const separator = this._getSeparator(connName) + const keyParts = split(key, separator) + const totalParts = size(keyParts) + const parentKey = slice(keyParts, 0, totalParts - 1) + const dbNode = get(this.databases, [connName, db], {}) + const isDBRoot = isEmpty(parentKey) + let node + if (isDBRoot) { + // use db root node + node = dbNode + } else { + node = nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, separator)}`) + } + if (node == null) { + return false + } + const keyCountUpdated = this._tidyNodeChildren(node, skipResort) + + if (keyCountUpdated) { + // update key count of parent and above + if (!isDBRoot) { + let i = totalParts - 1 + for (; i > 0; i--) { + const parentKey = join(slice(keyParts, 0, i), separator) + const parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`) + if (parentNode == null) { + break + } + parentNode.keys = sumBy(parentNode.children, 'keys') + } + } + // update key count of db + dbNode.keys = sumBy(dbNode.children, 'keys') + } + return true + }, + + /** + * sort all node item's children and calculate keys count + * @param node + * @param {boolean} skipSort skip sorting children + * @returns {boolean} return whether key count changed + * @private + */ + _tidyNodeChildren(node, skipSort) { let count = 0 if (!isEmpty(node.children)) { - this._sortNodes(node.children) + if (skipSort !== true) { + this._sortNodes(node.children) + } for (const elem of node.children) { - this._tidyNodeChildren(elem) + this._tidyNodeChildren(elem, skipSort) count += elem.keys } } else { count += 1 } - node.keys = count + if (node.keys !== count) { + node.keys = count + return true + } + return false }, /** @@ -743,13 +782,11 @@ const useConnectionStore = defineStore('connections', { */ async setKey(connName, db, key, keyType, value, ttl) { try { - console.log(connName, db, key, keyType, value, ttl) const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl) if (success) { // update tree view data - // this._addKey(connName, db, key) this._addKeyNodes(connName, db, [key]) - this._tidyNodeChildren(this.databases[connName][db]) + this._tidyNode(connName, db, key) return { success } } else { return { success, msg } @@ -1092,62 +1129,87 @@ const useConnectionStore = defineStore('connections', { * @param {string} connName * @param {number} db * @param {string} key + * @param {boolean} [isLayer] * @private */ - _deleteKeyNode(connName, db, key) { - const dbs = this.databases[connName] - const dbDetail = get(dbs, db, {}) + _deleteKeyNode(connName, db, key, isLayer) { + const dbRoot = get(this.databases, [connName, db], {}) const separator = this._getSeparator(connName) - if (dbDetail == null) { - return + if (dbRoot == null) { + return false } - const nodeMap = this.nodeMap[`${connName}#${db}`] - if (nodeMap == null) { - return + const nodeMap = this._getNodeMap(connName, db) + const keyParts = split(key, separator) + const totalParts = size(keyParts) + if (isLayer === true) { + this._deleteChildrenKeyNodes(nodeMap, key) } - const idx = key.lastIndexOf(separator) - let parentNode = null - let parentKey = '' - if (idx === -1) { - // root - parentNode = dbDetail + // remove from parent in tree node + const parentKey = slice(keyParts, 0, totalParts - 1) + let parentNode + if (isEmpty(parentKey)) { + parentNode = dbRoot } else { - parentKey = key.substring(0, idx) - parentNode = nodeMap.get(`#${ConnectionType.RedisKey}/${parentKey}`) + parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, separator)}`) } - if (parentNode == null || parentNode.children == null) { - return + // not found parent node + if (parentNode == null) { + return false } + remove(parentNode.children, { + type: isLayer ? ConnectionType.RedisKey : ConnectionType.RedisValue, + redisKey: key, + }) - // remove children - const delIdx = findIndex(parentNode.children, { redisKey: key }) - if (delIdx !== -1) { - const childKeys = parentNode.children[delIdx].keys || 1 - parentNode.children.splice(delIdx, 1) - parentNode.keys = Math.max(parentNode.keys - childKeys, 0) - } - - // also remove parent node if no more children - while (isEmpty(parentNode.children)) { - const idx = parentKey.lastIndexOf(separator) - if (idx !== -1) { - parentKey = parentKey.substring(0, idx) - parentNode = nodeMap.get(`#${ConnectionType.RedisKey}/${parentKey}`) - if (parentNode != null) { - parentNode.keys = (parentNode.keys || 1) - 1 - parentNode.children = [] + // check and remove empty layer node + let i = totalParts - 1 + for (; i >= 0; i--) { + const anceKey = join(slice(keyParts, 0, i), separator) + if (i > 0) { + const anceNode = nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`) + if (isEmpty(anceNode.children)) { + nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`) + } else { + // remove last empty layer node from parent + if (i !== totalParts - 1) { + const redisKey = slice(keyParts, 0, i + 1) + remove(anceNode.children, { type: ConnectionType.RedisKey, redisKey }) + } + break } } else { - // reach root, remove from db - const delIdx = findIndex(dbDetail.children, { redisKey: parentKey }) - dbDetail.keys = (dbDetail.keys || 1) - 1 - dbDetail.children.splice(delIdx, 1) - break + // last one, remove from db node + remove(dbRoot.children, { type: ConnectionType.RedisKey, redisKey: keyParts[0] }) } } + + return true + }, + + /** + * delete node and all it's children from nodeMap + * @param nodeMap + * @param key + * @private + */ + _deleteChildrenKeyNodes(nodeMap, key) { + const mapKey = `${ConnectionType.RedisKey}/${key}` + const node = nodeMap.get(mapKey) + for (const child of node.children || []) { + if (child.type === ConnectionType.RedisValue) { + if (!nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) { + console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`) + } + } else if (child.type === ConnectionType.RedisKey) { + this._deleteChildrenKeyNodes(nodeMap, child.redisKey) + } + } + if (!nodeMap.delete(mapKey)) { + console.warn('delete map key', mapKey) + } }, /** @@ -1163,6 +1225,7 @@ const useConnectionStore = defineStore('connections', { if (success) { // update tree view data this._deleteKeyNode(connName, db, key) + this._tidyNode(connName, db, key, true) // set tab content empty const tab = useTabStore() @@ -1202,7 +1265,8 @@ const useConnectionStore = defineStore('connections', { if (endsWith(prefix, separator)) { prefix = prefix.substring(0, prefix.length - 1) } - await this._deleteKeyNode(connName, db, prefix) + this._deleteKeyNode(connName, db, prefix, true) + this._tidyNode(connName, db, prefix, true) return true } } finally {