diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go
index 088ca2d..907cbd6 100644
--- a/backend/services/browser_service.go
+++ b/backend/services/browser_service.go
@@ -291,9 +291,7 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
if db >= 0 {
var rdb *redis.Client
if rdb, ok = client.(*redis.Client); ok && rdb != nil {
- if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil {
- return
- }
+ _ = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err()
}
}
return
@@ -1772,8 +1770,8 @@ func (b *browserService) SetKeyTTL(connName string, db int, k any, ttl int64) (r
}
// DeleteKey remove redis key
-func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (resp types.JSResp) {
- item, err := b.getRedisClient(connName, db)
+func (b *browserService) DeleteKey(server string, db int, k any, async bool) (resp types.JSResp) {
+ item, err := b.getRedisClient(server, db)
if err != nil {
resp.Msg = err.Error()
return
@@ -1862,6 +1860,34 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
return
}
+// DeleteOneKey delete one key
+func (b *browserService) DeleteOneKey(server string, db int, k any) (resp types.JSResp) {
+ item, err := b.getRedisClient(server, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ client, ctx := item.client, item.ctx
+ key := strutil.DecodeRedisKey(k)
+ if cluster, ok := client.(*redis.ClusterClient); ok {
+ // cluster mode
+ err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
+ return cli.Del(ctx, key).Err()
+ })
+ } else {
+ err = client.Del(ctx, key).Err()
+ }
+
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ return
+}
+
// FlushDB flush database
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db)
diff --git a/frontend/src/components/dialogs/DeleteKeyDialog.vue b/frontend/src/components/dialogs/DeleteKeyDialog.vue
index aef5813..76296a6 100644
--- a/frontend/src/components/dialogs/DeleteKeyDialog.vue
+++ b/frontend/src/components/dialogs/DeleteKeyDialog.vue
@@ -1,9 +1,10 @@
+
+
+
+
+
+
diff --git a/frontend/src/components/sidebar/BrowserPane.vue b/frontend/src/components/sidebar/BrowserPane.vue
index 7f75212..a8f97d8 100644
--- a/frontend/src/components/sidebar/BrowserPane.vue
+++ b/frontend/src/components/sidebar/BrowserPane.vue
@@ -4,7 +4,7 @@ import BrowserTree from './BrowserTree.vue'
import IconButton from '@/components/common/IconButton.vue'
import useTabStore from 'stores/tab.js'
import { computed, nextTick, onMounted, reactive, ref, unref, watch } from 'vue'
-import { get, map } from 'lodash'
+import { find, get, map } from 'lodash'
import Refresh from '@/components/icons/Refresh.vue'
import useDialogStore from 'stores/dialog.js'
import { useI18n } from 'vue-i18n'
@@ -20,6 +20,9 @@ import RedisTypeSelector from '@/components/common/RedisTypeSelector.vue'
import { types } from '@/consts/support_redis_type.js'
import Plus from '@/components/icons/Plus.vue'
import useConnectionStore from 'stores/connections.js'
+import ListCheckbox from '@/components/icons/ListCheckbox.vue'
+import Close from '@/components/icons/Close.vue'
+import More from '@/components/icons/More.vue'
const themeVars = useThemeVars()
const i18n = useI18n()
@@ -33,6 +36,8 @@ const currentName = computed(() => get(tabStore.currentTab, 'name', ''))
const browserTreeRef = ref(null)
const loading = ref(false)
const fullyLoaded = ref(false)
+const inCheckState = ref(false)
+const checkedCount = ref(0)
const selectedDB = computed(() => {
return browserStore.selectedDatabases[currentName.value] || 0
@@ -54,6 +59,17 @@ const dbSelectOptions = computed(() => {
})
})
+const moreOptions = computed(() => {
+ return [
+ { key: 'flush', label: i18n.t('interface.flush_db'), icon: render.renderIcon(Delete, { strokeWidth: 3.5 }) },
+ {
+ key: 'disconnect',
+ label: i18n.t('interface.disconnect'),
+ icon: render.renderIcon(Unlink, { strokeWidth: 3.5 }),
+ },
+ ]
+})
+
const loadProgress = computed(() => {
const db = browserStore.getDatabase(currentName.value, selectedDB.value)
if (db.maxKeys <= 0) {
@@ -62,6 +78,12 @@ const loadProgress = computed(() => {
return (db.keys * 100) / Math.max(db.keys, db.maxKeys)
})
+const checkedTip = computed(() => {
+ const dblist = browserStore.getDBList(currentName.value)
+ const db = find(dblist, { db: selectedDB.value })
+ return `${checkedCount.value} / ${db.maxKeys}`
+})
+
const onReload = async () => {
try {
loading.value = true
@@ -115,6 +137,10 @@ const onLoadAll = async () => {
}
}
+const onDeleteChecked = () => {
+ browserTreeRef.value?.deleteCheckedItems()
+}
+
const onFlush = () => {
dialogStore.openFlushDBDialog(currentName.value, selectedDB.value)
}
@@ -146,6 +172,17 @@ const onMatchInput = (matchVal, filterVal) => {
onReload()
}
+const onSelectOptions = (select) => {
+ switch (select) {
+ case 'flush':
+ onFlush()
+ break
+ case 'disconnect':
+ onDisconnect()
+ break
+ }
+}
+
watch(
() => browserStore.openedDB[currentName.value],
async (db, prevDB) => {
@@ -158,7 +195,7 @@ watch(
browserStore.closeDatabase(currentName.value, prevDB)
browserStore.setKeyFilter(currentName.value, {})
await browserStore.openDatabase(currentName.value, db)
- browserTreeRef.value?.resetExpandKey(currentName.value, db)
+ // browserTreeRef.value?.resetExpandKey(currentName.value, db)
fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, db)
browserTreeRef.value?.refreshTree()
@@ -232,6 +269,9 @@ onMounted(() => onReload())
onReload())
-
+
+
+
+
+
+
+
+ {{ checkedTip }}
+
+
+
+
@@ -299,17 +359,29 @@ onMounted(() => onReload())
:deep(.toggle-btn) {
border-style: solid;
border-width: 1px;
+ border-radius: 3px;
+ padding: 4px;
}
-:deep(.filter-on) {
+:deep(.toggle-on) {
border-color: v-bind('themeVars.iconColorDisabled');
background-color: v-bind('themeVars.iconColorDisabled');
}
-:deep(.filter-off) {
+:deep(.toggle-off) {
border-color: #0000;
}
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.3s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
.nav-pane-top {
//@include bottom-shadow(0.1);
color: v-bind('themeVars.iconColor');
diff --git a/frontend/src/components/sidebar/BrowserTree.vue b/frontend/src/components/sidebar/BrowserTree.vue
index b3cd532..e0abede 100644
--- a/frontend/src/components/sidebar/BrowserTree.vue
+++ b/frontend/src/components/sidebar/BrowserTree.vue
@@ -1,11 +1,11 @@
@@ -499,6 +545,9 @@ defineExpose({
:block-line="true"
:block-node="true"
:cancelable="false"
+ :cascade="true"
+ :checkable="props.checkMode"
+ :checked-keys="checkedKeys.keys"
:data="data"
:expand-on-click="false"
:expanded-keys="expandedKeys"
@@ -510,10 +559,12 @@ defineExpose({
:render-suffix="renderSuffix"
:selected-keys="selectedKeys"
:show-irrelevant-nodes="false"
+ check-strategy="child"
class="fill-height"
virtual-scroll
@update:selected-keys="onUpdateSelectedKeys"
- @update:expanded-keys="onUpdateExpanded" />
+ @update:expanded-keys="onUpdateExpanded"
+ @update:checked-keys="onUpdateCheckedKeys" />
}
+ * @param {string[]|number[][]} keys
+ * @return {Promise}
*/
- async deleteKeyPrefix(connName, db, prefix, async) {
- if (isEmpty(prefix)) {
- return false
- }
- try {
- if (!endsWith(prefix, '*')) {
- prefix += '*'
- }
- const { data, success, msg } = await DeleteKey(connName, db, prefix, async)
+ async deleteKeys(server, db, keys) {
+ const delMsgRef = $message.loading('', { duration: 0 })
+ let progress = 0
+ let count = size(keys)
+ let deletedCount = 0,
+ failCount = 0
+ for (const key of keys) {
+ delMsgRef.content = i18nGlobal.t('dialogue.deleting_key', {
+ key: decodeRedisKey(key),
+ index: ++progress,
+ count,
+ })
+ const { success } = await DeleteOneKey(server, db, key)
if (success) {
- // const { deleted: keys = [] } = data
- // for (const key of keys) {
- // await this._deleteKeyNode(connName, db, key)
- // }
- const deleteCount = get(data, 'deleteCount', 0)
- const separator = this._getSeparator(connName)
- if (endsWith(prefix, '*')) {
- prefix = prefix.substring(0, prefix.length - 1)
- }
- if (endsWith(prefix, separator)) {
- prefix = prefix.substring(0, prefix.length - 1)
- }
- this._deleteKeyNode(connName, db, prefix, true)
- this._tidyNode(connName, db, prefix, true)
- this._updateDBMaxKeys(connName, db, -deleteCount)
- return true
+ this._deleteKeyNode(server, db, key, false)
+ deletedCount += 1
+ } else {
+ failCount += 1
}
- } finally {
}
- return false
+ delMsgRef.destroy()
+ // refresh model data
+ this._tidyNode(server, db, '', true)
+ this._updateDBMaxKeys(server, db, -deletedCount)
+ if (failCount <= 0) {
+ // no fail
+ $message.success(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount }))
+ } else if (failCount >= deletedCount) {
+ // all fail
+ $message.error(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount }))
+ } else {
+ // some fail
+ $message.warn(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount }))
+ }
},
/**
diff --git a/frontend/src/stores/dialog.js b/frontend/src/stores/dialog.js
index e04310d..0b94e54 100644
--- a/frontend/src/stores/dialog.js
+++ b/frontend/src/stores/dialog.js
@@ -1,5 +1,5 @@
-import { defineStore } from 'pinia'
-import useConnectionStore from './connections.js'
+import { defineStore } from "pinia";
+import useConnectionStore from "./connections.js";
/**
* connection dialog type
@@ -160,6 +160,12 @@ const useDialogStore = defineStore('dialog', {
this.renameDialogVisible = false
},
+ /**
+ *
+ * @param {string} server
+ * @param {number} db
+ * @param {string | string[]} key
+ */
openDeleteKeyDialog(server, db, key) {
this.deleteKeyParam.server = server
this.deleteKeyParam.db = db
diff --git a/frontend/src/stores/tab.js b/frontend/src/stores/tab.js
index 37cdeb5..14b37ef 100644
--- a/frontend/src/stores/tab.js
+++ b/frontend/src/stores/tab.js
@@ -10,6 +10,7 @@ const useTabStore = defineStore('tab', {
* @property {string} [title] tab title
* @property {string} [icon] tab icon
* @property {string[]} selectedKeys
+ * @property {string[]} checkdeKeys
* @property {string} [type] key type
* @property {*} [value] key value
* @property {string} [server] server name
@@ -638,7 +639,7 @@ const useTabStore = defineStore('tab', {
},
/**
- * set selected keys of current display browser tree
+ * set selected keys in current display browser tree
* @param {string} server
* @param {string|string[]} [keys]
*/
@@ -647,7 +648,7 @@ const useTabStore = defineStore('tab', {
if (tab != null) {
if (keys == null) {
// select nothing
- tab.selectedKeys = [server]
+ tab.selectedKeys = []
} else if (typeof keys === 'string') {
tab.selectedKeys = [keys]
} else {