diff --git a/frontend/src/AppContent.vue b/frontend/src/AppContent.vue index e3aefe4..3991122 100644 --- a/frontend/src/AppContent.vue +++ b/frontend/src/AppContent.vue @@ -1,21 +1,25 @@ - + emit('update:value', val)" :options="menuOptions" > @@ -101,7 +128,7 @@ const openGithub = () => { diff --git a/frontend/src/components/content/ContentServerPane.vue b/frontend/src/components/content/ContentServerPane.vue new file mode 100644 index 0000000..130381f --- /dev/null +++ b/frontend/src/components/content/ContentServerPane.vue @@ -0,0 +1,43 @@ + + + + + + + + + + + + {{ $t('new_conn') }} + + + + + + + diff --git a/frontend/src/components/ContentTab.vue b/frontend/src/components/content/ContentValueTab.vue similarity index 92% rename from frontend/src/components/ContentTab.vue rename to frontend/src/components/content/ContentValueTab.vue index 762fe1d..dbc69b0 100644 --- a/frontend/src/components/ContentTab.vue +++ b/frontend/src/components/content/ContentValueTab.vue @@ -1,9 +1,8 @@ + + + + + + + + + + + diff --git a/frontend/src/components/new_value/AddHashValue.vue b/frontend/src/components/new_value/AddHashValue.vue index ac7134a..84f99bc 100644 --- a/frontend/src/components/new_value/AddHashValue.vue +++ b/frontend/src/components/new_value/AddHashValue.vue @@ -3,7 +3,7 @@ import { ref } from 'vue' import { flatMap, reject } from 'lodash' import Add from '../icons/Add.vue' import Delete from '../icons/Delete.vue' -import IconButton from '../IconButton.vue' +import IconButton from '../common/IconButton.vue' import { useI18n } from 'vue-i18n' const props = defineProps({ diff --git a/frontend/src/components/new_value/AddListValue.vue b/frontend/src/components/new_value/AddListValue.vue index ee81741..0043a0f 100644 --- a/frontend/src/components/new_value/AddListValue.vue +++ b/frontend/src/components/new_value/AddListValue.vue @@ -3,7 +3,7 @@ import { ref } from 'vue' import { compact } from 'lodash' import Add from '../icons/Add.vue' import Delete from '../icons/Delete.vue' -import IconButton from '../IconButton.vue' +import IconButton from '../common/IconButton.vue' import { useI18n } from 'vue-i18n' const props = defineProps({ diff --git a/frontend/src/components/new_value/AddZSetValue.vue b/frontend/src/components/new_value/AddZSetValue.vue index 381e08c..5d84aaf 100644 --- a/frontend/src/components/new_value/AddZSetValue.vue +++ b/frontend/src/components/new_value/AddZSetValue.vue @@ -3,7 +3,7 @@ import { ref } from 'vue' import { isEmpty, reject } from 'lodash' import Add from '../icons/Add.vue' import Delete from '../icons/Delete.vue' -import IconButton from '../IconButton.vue' +import IconButton from '../common/IconButton.vue' import { useI18n } from 'vue-i18n' const props = defineProps({ diff --git a/frontend/src/components/new_value/NewHashValue.vue b/frontend/src/components/new_value/NewHashValue.vue index a21dd64..cfbd46b 100644 --- a/frontend/src/components/new_value/NewHashValue.vue +++ b/frontend/src/components/new_value/NewHashValue.vue @@ -3,7 +3,7 @@ import { ref } from 'vue' import { flatMap, reject } from 'lodash' import Add from '../icons/Add.vue' import Delete from '../icons/Delete.vue' -import IconButton from '../IconButton.vue' +import IconButton from '../common/IconButton.vue' const props = defineProps({ value: Array, diff --git a/frontend/src/components/new_value/NewListValue.vue b/frontend/src/components/new_value/NewListValue.vue index d0657c7..ecbff64 100644 --- a/frontend/src/components/new_value/NewListValue.vue +++ b/frontend/src/components/new_value/NewListValue.vue @@ -3,7 +3,7 @@ import { ref } from 'vue' import { compact } from 'lodash' import Add from '../icons/Add.vue' import Delete from '../icons/Delete.vue' -import IconButton from '../IconButton.vue' +import IconButton from '../common/IconButton.vue' const props = defineProps({ value: Array, diff --git a/frontend/src/components/new_value/NewSetValue.vue b/frontend/src/components/new_value/NewSetValue.vue index 4a32a77..279be41 100644 --- a/frontend/src/components/new_value/NewSetValue.vue +++ b/frontend/src/components/new_value/NewSetValue.vue @@ -3,7 +3,7 @@ import { ref } from 'vue' import { compact, uniq } from 'lodash' import Add from '../icons/Add.vue' import Delete from '../icons/Delete.vue' -import IconButton from '../IconButton.vue' +import IconButton from '../common/IconButton.vue' const props = defineProps({ value: Array, diff --git a/frontend/src/components/new_value/NewZSetValue.vue b/frontend/src/components/new_value/NewZSetValue.vue index 7280d22..f45a62e 100644 --- a/frontend/src/components/new_value/NewZSetValue.vue +++ b/frontend/src/components/new_value/NewZSetValue.vue @@ -3,7 +3,7 @@ import { ref } from 'vue' import { flatMap, isEmpty, reject } from 'lodash' import Add from '../icons/Add.vue' import Delete from '../icons/Delete.vue' -import IconButton from '../IconButton.vue' +import IconButton from '../common/IconButton.vue' const props = defineProps({ value: Array, diff --git a/frontend/src/components/NavigationPane.vue b/frontend/src/components/sidebar/ConnectionPane.vue similarity index 78% rename from frontend/src/components/NavigationPane.vue rename to frontend/src/components/sidebar/ConnectionPane.vue index b27adb2..5c61d08 100644 --- a/frontend/src/components/NavigationPane.vue +++ b/frontend/src/components/sidebar/ConnectionPane.vue @@ -1,12 +1,12 @@ - - + + diff --git a/frontend/src/components/sidebar/ConnectionTree.vue b/frontend/src/components/sidebar/ConnectionTree.vue new file mode 100644 index 0000000..f040b69 --- /dev/null +++ b/frontend/src/components/sidebar/ConnectionTree.vue @@ -0,0 +1,271 @@ + + + + + + + + + + + {{ openingConnection ? $t('opening_connection') : '' }} + + + + + + + + + + diff --git a/frontend/src/components/sidebar/ConnectionTreeItem.vue b/frontend/src/components/sidebar/ConnectionTreeItem.vue new file mode 100644 index 0000000..f560219 --- /dev/null +++ b/frontend/src/components/sidebar/ConnectionTreeItem.vue @@ -0,0 +1,26 @@ + + + + + {{ title }} + localhost:3306 + + + + diff --git a/frontend/src/components/sidebar/DatabasePane.vue b/frontend/src/components/sidebar/DatabasePane.vue new file mode 100644 index 0000000..047753e --- /dev/null +++ b/frontend/src/components/sidebar/DatabasePane.vue @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/ConnectionsTree.vue b/frontend/src/components/sidebar/DatabaseTree.vue similarity index 68% rename from frontend/src/components/ConnectionsTree.vue rename to frontend/src/components/sidebar/DatabaseTree.vue index 6c79f2d..cfefc4b 100644 --- a/frontend/src/components/ConnectionsTree.vue +++ b/frontend/src/components/sidebar/DatabaseTree.vue @@ -1,38 +1,37 @@ @@ -420,7 +342,9 @@ const handleOutsideContextMenu = () => { { :render-label="renderLabel" :render-prefix="renderPrefix" :render-suffix="renderSuffix" - block-line class="fill-height" virtual-scroll /> , connections: ConnectionItem[]}} + */ + state: () => ({ + connections: [], // all connections + databases: {}, // all databases in opened connections group by name + }), + getters: { + anyConnectionOpened() { + return !isEmpty(this.databases) + }, + }, + actions: { + /** + * Load all store connections struct from local profile + * @returns {Promise} + */ + async initConnections() { + if (!isEmpty(this.connections)) { + return + } + const conns = [] + const { data = [{ groupName: '', connections: [] }] } = await ListConnection() + for (let i = 0; i < data.length; i++) { + const group = data[i] + // Top level group + if (isEmpty(group.groupName)) { + const len = size(group.connections) + for (let j = 0; j < len; j++) { + const item = group.connections[j] + conns.push({ + key: item.name, + label: item.name, + name: item.name, + type: ConnectionType.Server, + // isLeaf: false, + }) + } + } else { + // Custom group + const children = [] + const len = size(group.connections) + for (let j = 0; j < len; j++) { + const item = group.connections[j] + const value = group.groupName + '/' + item.name + children.push({ + key: value, + label: item.name, + name: item.name, + type: ConnectionType.Server, + children: j === len - 1 ? undefined : [], + // isLeaf: false, + }) + } + conns.push({ + key: group.groupName, + label: group.groupName, + type: ConnectionType.Group, + children, + }) + } + } + this.connections = conns + console.debug(JSON.stringify(this.connections)) + }, + + /** + * get database server by name + * @param name + * @returns {ConnectionItem|null} + */ + getConnection(name) { + const conns = this.connections + for (let i = 0; i < conns.length; i++) { + if (conns[i].type === ConnectionType.Server && conns[i].key === name) { + return conns[i] + } else if (conns[i].type === ConnectionType.Group) { + const children = conns[i].children + for (let j = 0; j < children.length; j++) { + if (children[j].type === ConnectionType.Server && conns[i].key === name) { + return children[j] + } + } + } + } + return null + }, + + /** + * Check if connection is connected + * @param name + * @returns {boolean} + */ + isConnected(name) { + let dbs = get(this.databases, name, []) + return !isEmpty(dbs) + }, + + /** + * Open connection + * @param {string} name + * @returns {Promise} + */ + async openConnection(name) { + if (this.isConnected(name)) { + return + } + + const { data, success, msg } = await OpenConnection(name) + if (!success) { + throw new Error(msg) + } + // append to db node to current connection + // const connNode = this.getConnection(name) + // if (connNode == null) { + // throw new Error('no such connection') + // } + const { db } = data + if (isEmpty(db)) { + throw new Error('no db loaded') + } + const dbs = [] + for (let i = 0; i < db.length; i++) { + dbs.push({ + key: `${name}/${db[i].name}`, + label: db[i].name, + name: name, + keys: db[i].keys, + db: i, + type: ConnectionType.RedisDB, + isLeaf: false, + }) + } + this.databases[name] = dbs + }, + + /** + * Close connection + * @param {string} name + * @returns {Promise} + */ + async closeConnection(name) { + const { success, msg } = await CloseConnection(name) + if (!success) { + // throw new Error(msg) + return false + } + + delete this.databases[name] + return true + }, + + /** + * Open database and load all keys + * @param connName + * @param db + * @returns {Promise} + */ + async openDatabase(connName, db) { + const { data, success, msg } = await OpenDatabase(connName, db) + if (!success) { + throw new Error(msg) + } + const { keys = [] } = data + if (isEmpty(keys)) { + const dbs = this.databases[connName] + dbs[db].children = [] + dbs[db].opened = true + return + } + + // insert child to children list by order + const sortedInsertChild = (childrenList, item) => { + const insertIdx = sortedIndexBy(childrenList, item, 'key') + childrenList.splice(insertIdx, 0, item) + // childrenList.push(item) + } + // update all node item's children num + const updateChildrenNum = (node) => { + let count = 0 + const totalChildren = size(node.children) + if (totalChildren > 0) { + for (const elem of node.children) { + updateChildrenNum(elem) + count += elem.keys + } + } else { + count += 1 + } + node.keys = count + // node.children = sortBy(node.children, 'label') + } + + const keyStruct = [] + const mark = {} + for (const key in keys) { + const keyPart = split(key, separator) + // const prefixLen = size(keyPart) - 1 + const len = size(keyPart) + let handlePath = '' + let ks = keyStruct + for (let i = 0; i < len; i++) { + handlePath += keyPart[i] + if (i !== len - 1) { + // layer + const treeKey = `${handlePath}@${ConnectionType.RedisKey}` + if (!mark.hasOwnProperty(treeKey)) { + mark[treeKey] = { + key: `${connName}/db${db}/${treeKey}`, + label: keyPart[i], + name: connName, + db, + keys: 0, + redisKey: handlePath, + type: ConnectionType.RedisKey, + children: [], + } + sortedInsertChild(ks, mark[treeKey]) + } + ks = mark[treeKey].children + handlePath += separator + } else { + // key + const treeKey = `${handlePath}@${ConnectionType.RedisValue}` + mark[treeKey] = { + key: `${connName}/db${db}/${treeKey}`, + label: keyPart[i], + name: connName, + db, + keys: 0, + redisKey: handlePath, + type: ConnectionType.RedisValue, + } + sortedInsertChild(ks, mark[treeKey]) + } + } + } + + // append db node to current connection's children + const dbs = this.databases[connName] + dbs[db].children = keyStruct + dbs[db].opened = true + updateChildrenNum(dbs[db]) + }, + + /** + * select node + * @param key + * @param name + * @param db + * @param type + * @param redisKey + */ + select({ key, name, db, type, redisKey }) { + if (type === ConnectionType.RedisValue) { + console.log(`[click]key:${key} db: ${db} redis key: ${redisKey}`) + + // async get value for key + this.loadKeyValue(name, db, redisKey).then(() => {}) + } + }, + + /** + * load redis key + * @param server + * @param db + * @param key + */ + async loadKeyValue(server, db, key) { + try { + const { data, success, msg } = await GetKeyValue(server, db, key) + if (success) { + const { type, ttl, value } = data + const tab = useTabStore() + tab.upsertTab({ + server, + db, + type, + ttl, + key, + value, + }) + } else { + console.warn('TODO: handle get key fail') + } + } finally { + } + }, + + /** + * + * @param {string} connName + * @param {number} db + * @param {string} key + * @private + */ + _addKey(connName, db, key) { + const dbs = this.databases[connName] + const dbDetail = get(dbs, db, {}) + + if (dbDetail == null) { + return + } + + const descendantChain = [dbDetail] + + const keyPart = split(key, separator) + let redisKey = '' + const keyLen = size(keyPart) + let added = false + for (let i = 0; i < keyLen; i++) { + redisKey += keyPart[i] + + const node = last(descendantChain) + const nodeList = get(node, 'children', []) + const len = size(nodeList) + const isLastKeyPart = i === keyLen - 1 + for (let j = 0; j < len + 1; j++) { + const treeKey = get(nodeList[j], 'key') + const isLast = j >= len - 1 + const currentKey = `${connName}/db${db}/${redisKey}@${ + isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey + }` + if (treeKey > currentKey || isLast) { + // out of search range, add new item + if (isLastKeyPart) { + // key not exists, add new one + const item = { + key: currentKey, + label: keyPart[i], + name: connName, + db, + keys: 1, + redisKey, + type: ConnectionType.RedisValue, + } + if (isLast) { + nodeList.push(item) + } else { + nodeList.splice(j, 0, item) + } + added = true + } else { + // layer not exists, add new one + const item = { + key: currentKey, + label: keyPart[i], + name: connName, + db, + keys: 0, + redisKey, + type: ConnectionType.RedisKey, + children: [], + } + if (isLast) { + nodeList.push(item) + descendantChain.push(last(nodeList)) + } else { + nodeList.splice(j, 0, item) + descendantChain.push(nodeList[j]) + } + redisKey += separator + added = true + } + break + } else if (treeKey === currentKey) { + if (isLastKeyPart) { + // same key exists, do nothing + console.log('TODO: same key exist, do nothing now, should replace value later') + } else { + // same group exists, find into it's children + descendantChain.push(nodeList[j]) + redisKey += separator + } + break + } + } + } + + // update ancestor node's info + if (added) { + const desLen = size(descendantChain) + for (let i = 0; i < desLen; i++) { + const children = get(descendantChain[i], 'children', []) + let keys = 0 + for (const child of children) { + if (child.type === ConnectionType.RedisKey) { + keys += get(child, 'keys', 1) + } else if (child.type === ConnectionType.RedisValue) { + keys += get(child, 'keys', 0) + } + } + descendantChain[i].keys = keys + } + } + }, + + /** + * set redis key + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {number} keyType + * @param {any} value + * @param {number} ttl + * @returns {Promise<{[msg]: string, success: boolean}>} + */ + async setKey(connName, db, key, keyType, value, ttl) { + try { + const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl) + if (success) { + // update tree view data + this._addKey(connName, db, key) + return { success } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * update hash field + * when field is set, newField is null, delete field + * when field is null, newField is set, add new field + * when both field and newField are set, and field === newField, update field + * when both field and newField are set, and field !== newField, delete field and add newField + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {string} field + * @param {string} newField + * @param {string} value + * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} + */ + async setHash(connName, db, key, field, newField, value) { + try { + const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '') + if (success) { + const { updated = {} } = data + return { success, updated } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * insert or update hash field item + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields + * @param {string[]} fieldItems field1, value1, filed2, value2... + * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} + */ + async addHashField(connName, db, key, action, fieldItems) { + try { + const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems) + if (success) { + const { updated = {} } = data + return { success, updated } + } else { + return { success: false, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * remove hash field + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {string} field + * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>} + */ + async removeHashField(connName, db, key, field) { + try { + const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '') + if (success) { + const { removed = [] } = data + return { success, removed } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * insert list item + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {int} action 0: push to head, 1: push to tail + * @param {string[]}values + * @returns {Promise<*|{msg, success: boolean}>} + */ + async addListItem(connName, db, key, action, values) { + try { + return AddListItem(connName, db, key, action, values) + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * prepend item to head of list + * @param connName + * @param db + * @param key + * @param values + * @returns {Promise<[msg]: string, success: boolean, [item]: []>} + */ + async prependListItem(connName, db, key, values) { + try { + const { data, success, msg } = await AddListItem(connName, db, key, 0, values) + if (success) { + const { left = [] } = data + return { success, item: left } + } else { + return { success: false, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * append item to tail of list + * @param connName + * @param db + * @param key + * @param values + * @returns {Promise<[msg]: string, success: boolean, [item]: any[]>} + */ + async appendListItem(connName, db, key, values) { + try { + const { data, success, msg } = await AddListItem(connName, db, key, 1, values) + if (success) { + const { right = [] } = data + return { success, item: right } + } else { + return { success: false, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * update value of list item by index + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {number} index + * @param {string} value + * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} + */ + async updateListItem(connName, db, key, index, value) { + try { + const { data, success, msg } = await SetListItem(connName, db, key, index, value) + if (success) { + const { updated = {} } = data + return { success, updated } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * remove list item + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {number} index + * @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>} + */ + async removeListItem(connName, db, key, index) { + try { + const { data, success, msg } = await SetListItem(connName, db, key, index, '') + if (success) { + const { removed = [] } = data + return { success, removed } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * add item to set + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {string} value + * @returns {Promise<{[msg]: string, success: boolean}>} + */ + async addSetItem(connName, db, key, value) { + try { + const { success, msg } = await SetSetItem(connName, db, key, false, [value]) + if (success) { + return { success } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * update value of set item + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {string} value + * @param {string} newValue + * @returns {Promise<{[msg]: string, success: boolean}>} + */ + async updateSetItem(connName, db, key, value, newValue) { + try { + const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue) + if (success) { + return { success: true } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * remove item from set + * @param connName + * @param db + * @param key + * @param value + * @returns {Promise<{[msg]: string, success: boolean}>} + */ + async removeSetItem(connName, db, key, value) { + try { + const { success, msg } = await SetSetItem(connName, db, key, true, [value]) + if (success) { + return { success } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * add item to sorted set + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {number} action + * @param {Object.} vs value: score + * @returns {Promise<{[msg]: string, success: boolean}>} + */ + async addZSetItem(connName, db, key, action, vs) { + try { + const { success, msg } = await AddZSetValue(connName, db, key, action, vs) + if (success) { + return { success } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * update item of sorted set + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {string} value + * @param {string} newValue + * @param {number} score + * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}, [removed]: []}>} + */ + async updateZSetItem(connName, db, key, value, newValue, score) { + try { + const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score) + if (success) { + const { updated, removed } = data + return { success, updated, removed } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * remove item from sorted set + * @param {string} connName + * @param {number} db + * @param key + * @param {string} value + * @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>} + */ + async removeZSetItem(connName, db, key, value) { + try { + const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0) + if (success) { + const { removed } = data + return { success, removed } + } else { + return { success, msg } + } + } catch (e) { + return { success: false, msg: e.message } + } + }, + + /** + * reset key's ttl + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {number} ttl + * @returns {Promise} + */ + async setTTL(connName, db, key, ttl) { + try { + const { success, msg } = await SetKeyTTL(connName, db, key, ttl) + return success === true + } catch (e) { + return false + } + }, + + /** + * + * @param {string} connName + * @param {number} db + * @param {string} key + * @private + */ + _removeKey(connName, db, key) { + const dbs = this.databases[connName] + const dbDetail = get(dbs, db, {}) + + if (dbDetail == null) { + return + } + + const descendantChain = [dbDetail] + const keyPart = split(key, separator) + let redisKey = '' + const keyLen = size(keyPart) + let deleted = false + let forceBreak = false + for (let i = 0; i < keyLen && !forceBreak; i++) { + redisKey += keyPart[i] + + const node = last(descendantChain) + const nodeList = get(node, 'children', []) + const len = size(nodeList) + const isLastKeyPart = i === keyLen - 1 + for (let j = 0; j < len; j++) { + const treeKey = get(nodeList[j], 'key') + const currentKey = `${connName}/db${db}/${redisKey}@${ + isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey + }` + if (treeKey > currentKey) { + // out of search range, target not exists + forceBreak = true + break + } else if (treeKey === currentKey) { + if (isLastKeyPart) { + // find target + nodeList.splice(j, 1) + node.keys -= 1 + deleted = true + forceBreak = true + } else { + // find into it's children + descendantChain.push(nodeList[j]) + redisKey += separator + } + break + } + } + + if (forceBreak) { + break + } + } + // console.log(JSON.stringify(descendantChain)) + + // update ancestor node's info + if (deleted) { + const desLen = size(descendantChain) + for (let i = desLen - 1; i > 0; i--) { + const children = get(descendantChain[i], 'children', []) + const parent = descendantChain[i - 1] + if (isEmpty(children)) { + const parentChildren = get(parent, 'children', []) + const k = get(descendantChain[i], 'key') + remove(parentChildren, (item) => item.key === k) + } + parent.keys -= 1 + } + } + }, + + /** + * remove redis key + * @param {string} connName + * @param {number} db + * @param {string} key + * @returns {Promise} + */ + async removeKey(connName, db, key) { + try { + const { data, success, msg } = await RemoveKey(connName, db, key) + if (success) { + // update tree view data + this._removeKey(connName, db, key) + + // set tab content empty + const tab = useTabStore() + tab.emptyTab(connName) + return true + } + } finally { + } + return false + }, + + /** + * rename key + * @param {string} connName + * @param {number} db + * @param {string} key + * @param {string} newKey + * @returns {Promise<{[msg]: string, success: boolean}>} + */ + async renameKey(connName, db, key, newKey) { + const { success = false, msg } = await RenameKey(connName, db, key, newKey) + if (success) { + // delete old key and add new key struct + this._removeKey(connName, db, key) + this._addKey(connName, db, newKey) + return { success: true } + } else { + return { success: false, msg } + } + }, + }, +}) + +export default useConnectionStore diff --git a/frontend/src/stores/connection.js b/frontend/src/stores/database.js similarity index 92% rename from frontend/src/stores/connection.js rename to frontend/src/stores/database.js index 80fa0c0..75549d8 100644 --- a/frontend/src/stores/connection.js +++ b/frontend/src/stores/database.js @@ -6,7 +6,6 @@ import { AddZSetValue, CloseConnection, GetKeyValue, - ListConnection, OpenConnection, OpenDatabase, RemoveKey, @@ -21,12 +20,13 @@ import { } from '../../wailsjs/go/services/connectionService.js' import { ConnectionType } from '../consts/connection_type.js' import useTabStore from './tab.js' +import useConnectionStore from './connections.js' const separator = ':' -const useConnectionStore = defineStore('connection', { +const useDatabaseStore = defineStore('database', { /** - * @typedef {Object} ConnectionItem + * @typedef {Object} DatabaseItem * @property {string} key * @property {string} label * @property {string} name - server name, type != ConnectionType.Group only @@ -40,62 +40,14 @@ const useConnectionStore = defineStore('connection', { /** * - * @returns {{connections: ConnectionItem[]}} + * @returns {{connections: DatabaseItem[]}} */ state: () => ({ connections: [], // all connections list + databases: {}, // all database group by opened connections }), getters: {}, actions: { - /** - * Load all store connections struct from local profile - * @returns {Promise} - */ - async initConnection() { - if (!isEmpty(this.connections)) { - return - } - const { data = [{ groupName: '', connections: [] }] } = await ListConnection() - for (let i = 0; i < data.length; i++) { - const group = data[i] - // Top level group - if (isEmpty(group.groupName)) { - for (let j = 0; j < group.connections.length; j++) { - const item = group.connections[j] - this.connections.push({ - key: item.name, - label: item.name, - name: item.name, - type: ConnectionType.Server, - // isLeaf: false, - }) - } - } else { - // Custom group - const children = [] - for (let j = 0; j < group.connections.length; j++) { - const item = group.connections[j] - const value = group.groupName + '/' + item.name - children.push({ - key: value, - label: item.name, - name: item.name, - type: ConnectionType.Server, - children: j === group.connections.length - 1 ? undefined : [], - // isLeaf: false, - }) - } - this.connections.push({ - key: group.groupName, - label: group.groupName, - type: ConnectionType.Group, - children, - }) - } - } - console.debug(JSON.stringify(this.connections)) - }, - /** * Open connection * @param {string} connName @@ -107,7 +59,8 @@ const useConnectionStore = defineStore('connection', { throw new Error(msg) } // append to db node to current connection - const connNode = this.getConnection(connName) + const connStore = useConnectionStore() + const connNode = connStore.getConnection(connName) if (connNode == null) { throw new Error('no such connection') } @@ -901,4 +854,4 @@ const useConnectionStore = defineStore('connection', { }, }) -export default useConnectionStore +export default useDatabaseStore diff --git a/frontend/src/stores/tab.js b/frontend/src/stores/tab.js index 3fb14f5..cd3e4d3 100644 --- a/frontend/src/stores/tab.js +++ b/frontend/src/stores/tab.js @@ -1,4 +1,4 @@ -import { find, findIndex, isEmpty, size } from 'lodash' +import { find, findIndex, size } from 'lodash' import { defineStore } from 'pinia' const useTabStore = defineStore('tab', { @@ -21,6 +21,8 @@ const useTabStore = defineStore('tab', { * @returns {{tabList: TabItem[], activatedTab: string, activatedIndex: number}} */ state: () => ({ + nav: 'server', + asideWidth: 300, tabList: [], activatedTab: '', activatedIndex: 0, // current activated tab index @@ -31,9 +33,9 @@ const useTabStore = defineStore('tab', { * @returns {TabItem[]} */ tabs() { - if (isEmpty(this.tabList)) { - this.newBlankTab() - } + // if (isEmpty(this.tabList)) { + // this.newBlankTab() + // } return this.tabList }, @@ -53,7 +55,7 @@ const useTabStore = defineStore('tab', { currentTabIndex() { const len = size(this.tabs) if (this.activatedIndex < 0 || this.activatedIndex >= len) { - this.activatedIndex = 0 + this._setActivatedIndex(0) } return this.tabs[this.activatedIndex] }, @@ -68,17 +70,22 @@ const useTabStore = defineStore('tab', { title: 'new tab', blank: true, }) - this.activatedIndex = size(this.tabList) - 1 + this._setActivatedIndex(size(this.tabList) - 1) + }, + + _setActivatedIndex(idx) { + this.activatedIndex = idx + this.nav = idx >= 0 ? 'structure' : 'server' }, /** * update or insert a new tab if not exists with the same name * @param {string} server - * @param {number} db - * @param {number} type - * @param {number} ttl - * @param {string} key - * @param {*} value + * @param {number} [db] + * @param {number} [type] + * @param {number} [ttl] + * @param {string} [key] + * @param {*} [value] */ upsertTab({ server, db, type, ttl, key, value }) { let tabIndex = findIndex(this.tabList, { name: server }) @@ -90,20 +97,21 @@ const useTabStore = defineStore('tab', { type, ttl, key, - value, + valu, }) tabIndex = this.tabList.length - 1 } const tab = this.tabList[tabIndex] tab.blank = false - tab.title = `${server}/db${db}` + // tab.title = db !== undefined ? `${server}/db${db}` : `${server}` + tab.title = server tab.server = server tab.db = db tab.type = type tab.ttl = ttl tab.key = key tab.value = value - this.activatedIndex = tabIndex + this._setActivatedIndex(tabIndex) // this.activatedTab = tab.name }, @@ -149,23 +157,27 @@ const useTabStore = defineStore('tab', { const len = size(this.tabs) // ignore remove last blank tab if (len === 1 && this.tabs[0].blank) { - return + return null } if (tabIndex < 0 || tabIndex >= len) { - return + return null } - this.tabList.splice(tabIndex, 1) + const removed = this.tabList.splice(tabIndex, 1) - if (len === 1) { - this.newBlankTab() - } else { - // Update select index if removed index equal current selected - this.activatedIndex -= 1 - if (this.activatedIndex < 0 && this.tabList.length > 0) { - this.activatedIndex = 0 + // update select index if removed index equal current selected + this.activatedIndex -= 1 + if (this.activatedIndex < 0) { + if (this.tabList.length > 0) { + this._setActivatedIndex(0) + } else { + this._setActivatedIndex(-1) } + } else { + this._setActivatedIndex(this.activatedIndex) } + + return size(removed) > 0 ? removed[0] : null }, removeTabByName(tabName) { const idx = findIndex(this.tabs, { name: tabName }) diff --git a/frontend/src/style.scss b/frontend/src/style.scss index 2419964..5d5b12e 100644 --- a/frontend/src/style.scss +++ b/frontend/src/style.scss @@ -104,6 +104,6 @@ body { } .context-menu-item { - min-width: 120px; + min-width: 100px; padding-right: 10px; }