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()) - @@ -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 {