diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index 9a0144b..34e8e66 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -107,9 +107,9 @@ func (c *connectionService) SaveConnection(name string, param types.ConnectionCo return } -// RemoveConnection remove connection by name -func (c *connectionService) RemoveConnection(name string) (resp types.JSResp) { - err := c.conns.RemoveConnection(name) +// DeleteConnection remove connection by name +func (c *connectionService) DeleteConnection(name string) (resp types.JSResp) { + err := c.conns.DeleteConnection(name) if err != nil { resp.Msg = err.Error() return @@ -151,9 +151,9 @@ func (c *connectionService) RenameGroup(name, newName string) (resp types.JSResp return } -// RemoveGroup remove group by name -func (c *connectionService) RemoveGroup(name string, includeConn bool) (resp types.JSResp) { - err := c.conns.RemoveGroup(name, includeConn) +// DeleteGroup remove group by name +func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp types.JSResp) { + err := c.conns.DeleteGroup(name, includeConn) if err != nil { resp.Msg = err.Error() return @@ -318,12 +318,8 @@ func (c *connectionService) ScanKeys(connName string, db int, prefix string) (re return } - if !strings.HasSuffix(prefix, "*") { - prefix += ":*" - } - - //var keys []string - keys := map[string]keyItem{} + var keys []string + //keys := map[string]keyItem{} var cursor uint64 for { var loadedKey []string @@ -332,11 +328,11 @@ func (c *connectionService) ScanKeys(connName string, db int, prefix string) (re resp.Msg = err.Error() return } - //c.updateDBKey(connName, db, loadedKey) - for _, k := range loadedKey { - //t, _ := rdb.Type(ctx, k).Result() - keys[k] = keyItem{Type: "t"} - } + keys = append(keys, loadedKey...) + //for _, k := range loadedKey { + // //t, _ := rdb.Type(ctx, k).Result() + // keys[k] = keyItem{Type: "t"} + //} //keys = append(keys, loadedKey...) // no more loadedKey if cursor == 0 { @@ -873,15 +869,15 @@ func (c *connectionService) SetKeyTTL(connName string, db int, key string, ttl i return } -// RemoveKey remove redis key -func (c *connectionService) RemoveKey(connName string, db int, key string) (resp types.JSResp) { +// DeleteKey remove redis key +func (c *connectionService) DeleteKey(connName string, db int, keys []string) (resp types.JSResp) { rdb, ctx, err := c.getRedisClient(connName, db) if err != nil { resp.Msg = err.Error() return } - rmCount, err := rdb.Del(ctx, key).Result() + rmCount, err := rdb.Del(ctx, keys...).Result() if err != nil { resp.Msg = err.Error() return diff --git a/backend/storage/connections.go b/backend/storage/connections.go index 55d282c..17cf95e 100644 --- a/backend/storage/connections.go +++ b/backend/storage/connections.go @@ -210,8 +210,8 @@ func (c *ConnectionsStorage) UpdateConnection(name string, param types.Connectio return c.saveConnections(conns) } -// RemoveConnection remove special connection -func (c *ConnectionsStorage) RemoveConnection(name string) error { +// DeleteConnection remove special connection +func (c *ConnectionsStorage) DeleteConnection(name string) error { c.mutex.Lock() defer c.mutex.Unlock() @@ -328,8 +328,8 @@ func (c *ConnectionsStorage) RenameGroup(name, newName string) error { return c.saveConnections(conns) } -// RemoveGroup remove special group, include all connections under it -func (c *ConnectionsStorage) RemoveGroup(group string, includeConnection bool) error { +// DeleteGroup remove special group, include all connections under it +func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error { c.mutex.Lock() defer c.mutex.Unlock() diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6716a6d..4b45ee6 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,6 +10,7 @@ import plaintext from 'highlight.js/lib/languages/plaintext' import AddFieldsDialog from './components/dialogs/AddFieldsDialog.vue' import AppContent from './AppContent.vue' import GroupDialog from './components/dialogs/GroupDialog.vue' +import DeleteKeyDialog from './components/dialogs/DeleteKeyDialog.vue' hljs.registerLanguage('json', json) hljs.registerLanguage('plaintext', plaintext) @@ -46,6 +47,7 @@ const themeOverrides = { + diff --git a/frontend/src/components/content_value/ContentToolbar.vue b/frontend/src/components/content_value/ContentToolbar.vue index 4ee13e9..ce75739 100644 --- a/frontend/src/components/content_value/ContentToolbar.vue +++ b/frontend/src/components/content_value/ContentToolbar.vue @@ -41,7 +41,7 @@ const onReloadKey = () => { const confirmDialog = useConfirmDialog() const onDeleteKey = () => { confirmDialog.warning(i18n.t('remove_tip', { name: props.keyPath }), () => { - connectionStore.removeKey(props.server, props.db, props.keyPath).then((success) => { + connectionStore.deleteKey(props.server, props.db, props.keyPath).then((success) => { if (success) { message.success(i18n.t('delete_key_succ', { key: props.keyPath })) } diff --git a/frontend/src/components/dialogs/DeleteKeyDialog.vue b/frontend/src/components/dialogs/DeleteKeyDialog.vue new file mode 100644 index 0000000..f6e0982 --- /dev/null +++ b/frontend/src/components/dialogs/DeleteKeyDialog.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/frontend/src/components/dialogs/NewKeyDialog.vue b/frontend/src/components/dialogs/NewKeyDialog.vue index eed30f9..365341e 100644 --- a/frontend/src/components/dialogs/NewKeyDialog.vue +++ b/frontend/src/components/dialogs/NewKeyDialog.vue @@ -36,7 +36,7 @@ const dbOptions = computed(() => ) const newFormRef = ref(null) -const formLabelWidth = '60px' +const formLabelWidth = '100px' const options = computed(() => { return Object.keys(types).map((t) => ({ value: t, @@ -125,7 +125,7 @@ const onClose = () => { - + diff --git a/frontend/src/components/sidebar/BrowserPane.vue b/frontend/src/components/sidebar/BrowserPane.vue index 1acb0b4..1593aab 100644 --- a/frontend/src/components/sidebar/BrowserPane.vue +++ b/frontend/src/components/sidebar/BrowserPane.vue @@ -38,7 +38,7 @@ const message = useMessage() const onDeleteKey = () => { const { server, db, key } = currentSelect.value confirmDialog.warning(i18n.t('remove_tip', { name: key }), () => { - connectionStore.removeKey(server, db, key).then((success) => { + connectionStore.deleteKey(server, db, key).then((success) => { if (success) { message.success(i18n.t('delete_key_succ', { key })) } diff --git a/frontend/src/components/sidebar/BrowserTree.vue b/frontend/src/components/sidebar/BrowserTree.vue index 55fc1c3..098fd40 100644 --- a/frontend/src/components/sidebar/BrowserTree.vue +++ b/frontend/src/components/sidebar/BrowserTree.vue @@ -278,22 +278,24 @@ const handleSelectContextMenu = (key) => { nextTick().then(() => expandKey(nodeKey)) break case 'db_reload': - connectionStore.scanKeys(name, db) + connectionStore.reopenDatabase(name, db) break case 'db_newkey': case 'key_newkey': dialogStore.openNewKeyDialog(redisKey, name, db) break case 'key_reload': - connectionStore.scanKeys(name, db, redisKey) + connectionStore.loadKeys(name, db, redisKey) break case 'value_reload': connectionStore.loadKeyValue(name, db, redisKey) break case 'key_remove': + dialogStore.openDeleteKeyDialog(name, db, redisKey + ':*') + break case 'value_remove': confirmDialog.warning(i18n.t('remove_tip', { name: redisKey }), () => { - connectionStore.removeKey(name, db, redisKey).then((success) => { + connectionStore.deleteKey(name, db, redisKey).then((success) => { if (success) { message.success(i18n.t('delete_key_succ', { key: redisKey })) } diff --git a/frontend/src/components/sidebar/ConnectionTree.vue b/frontend/src/components/sidebar/ConnectionTree.vue index 84f0204..46449b4 100644 --- a/frontend/src/components/sidebar/ConnectionTree.vue +++ b/frontend/src/components/sidebar/ConnectionTree.vue @@ -191,7 +191,7 @@ const openConnection = async (name) => { const dialog = useDialog() const removeConnection = (name) => { confirmDialog.warning(i18n.t('remove_tip', { type: i18n.t('conn_name'), name }), async () => { - connectionStore.removeConnection(name).then(({ success, msg }) => { + connectionStore.deleteConnection(name).then(({ success, msg }) => { if (!success) { message.error(msg) } diff --git a/frontend/src/langs/en.json b/frontend/src/langs/en.json index cfddbf5..3c607c1 100644 --- a/frontend/src/langs/en.json +++ b/frontend/src/langs/en.json @@ -18,6 +18,12 @@ "forever": "Forever", "rename_key": "Rename Key", "delete_key": "Delete Key", + "batch_delete_key": "Batch Delete Key", + "db_index": "Database Index", + "key_expression": "Key Expression", + "affected_key": "Affected Key", + "show_affected_key": "Show Affected Key", + "confirm_delete_key": "Confirm Delete {num}", "delete_key_succ": "\"{key}\" has been deleted", "copy_value": "Copy Value", "edit_value": "Edit Value", diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index 4d18b29..6e8f2a3 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -18,6 +18,12 @@ "forever": "永久", "rename_key": "重命名键", "delete_key": "删除键", + "batch_delete_key": "批量删除键", + "db_index": "数据库编号", + "key_expression": "键名表达式", + "affected_key": "受影响的键名", + "show_affected_key": "查看受影响的键名", + "confirm_delete_key": "确认删除{num}个键", "delete_key_succ": "{key} 已被删除", "copy_value": "复制值", "edit_value": "修改值", diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js index f02559e..33f65cd 100644 --- a/frontend/src/stores/connections.js +++ b/frontend/src/stores/connections.js @@ -1,19 +1,19 @@ import { defineStore } from 'pinia' -import { findIndex, get, isEmpty, last, map, remove, size, sortedIndexBy, split, uniq } from 'lodash' +import { endsWith, findIndex, get, isEmpty, last, remove, size, sortedIndexBy, split, uniq } from 'lodash' import { AddHashField, AddListItem, AddZSetValue, CloseConnection, CreateGroup, + DeleteConnection, + DeleteGroup, + DeleteKey, GetConnection, GetKeyValue, ListConnection, OpenConnection, OpenDatabase, - RemoveConnection, - RemoveGroup, - RemoveKey, RenameGroup, RenameKey, SaveConnection, @@ -29,6 +29,7 @@ import { } from '../../wailsjs/go/services/connectionService.js' import { ConnectionType } from '../consts/connection_type.js' import useTabStore from './tab.js' +import { nextTick } from 'vue' const separator = ':' @@ -313,10 +314,10 @@ const useConnectionStore = defineStore('connections', { * @param name * @returns {Promise<{success: boolean, [msg]: string}>} */ - async removeConnection(name) { + async deleteConnection(name) { // close connection first await this.closeConnection(name) - const { success, msg } = await RemoveConnection(name) + const { success, msg } = await DeleteConnection(name) if (!success) { return { success: false, msg } } @@ -357,13 +358,13 @@ const useConnectionStore = defineStore('connections', { }, /** - * remove group by name + * delete group by name * @param {string} name * @param {boolean} [includeConn] * @returns {Promise<{success: boolean, [msg]: string}>} */ async deleteGroup(name, includeConn) { - const { success, msg } = await RemoveGroup(name, includeConn === true) + const { success, msg } = await DeleteGroup(name, includeConn === true) if (!success) { return { success: false, msg } } @@ -394,6 +395,18 @@ const useConnectionStore = defineStore('connections', { this._updateNodeChildren(connName, db, keys) }, + /** + * reopen database + * @param connName + * @param db + * @returns {Promise} + */ + async reopenDatabase(connName, db) { + const dbs = this.databases[connName] + dbs[db].children = undefined + dbs[db].isLeaf = false + }, + /** * load redis key * @param server @@ -426,40 +439,78 @@ const useConnectionStore = defineStore('connections', { * @param {string} connName * @param {number} db * @param {string} [prefix] full reload database if prefix is null - * @returns {Promise} + * @returns {Promise<{keys: string[]}>} */ async scanKeys(connName, db, prefix) { const { data, success, msg } = await ScanKeys(connName, db, prefix || '*') if (!success) { throw new Error(msg) } + const { keys = [] } = data + return { keys, success } + }, + + /** + * load keys with prefix + * @param {string} connName + * @param {number} db + * @param {string} [prefix] + * @returns {Promise} + */ + async loadKeys(connName, db, prefix) { + let scanPrefix = prefix + if (isEmpty(scanPrefix)) { + scanPrefix = '*' + } else { + if (!endsWith(prefix, separator + '*')) { + scanPrefix = prefix + separator + '*' + } + } + const { keys, success } = await this.scanKeys(connName, db, scanPrefix) + if (!success) { + return + } + // remove current keys below prefix + this._deleteKeyNodes(connName, db, prefix) + this._updateNodeChildren(connName, db, keys) + }, + + /** + * remove key with prefix + * @param {string} connName + * @param {number} db + * @param {string} prefix + * @returns {boolean} + * @private + */ + _deleteKeyNodes(connName, db, prefix) { const dbs = this.databases[connName] let node = dbs[db] - if (!isEmpty(prefix)) { - const prefixPart = split(prefix, separator) - for (const key of prefixPart) { - const idx = findIndex(node.children, { label: key }) - if (idx === -1) { - node = null - break - } + 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] } } - if (node != null) { - node.children = [] - } - - const { keys = [] } = data - this._updateNodeChildren(connName, db, keys) + return false }, /** * remove keys in db * @param {string} connName * @param {number} db - * @param {Object.[]} keys + * @param {string[]} keys * @private */ _updateNodeChildren(connName, db, keys) { @@ -495,7 +546,7 @@ const useConnectionStore = defineStore('connections', { dbs[db].children = [] } const keyStruct = dbs[db].children - for (const key in keys) { + for (const key of keys) { const keyPart = split(key, separator) // const prefixLen = size(keyPart) - 1 const len = size(keyPart) @@ -1010,7 +1061,7 @@ const useConnectionStore = defineStore('connections', { * @param {string} key * @private */ - _removeKey(connName, db, key) { + _deleteKeyNode(connName, db, key) { const dbs = this.databases[connName] const dbDetail = get(dbs, db, {}) @@ -1078,18 +1129,18 @@ const useConnectionStore = defineStore('connections', { }, /** - * remove redis key + * delete redis key * @param {string} connName * @param {number} db * @param {string} key * @returns {Promise} */ - async removeKey(connName, db, key) { + async deleteKey(connName, db, key) { try { - const { data, success, msg } = await RemoveKey(connName, db, key) + const { data, success, msg } = await DeleteKey(connName, db, [key]) if (success) { // update tree view data - this._removeKey(connName, db, key) + this._deleteKeyNode(connName, db, key) // set tab content empty const tab = useTabStore() @@ -1101,6 +1152,32 @@ const useConnectionStore = defineStore('connections', { return false }, + /** + * delete keys with prefix + * @param connName + * @param db + * @param prefix + * @param keys + * @returns {Promise} + */ + async deleteKeys(connName, db, prefix, keys) { + if (isEmpty(keys)) { + return false + } + try { + const { success, msg } = await DeleteKey(connName, db, keys) + if (success) { + for (const key of keys) { + await this.deleteKey(connName, db, key) + await nextTick() + } + return true + } + } finally { + } + return false + }, + /** * rename key * @param {string} connName @@ -1113,7 +1190,7 @@ const useConnectionStore = defineStore('connections', { 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._deleteKeyNode(connName, db, key) this._addKey(connName, db, newKey) return { success: true } } else { diff --git a/frontend/src/stores/dialog.js b/frontend/src/stores/dialog.js index 8322750..88ace03 100644 --- a/frontend/src/stores/dialog.js +++ b/frontend/src/stores/dialog.js @@ -36,6 +36,13 @@ const useDialogStore = defineStore('dialog', { }, renameDialogVisible: false, + deleteKeyParam: { + server: '', + db: 0, + key: '', + }, + deleteKeyDialogVisible: false, + selectTTL: -1, ttlDialogVisible: false, @@ -92,6 +99,16 @@ const useDialogStore = defineStore('dialog', { this.renameDialogVisible = false }, + openDeleteKeyDialog(server, db, key) { + this.deleteKeyParam.server = server + this.deleteKeyParam.db = db + this.deleteKeyParam.key = key + this.deleteKeyDialogVisible = true + }, + closeDeleteKeyDialog() { + this.deleteKeyDialogVisible = false + }, + /** * * @param {string} prefix