mirror of
				https://github.com/tiny-craft/tiny-rdm.git
				synced 2025-11-01 02:52:33 +08:00 
			
		
		
		
	feat: add multiple check mode for keys
perf: add multiple keys deletion and progress indicator
This commit is contained in:
		| @@ -1,9 +1,10 @@ | ||||
| <script setup> | ||||
| import { reactive, ref, watch } from 'vue' | ||||
| import { computed, reactive, ref, watch } from 'vue' | ||||
| import useDialog from 'stores/dialog' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { isEmpty, size } from 'lodash' | ||||
| import { isEmpty, map, size } from 'lodash' | ||||
| import useBrowserStore from 'stores/browser.js' | ||||
| import { decodeRedisKey } from '@/utils/key_convert.js' | ||||
|  | ||||
| const deleteForm = reactive({ | ||||
|     server: '', | ||||
| @@ -25,16 +26,23 @@ watch( | ||||
|             deleteForm.server = server | ||||
|             deleteForm.db = db | ||||
|             deleteForm.key = key | ||||
|             deleteForm.showAffected = false | ||||
|             deleteForm.loadingAffected = false | ||||
|             deleteForm.affectedKeys = [] | ||||
|             deleteForm.async = true | ||||
|             // deleteForm.async = true | ||||
|             loading.value = false | ||||
|             deleting.value = false | ||||
|             if (key instanceof Array) { | ||||
|                 deleteForm.showAffected = true | ||||
|                 deleteForm.affectedKeys = key | ||||
|             } else { | ||||
|                 deleteForm.showAffected = false | ||||
|                 deleteForm.affectedKeys = [] | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| const loading = ref(false) | ||||
| const deleting = ref(false) | ||||
| const scanAffectedKey = async () => { | ||||
|     try { | ||||
|         loading.value = true | ||||
| @@ -53,20 +61,21 @@ const resetAffected = () => { | ||||
|     deleteForm.affectedKeys = [] | ||||
| } | ||||
|  | ||||
| const logLines = computed(() => { | ||||
|     return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k)) | ||||
| }) | ||||
|  | ||||
| const i18n = useI18n() | ||||
| const onConfirmDelete = async () => { | ||||
|     try { | ||||
|         loading.value = true | ||||
|         const { server, db, key, async } = deleteForm | ||||
|         const success = await browserStore.deleteKeyPrefix(server, db, key, async) | ||||
|         if (success) { | ||||
|             $message.success(i18n.t('dialogue.handle_succ')) | ||||
|         } | ||||
|         deleting.value = true | ||||
|         const { server, db, key, affectedKeys } = deleteForm | ||||
|         browserStore.deleteKeys(server, db, affectedKeys).catch((e) => {}) | ||||
|     } catch (e) { | ||||
|         $message.error(e.message) | ||||
|         return | ||||
|     } finally { | ||||
|         loading.value = false | ||||
|         deleting.value = false | ||||
|     } | ||||
|     dialogStore.closeDeleteKeyDialog() | ||||
| } | ||||
| @@ -88,29 +97,35 @@ const onClose = () => { | ||||
|         transform-origin="center"> | ||||
|         <n-spin :show="loading"> | ||||
|             <n-form :model="deleteForm" :show-require-mark="false" label-placement="top"> | ||||
|                 <n-form-item :label="$t('dialogue.key.server')"> | ||||
|                     <n-input :value="deleteForm.server" readonly /> | ||||
|                 </n-form-item> | ||||
|                 <n-form-item :label="$t('dialogue.key.db_index')"> | ||||
|                     <n-input :value="deleteForm.db.toString()" readonly /> | ||||
|                 </n-form-item> | ||||
|                 <n-form-item :label="$t('dialogue.key.key_expression')" required> | ||||
|                 <n-grid :x-gap="10"> | ||||
|                     <n-form-item-gi :label="$t('dialogue.key.server')" :span="12"> | ||||
|                         <n-input :value="deleteForm.server" readonly /> | ||||
|                     </n-form-item-gi> | ||||
|                     <n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12"> | ||||
|                         <n-input :value="deleteForm.db.toString()" readonly /> | ||||
|                     </n-form-item-gi> | ||||
|                 </n-grid> | ||||
|                 <n-form-item | ||||
|                     v-if="!(deleteForm.key instanceof Array)" | ||||
|                     :label="$t('dialogue.key.key_expression')" | ||||
|                     required> | ||||
|                     <n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" /> | ||||
|                 </n-form-item> | ||||
|                 <n-form-item :label="$t('dialogue.key.async_delete')" required> | ||||
|                     <n-checkbox v-model:checked="deleteForm.async"> | ||||
|                         {{ $t('dialogue.key.async_delete_title') }} | ||||
|                     </n-checkbox> | ||||
|                 </n-form-item> | ||||
|                 <!--                <n-form-item :label="$t('dialogue.key.async_delete')" required>--> | ||||
|                 <!--                    <n-checkbox v-model:checked="deleteForm.async">--> | ||||
|                 <!--                        {{ $t('dialogue.key.async_delete_title') }}--> | ||||
|                 <!--                    </n-checkbox>--> | ||||
|                 <!--                </n-form-item>--> | ||||
|                 <n-card | ||||
|                     v-if="deleteForm.showAffected" | ||||
|                     :title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`" | ||||
|                     embedded | ||||
|                     size="small"> | ||||
|                     <n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text /> | ||||
|                     <n-log | ||||
|                         v-else | ||||
|                         :line-height="1.5" | ||||
|                         :lines="deleteForm.affectedKeys" | ||||
|                         :lines="logLines" | ||||
|                         :rows="10" | ||||
|                         style="user-select: text; cursor: text" /> | ||||
|                 </n-card> | ||||
|   | ||||
							
								
								
									
										63
									
								
								frontend/src/components/icons/ListCheckbox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								frontend/src/components/icons/ListCheckbox.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| <script setup> | ||||
| const props = defineProps({ | ||||
|     strokeWidth: { | ||||
|         type: [Number, String], | ||||
|         default: 3, | ||||
|     }, | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"> | ||||
|         <path clip-rule="evenodd" d="M20 24H44H20Z" fill="none" fill-rule="evenodd" /> | ||||
|         <path | ||||
|             :stroke-width="props.strokeWidth" | ||||
|             d="M20 24H44" | ||||
|             stroke="currentColor" | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" /> | ||||
|         <path clip-rule="evenodd" d="M20 38H44H20Z" fill="none" fill-rule="evenodd" /> | ||||
|         <path | ||||
|             :stroke-width="props.strokeWidth" | ||||
|             d="M20 38H44" | ||||
|             stroke="currentColor" | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" /> | ||||
|         <path clip-rule="evenodd" d="M20 10H44H20Z" fill="none" fill-rule="evenodd" /> | ||||
|         <path | ||||
|             :stroke-width="props.strokeWidth" | ||||
|             d="M20 10H44" | ||||
|             stroke="currentColor" | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" /> | ||||
|         <rect | ||||
|             :stroke-width="props.strokeWidth" | ||||
|             fill="none" | ||||
|             height="8" | ||||
|             stroke="currentColor" | ||||
|             stroke-linejoin="round" | ||||
|             width="8" | ||||
|             x="4" | ||||
|             y="34" /> | ||||
|         <rect | ||||
|             :stroke-width="props.strokeWidth" | ||||
|             fill="none" | ||||
|             height="8" | ||||
|             stroke="currentColor" | ||||
|             stroke-linejoin="round" | ||||
|             width="8" | ||||
|             x="4" | ||||
|             y="20" /> | ||||
|         <rect | ||||
|             :stroke-width="props.strokeWidth" | ||||
|             fill="none" | ||||
|             height="8" | ||||
|             stroke="currentColor" | ||||
|             stroke-linejoin="round" | ||||
|             width="8" | ||||
|             x="4" | ||||
|             y="6" /> | ||||
|     </svg> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss" scoped></style> | ||||
| @@ -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()) | ||||
|         <!-- tree view --> | ||||
|         <browser-tree | ||||
|             ref="browserTreeRef" | ||||
|             v-model:checked-count="checkedCount" | ||||
|             :check-mode="inCheckState" | ||||
|             :db="browserStore.openedDB[currentName]" | ||||
|             :full-loaded="fullyLoaded" | ||||
|             :loading="loading && loadProgress <= 0" | ||||
|             :pattern="filterForm.filter" | ||||
| @@ -245,50 +285,70 @@ onMounted(() => onReload()) | ||||
|             <!--                :stroke-width="3.5"--> | ||||
|             <!--                unselect-stroke-width="3"--> | ||||
|             <!--                @update:value="onSwitchView" />--> | ||||
|             <div class="flex-box-h nav-pane-func"> | ||||
|                 <n-select | ||||
|                     v-model:value="browserStore.openedDB[currentName]" | ||||
|                     :consistent-menu-width="false" | ||||
|                     :filter="(pattern, option) => option.value.toString() === pattern" | ||||
|                     :options="dbSelectOptions" | ||||
|                     filterable | ||||
|                     size="small" | ||||
|                     style="min-width: 100px; max-width: 200px" | ||||
|                     @update:value="handleSelectDB" /> | ||||
|                 <icon-button | ||||
|                     :button-class="['nav-pane-func-btn']" | ||||
|                     :disabled="fullyLoaded" | ||||
|                     :icon="LoadList" | ||||
|                     :loading="loading" | ||||
|                     :stroke-width="3.5" | ||||
|                     size="20" | ||||
|                     t-tooltip="interface.load_more" | ||||
|                     @click="onLoadMore" /> | ||||
|                 <icon-button | ||||
|                     :button-class="['nav-pane-func-btn']" | ||||
|                     :disabled="fullyLoaded" | ||||
|                     :icon="LoadAll" | ||||
|                     :loading="loading" | ||||
|                     :stroke-width="3.5" | ||||
|                     size="20" | ||||
|                     t-tooltip="interface.load_all" | ||||
|                     @click="onLoadAll" /> | ||||
|                 <div class="flex-item-expand" style="min-width: 10px" /> | ||||
|                 <icon-button | ||||
|                     :button-class="['nav-pane-func-btn']" | ||||
|                     :icon="Delete" | ||||
|                     :stroke-width="3.5" | ||||
|                     size="20" | ||||
|                     t-tooltip="interface.flush_db" | ||||
|                     @click="onFlush" /> | ||||
|                 <icon-button | ||||
|                     :button-class="['nav-pane-func-btn']" | ||||
|                     :icon="Unlink" | ||||
|                     :stroke-width="3.5" | ||||
|                     size="20" | ||||
|                     t-tooltip="interface.disconnect" | ||||
|                     @click="onDisconnect" /> | ||||
|             </div> | ||||
|             <transition mode="out-in" name="fade"> | ||||
|                 <div v-if="!inCheckState" class="flex-box-h nav-pane-func"> | ||||
|                     <n-select | ||||
|                         v-model:value="browserStore.openedDB[currentName]" | ||||
|                         :consistent-menu-width="false" | ||||
|                         :filter="(pattern, option) => option.value.toString() === pattern" | ||||
|                         :options="dbSelectOptions" | ||||
|                         filterable | ||||
|                         size="small" | ||||
|                         style="min-width: 100px; max-width: 200px" | ||||
|                         @update:value="handleSelectDB" /> | ||||
|                     <icon-button | ||||
|                         :button-class="['nav-pane-func-btn']" | ||||
|                         :disabled="fullyLoaded" | ||||
|                         :icon="LoadList" | ||||
|                         :loading="loading" | ||||
|                         :stroke-width="3.5" | ||||
|                         size="20" | ||||
|                         t-tooltip="interface.load_more" | ||||
|                         @click="onLoadMore" /> | ||||
|                     <icon-button | ||||
|                         :button-class="['nav-pane-func-btn']" | ||||
|                         :disabled="fullyLoaded" | ||||
|                         :icon="LoadAll" | ||||
|                         :loading="loading" | ||||
|                         :stroke-width="3.5" | ||||
|                         size="20" | ||||
|                         t-tooltip="interface.load_all" | ||||
|                         @click="onLoadAll" /> | ||||
|                     <div class="flex-item-expand" style="min-width: 10px" /> | ||||
|                     <icon-button | ||||
|                         :button-class="['nav-pane-func-btn']" | ||||
|                         :icon="ListCheckbox" | ||||
|                         :stroke-width="3.5" | ||||
|                         size="20" | ||||
|                         t-tooltip="interface.check_mode" | ||||
|                         @click="inCheckState = true" /> | ||||
|                     <n-dropdown :options="moreOptions" placement="top-end" @select="onSelectOptions"> | ||||
|                         <icon-button :button-class="['nav-pane-func-btn']" :icon="More" :stroke-width="3.5" size="20" /> | ||||
|                     </n-dropdown> | ||||
|                 </div> | ||||
|  | ||||
|                 <!-- check mode function bar --> | ||||
|                 <div v-else class="flex-box-h nav-pane-func"> | ||||
|                     <icon-button | ||||
|                         :button-class="['nav-pane-func-btn']" | ||||
|                         :disabled="checkedCount <= 0" | ||||
|                         :icon="Delete" | ||||
|                         :stroke-width="3.5" | ||||
|                         size="20" | ||||
|                         t-tooltip="interface.delete_checked" | ||||
|                         @click="onDeleteChecked" /> | ||||
|                     <div class="flex-item-expand ellipsis" style="text-align: center; margin: 0 5px"> | ||||
|                         <n-text>{{ checkedTip }}</n-text> | ||||
|                     </div> | ||||
|                     <icon-button | ||||
|                         :button-class="['nav-pane-func-btn']" | ||||
|                         :icon="Close" | ||||
|                         :stroke-width="3.5" | ||||
|                         size="20" | ||||
|                         t-tooltip="interface.quit_check_mode" | ||||
|                         @click="inCheckState = false" /> | ||||
|                 </div> | ||||
|             </transition> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| <script setup> | ||||
| import { computed, h, nextTick, reactive, ref } from 'vue' | ||||
| import { computed, h, nextTick, reactive, ref, watchEffect } from 'vue' | ||||
| import { ConnectionType } from '@/consts/connection_type.js' | ||||
| import { NIcon, NSpace, useThemeVars } from 'naive-ui' | ||||
| import Key from '@/components/icons/Key.vue' | ||||
| import Binary from '@/components/icons/Binary.vue' | ||||
| import Database from '@/components/icons/Database.vue' | ||||
| import { find, get, includes, indexOf, isEmpty, remove, size, startsWith } from 'lodash' | ||||
| import { filter, find, get, includes, indexOf, isEmpty, map, remove, size, startsWith } from 'lodash' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import Refresh from '@/components/icons/Refresh.vue' | ||||
| import CopyLink from '@/components/icons/CopyLink.vue' | ||||
| @@ -26,21 +26,40 @@ import { useRender } from '@/utils/render.js' | ||||
|  | ||||
| const props = defineProps({ | ||||
|     server: String, | ||||
|     db: Number, | ||||
|     keyView: String, | ||||
|     loading: Boolean, | ||||
|     pattern: String, | ||||
|     fullLoaded: Boolean, | ||||
|     checkMode: Boolean, | ||||
|     checkedCount: Number, | ||||
| }) | ||||
|  | ||||
| const emit = defineEmits(['update:checked-count']) | ||||
|  | ||||
| const themeVars = useThemeVars() | ||||
| const render = useRender() | ||||
| const i18n = useI18n() | ||||
| const expandedKeys = ref([props.server]) | ||||
| const expandedKeys = ref([]) | ||||
| const checkedKeys = reactive({ | ||||
|     keys: [], | ||||
|     redisKeys: [], | ||||
| }) | ||||
| const connectionStore = useConnectionStore() | ||||
| const browserStore = useBrowserStore() | ||||
| const tabStore = useTabStore() | ||||
| const dialogStore = useDialogStore() | ||||
|  | ||||
| watchEffect( | ||||
|     () => { | ||||
|         if (!props.checkMode) { | ||||
|             resetCheckedKey() | ||||
|         } | ||||
|         emit('update:checked-count', size(checkedKeys.keys)) | ||||
|     }, | ||||
|     { flush: 'post' }, | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @type {ComputedRef<string[]>} | ||||
| @@ -48,9 +67,9 @@ const dialogStore = useDialogStore() | ||||
| const selectedKeys = computed(() => { | ||||
|     const tab = find(tabStore.tabList, { name: props.server }) | ||||
|     if (tab != null) { | ||||
|         return get(tab, 'selectedKeys', [props.server]) | ||||
|         return get(tab, 'selectedKeys', []) | ||||
|     } | ||||
|     return [props.server] | ||||
|     return [] | ||||
| }) | ||||
|  | ||||
| const data = computed(() => { | ||||
| @@ -170,6 +189,11 @@ const resetExpandKey = (server, db, includeDB) => { | ||||
|     }) | ||||
| } | ||||
|  | ||||
| const resetCheckedKey = () => { | ||||
|     checkedKeys.keys = [] | ||||
|     checkedKeys.redisKeys = [] | ||||
| } | ||||
|  | ||||
| const handleSelectContextMenu = (key) => { | ||||
|     contextMenuParam.show = false | ||||
|     const selectedKey = get(selectedKeys.value, 0) | ||||
| @@ -247,6 +271,32 @@ const handleSelectContextMenu = (key) => { | ||||
|     } | ||||
| } | ||||
|  | ||||
| const onUpdateSelectedKeys = (keys, options) => { | ||||
|     try { | ||||
|         if (!isEmpty(options)) { | ||||
|             // prevent load duplicate key | ||||
|             for (const node of options) { | ||||
|                 if (node.type === ConnectionType.RedisValue) { | ||||
|                     const { key, db } = node | ||||
|                     const redisKey = node.redisKeyCode || node.redisKey | ||||
|                     if (!includes(selectedKeys.value, key)) { | ||||
|                         browserStore.loadKeySummary({ | ||||
|                             server: props.server, | ||||
|                             db, | ||||
|                             key: redisKey, | ||||
|                         }) | ||||
|                     } | ||||
|                     return | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // default is load blank key to display server status | ||||
|         tabStore.openBlank(props.server) | ||||
|     } finally { | ||||
|         tabStore.setSelectedKeys(props.server, keys) | ||||
|     } | ||||
| } | ||||
|  | ||||
| const onUpdateExpanded = (value, option, meta) => { | ||||
|     expandedKeys.value = value | ||||
|     if (!meta.node) { | ||||
| @@ -273,30 +323,17 @@ const onUpdateExpanded = (value, option, meta) => { | ||||
|     } | ||||
| } | ||||
|  | ||||
| const onUpdateSelectedKeys = (keys, options) => { | ||||
|     try { | ||||
|         if (!isEmpty(options)) { | ||||
|             // prevent load duplicate key | ||||
|             for (const node of options) { | ||||
|                 if (node.type === ConnectionType.RedisValue) { | ||||
|                     const { key, db } = node | ||||
|                     const redisKey = node.redisKeyCode || node.redisKey | ||||
|                     if (!includes(selectedKeys.value, key)) { | ||||
|                         browserStore.loadKeySummary({ | ||||
|                             server: props.server, | ||||
|                             db, | ||||
|                             key: redisKey, | ||||
|                         }) | ||||
|                     } | ||||
|                     return | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // default is load blank key to display server status | ||||
|         tabStore.openBlank(props.server) | ||||
|     } finally { | ||||
|         tabStore.setSelectedKeys(props.server, keys) | ||||
|     } | ||||
| /** | ||||
|  * | ||||
|  * @param {string[]} keys | ||||
|  * @param {TreeOption[]} options | ||||
|  */ | ||||
| const onUpdateCheckedKeys = (keys, options) => { | ||||
|     checkedKeys.keys = keys | ||||
|     checkedKeys.redisKeys = map( | ||||
|         filter(options, (o) => o.type === ConnectionType.RedisValue), | ||||
|         (o) => o.redisKeyCode || o.redisKey, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| const renderPrefix = ({ option }) => { | ||||
| @@ -449,8 +486,10 @@ const nodeProps = ({ option }) => { | ||||
|                 console.warn('TODO: alert to ignore double click when loading') | ||||
|                 return | ||||
|             } | ||||
|             // default handle is expand current node | ||||
|             nextTick().then(() => expandKey(option.key)) | ||||
|             if (!props.checkMode) { | ||||
|                 // default handle is expand current node | ||||
|                 nextTick().then(() => expandKey(option.key)) | ||||
|             } | ||||
|         }, | ||||
|         onContextmenu(e) { | ||||
|             e.preventDefault() | ||||
| @@ -484,6 +523,13 @@ defineExpose({ | ||||
|     resetExpandKey, | ||||
|     refreshTree: () => { | ||||
|         treeKey.value = Date.now() | ||||
|         expandedKeys.value = [] | ||||
|         resetCheckedKey() | ||||
|     }, | ||||
|     deleteCheckedItems: () => { | ||||
|         if (!isEmpty(checkedKeys.redisKeys)) { | ||||
|             dialogStore.openDeleteKeyDialog(props.server, props.db, checkedKeys.redisKeys) | ||||
|         } | ||||
|     }, | ||||
| }) | ||||
| </script> | ||||
| @@ -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" /> | ||||
|         <n-dropdown | ||||
|             :options="contextMenuParam.options" | ||||
|             :render-label="renderContextLabel" | ||||
|   | ||||
| @@ -69,6 +69,9 @@ | ||||
|     "delete_key": "Delete Key", | ||||
|     "batch_delete_key": "Batch Delete Keys", | ||||
|     "flush_db": "Flush Database", | ||||
|     "check_mode": "Check Mode", | ||||
|     "quit_check_mode": "Quit Check Mode", | ||||
|     "delete_checked": "Delete Checked Items", | ||||
|     "copy_value": "Copy Value", | ||||
|     "edit_value": "Edit Value", | ||||
|     "save_update": "Save Update", | ||||
| @@ -132,6 +135,8 @@ | ||||
|     "remove_tip": "{type} \"{name}\" will be deleted", | ||||
|     "remove_group_tip": "Group \"{name}\" and all connections in it will be deleted", | ||||
|     "delete_key_succ": "\"{key}\" has been deleted", | ||||
|     "deleting_key": "Deleting key: {key} ({index}/{count})", | ||||
|     "delete_completed": "Deletion process has been completed, {success} successed, {fail} failed", | ||||
|     "rename_binary_key_fail": "Rename binary key name is unsupported", | ||||
|     "handle_succ": "Success!", | ||||
|     "reload_succ": "Reloaded!", | ||||
|   | ||||
| @@ -69,6 +69,9 @@ | ||||
|     "delete_key": "删除键", | ||||
|     "batch_delete_key": "批量删除键", | ||||
|     "flush_db": "清空数据库", | ||||
|     "check_mode": "勾选模式", | ||||
|     "quit_check_mode": "退出勾选模式", | ||||
|     "delete_checked": "删除勾选项", | ||||
|     "copy_value": "复制值", | ||||
|     "edit_value": "修改值", | ||||
|     "save_update": "保存修改", | ||||
| @@ -132,6 +135,8 @@ | ||||
|     "remove_tip": "{type} \"{name}\" 将会被删除", | ||||
|     "remove_group_tip": "分组 \"{name}\"及其所有连接将会被删除", | ||||
|     "delete_key_succ": "{key} 已被删除", | ||||
|     "deleting_key": "正在删除键:{key} ({index}/{count})", | ||||
|     "delete_completed": "已完成删除操作,成功{success}个,失败{fail}个", | ||||
|     "rename_binary_key_fail": "不支持重命名二进制键名", | ||||
|     "handle_succ": "操作成功", | ||||
|     "reload_succ": "已重新载入", | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import { | ||||
|     CloseConnection, | ||||
|     ConvertValue, | ||||
|     DeleteKey, | ||||
|     DeleteOneKey, | ||||
|     FlushDB, | ||||
|     GetCmdHistory, | ||||
|     GetKeyDetail, | ||||
| @@ -53,6 +54,7 @@ import { ConnectionType } from '@/consts/connection_type.js' | ||||
| import useConnectionStore from 'stores/connections.js' | ||||
| import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' | ||||
| import { isRedisGlob } from '@/utils/glob_pattern.js' | ||||
| import { i18nGlobal } from '@/utils/i18n.js' | ||||
|  | ||||
| const useBrowserStore = defineStore('browser', { | ||||
|     /** | ||||
| @@ -1841,43 +1843,46 @@ const useBrowserStore = defineStore('browser', { | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * delete keys with prefix | ||||
|          * @param {string} connName | ||||
|          * delete multiple keys | ||||
|          * @param {string} server | ||||
|          * @param {number} db | ||||
|          * @param {string} prefix | ||||
|          * @param {boolean} async | ||||
|          * @returns {Promise<boolean>} | ||||
|          * @param {string[]|number[][]} keys | ||||
|          * @return {Promise<void>} | ||||
|          */ | ||||
|         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 })) | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Lykin
					Lykin