feat: add delete key dialog and logic

refactor: tidy function of connection store
This commit is contained in:
tiny-craft 2023-07-06 01:22:14 +08:00
parent d7955702f8
commit 1841ccf3d3
13 changed files with 293 additions and 63 deletions

View File

@ -107,9 +107,9 @@ func (c *connectionService) SaveConnection(name string, param types.ConnectionCo
return return
} }
// RemoveConnection remove connection by name // DeleteConnection remove connection by name
func (c *connectionService) RemoveConnection(name string) (resp types.JSResp) { func (c *connectionService) DeleteConnection(name string) (resp types.JSResp) {
err := c.conns.RemoveConnection(name) err := c.conns.DeleteConnection(name)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -151,9 +151,9 @@ func (c *connectionService) RenameGroup(name, newName string) (resp types.JSResp
return return
} }
// RemoveGroup remove group by name // DeleteGroup remove group by name
func (c *connectionService) RemoveGroup(name string, includeConn bool) (resp types.JSResp) { func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp types.JSResp) {
err := c.conns.RemoveGroup(name, includeConn) err := c.conns.DeleteGroup(name, includeConn)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -318,12 +318,8 @@ func (c *connectionService) ScanKeys(connName string, db int, prefix string) (re
return return
} }
if !strings.HasSuffix(prefix, "*") { var keys []string
prefix += ":*" //keys := map[string]keyItem{}
}
//var keys []string
keys := map[string]keyItem{}
var cursor uint64 var cursor uint64
for { for {
var loadedKey []string var loadedKey []string
@ -332,11 +328,11 @@ func (c *connectionService) ScanKeys(connName string, db int, prefix string) (re
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
//c.updateDBKey(connName, db, loadedKey) keys = append(keys, loadedKey...)
for _, k := range loadedKey { //for _, k := range loadedKey {
//t, _ := rdb.Type(ctx, k).Result() // //t, _ := rdb.Type(ctx, k).Result()
keys[k] = keyItem{Type: "t"} // keys[k] = keyItem{Type: "t"}
} //}
//keys = append(keys, loadedKey...) //keys = append(keys, loadedKey...)
// no more loadedKey // no more loadedKey
if cursor == 0 { if cursor == 0 {
@ -873,15 +869,15 @@ func (c *connectionService) SetKeyTTL(connName string, db int, key string, ttl i
return return
} }
// RemoveKey remove redis key // DeleteKey remove redis key
func (c *connectionService) RemoveKey(connName string, db int, key string) (resp types.JSResp) { func (c *connectionService) DeleteKey(connName string, db int, keys []string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) rdb, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
rmCount, err := rdb.Del(ctx, key).Result() rmCount, err := rdb.Del(ctx, keys...).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return

View File

@ -210,8 +210,8 @@ func (c *ConnectionsStorage) UpdateConnection(name string, param types.Connectio
return c.saveConnections(conns) return c.saveConnections(conns)
} }
// RemoveConnection remove special connection // DeleteConnection remove special connection
func (c *ConnectionsStorage) RemoveConnection(name string) error { func (c *ConnectionsStorage) DeleteConnection(name string) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
@ -328,8 +328,8 @@ func (c *ConnectionsStorage) RenameGroup(name, newName string) error {
return c.saveConnections(conns) return c.saveConnections(conns)
} }
// RemoveGroup remove special group, include all connections under it // DeleteGroup remove special group, include all connections under it
func (c *ConnectionsStorage) RemoveGroup(group string, includeConnection bool) error { func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()

View File

@ -10,6 +10,7 @@ import plaintext from 'highlight.js/lib/languages/plaintext'
import AddFieldsDialog from './components/dialogs/AddFieldsDialog.vue' import AddFieldsDialog from './components/dialogs/AddFieldsDialog.vue'
import AppContent from './AppContent.vue' import AppContent from './AppContent.vue'
import GroupDialog from './components/dialogs/GroupDialog.vue' import GroupDialog from './components/dialogs/GroupDialog.vue'
import DeleteKeyDialog from './components/dialogs/DeleteKeyDialog.vue'
hljs.registerLanguage('json', json) hljs.registerLanguage('json', json)
hljs.registerLanguage('plaintext', plaintext) hljs.registerLanguage('plaintext', plaintext)
@ -46,6 +47,7 @@ const themeOverrides = {
<new-key-dialog /> <new-key-dialog />
<add-fields-dialog /> <add-fields-dialog />
<rename-key-dialog /> <rename-key-dialog />
<delete-key-dialog />
<set-ttl-dialog /> <set-ttl-dialog />
<preferences-dialog /> <preferences-dialog />
</n-dialog-provider> </n-dialog-provider>

View File

@ -41,7 +41,7 @@ const onReloadKey = () => {
const confirmDialog = useConfirmDialog() const confirmDialog = useConfirmDialog()
const onDeleteKey = () => { const onDeleteKey = () => {
confirmDialog.warning(i18n.t('remove_tip', { name: props.keyPath }), () => { 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) { if (success) {
message.success(i18n.t('delete_key_succ', { key: props.keyPath })) message.success(i18n.t('delete_key_succ', { key: props.keyPath }))
} }

View File

@ -0,0 +1,124 @@
<script setup>
import { reactive, watch } from 'vue'
import useDialog from '../../stores/dialog'
import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import useConnectionStore from '../../stores/connections.js'
import { isEmpty, size } from 'lodash'
const deleteForm = reactive({
server: '',
db: 0,
key: '',
showAffected: false,
loadingAffected: false,
affectedKeys: [],
})
const dialogStore = useDialog()
const connectionStore = useConnectionStore()
watch(
() => dialogStore.deleteKeyDialogVisible,
(visible) => {
if (visible) {
const { server, db, key } = dialogStore.deleteKeyParam
deleteForm.server = server
deleteForm.db = db
deleteForm.key = key
deleteForm.showAffected = false
deleteForm.loadingAffected = false
deleteForm.affectedKeys = []
}
}
)
const scanAffectedKey = async () => {
try {
deleteForm.loadingAffected = true
const { keys = [] } = await connectionStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
deleteForm.affectedKeys = keys || []
deleteForm.showAffected = true
} finally {
deleteForm.loadingAffected = false
}
}
const resetAffected = () => {
deleteForm.showAffected = false
deleteForm.affectedKeys = []
}
const i18n = useI18n()
const message = useMessage()
const onConfirmDelete = async () => {
try {
const { server, db, key } = deleteForm
const success = await connectionStore.deleteKeys(server, db, key, deleteForm.affectedKeys)
if (success) {
message.success(i18n.t('handle_succ'))
}
} catch (e) {
message.error(e.message)
}
dialogStore.closeDeleteKeyDialog()
}
const onClose = () => {
dialogStore.closeDeleteKeyDialog()
}
</script>
<template>
<n-modal
v-model:show="dialogStore.deleteKeyDialogVisible"
:closable="false"
:close-on-esc="false"
:mask-closable="false"
:show-icon="false"
:title="$t('batch_delete_key')"
preset="dialog"
transform-origin="center"
>
<n-form
:model="deleteForm"
:show-require-mark="false"
label-align="right"
label-placement="left"
label-width="auto"
>
<n-form-item :label="$t('server')">
<n-input :value="deleteForm.server" readonly />
</n-form-item>
<n-form-item :label="$t('db_index')">
<n-input :value="deleteForm.db.toString()" readonly />
</n-form-item>
<n-form-item :label="$t('key_expression')" required>
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
</n-form-item>
<n-card v-if="deleteForm.showAffected" :title="$t('affected_key')" size="small">
<n-skeleton v-if="deleteForm.loadingAffected" text :repeat="10" />
<n-log
v-else
:rows="10"
:line-height="1.5"
:lines="deleteForm.affectedKeys"
style="user-select: text; cursor: text"
/>
</n-card>
</n-form>
<template #action>
<div class="flex-item n-dialog__action">
<n-button @click="onClose">{{ $t('cancel') }}</n-button>
<n-button v-if="!deleteForm.showAffected" type="primary" @click="scanAffectedKey">
{{ $t('show_affected_key') }}
</n-button>
<n-button v-else type="error" :disabled="isEmpty(deleteForm.affectedKeys)" @click="onConfirmDelete">
{{ $t('confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
</n-button>
</div>
</template>
</n-modal>
</template>
<style lang="scss" scoped></style>

View File

@ -36,7 +36,7 @@ const dbOptions = computed(() =>
) )
const newFormRef = ref(null) const newFormRef = ref(null)
const formLabelWidth = '60px' const formLabelWidth = '100px'
const options = computed(() => { const options = computed(() => {
return Object.keys(types).map((t) => ({ return Object.keys(types).map((t) => ({
value: t, value: t,
@ -125,7 +125,7 @@ const onClose = () => {
<n-form-item :label="$t('key')" path="key" required> <n-form-item :label="$t('key')" path="key" required>
<n-input v-model:value="newForm.key" placeholder="" /> <n-input v-model:value="newForm.key" placeholder="" />
</n-form-item> </n-form-item>
<n-form-item label="DB" path="db" required> <n-form-item :label="$t('db_index')" path="db" required>
<n-select v-model:value="newForm.db" :options="dbOptions" /> <n-select v-model:value="newForm.db" :options="dbOptions" />
</n-form-item> </n-form-item>
<n-form-item :label="$t('type')" path="type" required> <n-form-item :label="$t('type')" path="type" required>

View File

@ -38,7 +38,7 @@ const message = useMessage()
const onDeleteKey = () => { const onDeleteKey = () => {
const { server, db, key } = currentSelect.value const { server, db, key } = currentSelect.value
confirmDialog.warning(i18n.t('remove_tip', { name: key }), () => { confirmDialog.warning(i18n.t('remove_tip', { name: key }), () => {
connectionStore.removeKey(server, db, key).then((success) => { connectionStore.deleteKey(server, db, key).then((success) => {
if (success) { if (success) {
message.success(i18n.t('delete_key_succ', { key })) message.success(i18n.t('delete_key_succ', { key }))
} }

View File

@ -278,22 +278,24 @@ const handleSelectContextMenu = (key) => {
nextTick().then(() => expandKey(nodeKey)) nextTick().then(() => expandKey(nodeKey))
break break
case 'db_reload': case 'db_reload':
connectionStore.scanKeys(name, db) connectionStore.reopenDatabase(name, db)
break break
case 'db_newkey': case 'db_newkey':
case 'key_newkey': case 'key_newkey':
dialogStore.openNewKeyDialog(redisKey, name, db) dialogStore.openNewKeyDialog(redisKey, name, db)
break break
case 'key_reload': case 'key_reload':
connectionStore.scanKeys(name, db, redisKey) connectionStore.loadKeys(name, db, redisKey)
break break
case 'value_reload': case 'value_reload':
connectionStore.loadKeyValue(name, db, redisKey) connectionStore.loadKeyValue(name, db, redisKey)
break break
case 'key_remove': case 'key_remove':
dialogStore.openDeleteKeyDialog(name, db, redisKey + ':*')
break
case 'value_remove': case 'value_remove':
confirmDialog.warning(i18n.t('remove_tip', { name: redisKey }), () => { confirmDialog.warning(i18n.t('remove_tip', { name: redisKey }), () => {
connectionStore.removeKey(name, db, redisKey).then((success) => { connectionStore.deleteKey(name, db, redisKey).then((success) => {
if (success) { if (success) {
message.success(i18n.t('delete_key_succ', { key: redisKey })) message.success(i18n.t('delete_key_succ', { key: redisKey }))
} }

View File

@ -191,7 +191,7 @@ const openConnection = async (name) => {
const dialog = useDialog() const dialog = useDialog()
const removeConnection = (name) => { const removeConnection = (name) => {
confirmDialog.warning(i18n.t('remove_tip', { type: i18n.t('conn_name'), name }), async () => { 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) { if (!success) {
message.error(msg) message.error(msg)
} }

View File

@ -18,6 +18,12 @@
"forever": "Forever", "forever": "Forever",
"rename_key": "Rename Key", "rename_key": "Rename Key",
"delete_key": "Delete 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", "delete_key_succ": "\"{key}\" has been deleted",
"copy_value": "Copy Value", "copy_value": "Copy Value",
"edit_value": "Edit Value", "edit_value": "Edit Value",

View File

@ -18,6 +18,12 @@
"forever": "永久", "forever": "永久",
"rename_key": "重命名键", "rename_key": "重命名键",
"delete_key": "删除键", "delete_key": "删除键",
"batch_delete_key": "批量删除键",
"db_index": "数据库编号",
"key_expression": "键名表达式",
"affected_key": "受影响的键名",
"show_affected_key": "查看受影响的键名",
"confirm_delete_key": "确认删除{num}个键",
"delete_key_succ": "{key} 已被删除", "delete_key_succ": "{key} 已被删除",
"copy_value": "复制值", "copy_value": "复制值",
"edit_value": "修改值", "edit_value": "修改值",

View File

@ -1,19 +1,19 @@
import { defineStore } from 'pinia' 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 { import {
AddHashField, AddHashField,
AddListItem, AddListItem,
AddZSetValue, AddZSetValue,
CloseConnection, CloseConnection,
CreateGroup, CreateGroup,
DeleteConnection,
DeleteGroup,
DeleteKey,
GetConnection, GetConnection,
GetKeyValue, GetKeyValue,
ListConnection, ListConnection,
OpenConnection, OpenConnection,
OpenDatabase, OpenDatabase,
RemoveConnection,
RemoveGroup,
RemoveKey,
RenameGroup, RenameGroup,
RenameKey, RenameKey,
SaveConnection, SaveConnection,
@ -29,6 +29,7 @@ import {
} from '../../wailsjs/go/services/connectionService.js' } from '../../wailsjs/go/services/connectionService.js'
import { ConnectionType } from '../consts/connection_type.js' import { ConnectionType } from '../consts/connection_type.js'
import useTabStore from './tab.js' import useTabStore from './tab.js'
import { nextTick } from 'vue'
const separator = ':' const separator = ':'
@ -313,10 +314,10 @@ const useConnectionStore = defineStore('connections', {
* @param name * @param name
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
*/ */
async removeConnection(name) { async deleteConnection(name) {
// close connection first // close connection first
await this.closeConnection(name) await this.closeConnection(name)
const { success, msg } = await RemoveConnection(name) const { success, msg } = await DeleteConnection(name)
if (!success) { if (!success) {
return { success: false, msg } return { success: false, msg }
} }
@ -357,13 +358,13 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* remove group by name * delete group by name
* @param {string} name * @param {string} name
* @param {boolean} [includeConn] * @param {boolean} [includeConn]
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
*/ */
async deleteGroup(name, includeConn) { async deleteGroup(name, includeConn) {
const { success, msg } = await RemoveGroup(name, includeConn === true) const { success, msg } = await DeleteGroup(name, includeConn === true)
if (!success) { if (!success) {
return { success: false, msg } return { success: false, msg }
} }
@ -394,6 +395,18 @@ const useConnectionStore = defineStore('connections', {
this._updateNodeChildren(connName, db, keys) this._updateNodeChildren(connName, db, keys)
}, },
/**
* reopen database
* @param connName
* @param db
* @returns {Promise<void>}
*/
async reopenDatabase(connName, db) {
const dbs = this.databases[connName]
dbs[db].children = undefined
dbs[db].isLeaf = false
},
/** /**
* load redis key * load redis key
* @param server * @param server
@ -426,40 +439,78 @@ const useConnectionStore = defineStore('connections', {
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {string} [prefix] full reload database if prefix is null * @param {string} [prefix] full reload database if prefix is null
* @returns {Promise<void>} * @returns {Promise<{keys: string[]}>}
*/ */
async scanKeys(connName, db, prefix) { async scanKeys(connName, db, prefix) {
const { data, success, msg } = await ScanKeys(connName, db, prefix || '*') const { data, success, msg } = await ScanKeys(connName, db, prefix || '*')
if (!success) { if (!success) {
throw new Error(msg) 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<void>}
*/
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 // 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] const dbs = this.databases[connName]
let node = dbs[db] let node = dbs[db]
if (!isEmpty(prefix)) { const prefixPart = split(prefix, separator)
const prefixPart = split(prefix, separator) const partLen = size(prefixPart)
for (const key of prefixPart) { for (let i = 0; i < partLen; i++) {
const idx = findIndex(node.children, { label: key }) let idx = findIndex(node.children, { label: prefixPart[i] })
if (idx === -1) { if (idx === -1) {
node = null node = null
break break
} }
if (i === partLen - 1) {
// remove last part from parent
node.children.splice(idx, 1)
return true
} else {
node = node.children[idx] node = node.children[idx]
} }
} }
if (node != null) { return false
node.children = []
}
const { keys = [] } = data
this._updateNodeChildren(connName, db, keys)
}, },
/** /**
* remove keys in db * remove keys in db
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {Object.<string, {}>[]} keys * @param {string[]} keys
* @private * @private
*/ */
_updateNodeChildren(connName, db, keys) { _updateNodeChildren(connName, db, keys) {
@ -495,7 +546,7 @@ const useConnectionStore = defineStore('connections', {
dbs[db].children = [] dbs[db].children = []
} }
const keyStruct = dbs[db].children const keyStruct = dbs[db].children
for (const key in keys) { for (const key of keys) {
const keyPart = split(key, separator) const keyPart = split(key, separator)
// const prefixLen = size(keyPart) - 1 // const prefixLen = size(keyPart) - 1
const len = size(keyPart) const len = size(keyPart)
@ -1010,7 +1061,7 @@ const useConnectionStore = defineStore('connections', {
* @param {string} key * @param {string} key
* @private * @private
*/ */
_removeKey(connName, db, key) { _deleteKeyNode(connName, db, key) {
const dbs = this.databases[connName] const dbs = this.databases[connName]
const dbDetail = get(dbs, db, {}) const dbDetail = get(dbs, db, {})
@ -1078,18 +1129,18 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* remove redis key * delete redis key
* @param {string} connName * @param {string} connName
* @param {number} db * @param {number} db
* @param {string} key * @param {string} key
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
async removeKey(connName, db, key) { async deleteKey(connName, db, key) {
try { try {
const { data, success, msg } = await RemoveKey(connName, db, key) const { data, success, msg } = await DeleteKey(connName, db, [key])
if (success) { if (success) {
// update tree view data // update tree view data
this._removeKey(connName, db, key) this._deleteKeyNode(connName, db, key)
// set tab content empty // set tab content empty
const tab = useTabStore() const tab = useTabStore()
@ -1101,6 +1152,32 @@ const useConnectionStore = defineStore('connections', {
return false return false
}, },
/**
* delete keys with prefix
* @param connName
* @param db
* @param prefix
* @param keys
* @returns {Promise<boolean>}
*/
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 * rename key
* @param {string} connName * @param {string} connName
@ -1113,7 +1190,7 @@ const useConnectionStore = defineStore('connections', {
const { success = false, msg } = await RenameKey(connName, db, key, newKey) const { success = false, msg } = await RenameKey(connName, db, key, newKey)
if (success) { if (success) {
// delete old key and add new key struct // delete old key and add new key struct
this._removeKey(connName, db, key) this._deleteKeyNode(connName, db, key)
this._addKey(connName, db, newKey) this._addKey(connName, db, newKey)
return { success: true } return { success: true }
} else { } else {

View File

@ -36,6 +36,13 @@ const useDialogStore = defineStore('dialog', {
}, },
renameDialogVisible: false, renameDialogVisible: false,
deleteKeyParam: {
server: '',
db: 0,
key: '',
},
deleteKeyDialogVisible: false,
selectTTL: -1, selectTTL: -1,
ttlDialogVisible: false, ttlDialogVisible: false,
@ -92,6 +99,16 @@ const useDialogStore = defineStore('dialog', {
this.renameDialogVisible = false 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 * @param {string} prefix