feat: add multiple check mode for keys
perf: add multiple keys deletion and progress indicator
This commit is contained in:
parent
7c4f0197ba
commit
8573d24a47
|
@ -291,9 +291,7 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
|
||||||
if db >= 0 {
|
if db >= 0 {
|
||||||
var rdb *redis.Client
|
var rdb *redis.Client
|
||||||
if rdb, ok = client.(*redis.Client); ok && rdb != nil {
|
if rdb, ok = client.(*redis.Client); ok && rdb != nil {
|
||||||
if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil {
|
_ = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err()
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -1772,8 +1770,8 @@ func (b *browserService) SetKeyTTL(connName string, db int, k any, ttl int64) (r
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteKey remove redis key
|
// DeleteKey remove redis key
|
||||||
func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (resp types.JSResp) {
|
func (b *browserService) DeleteKey(server string, db int, k any, async bool) (resp types.JSResp) {
|
||||||
item, err := b.getRedisClient(connName, db)
|
item, err := b.getRedisClient(server, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Msg = err.Error()
|
resp.Msg = err.Error()
|
||||||
return
|
return
|
||||||
|
@ -1862,6 +1860,34 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
|
||||||
return
|
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
|
// FlushDB flush database
|
||||||
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
|
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
|
||||||
item, err := b.getRedisClient(connName, db)
|
item, err := b.getRedisClient(connName, db)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import useDialog from 'stores/dialog'
|
import useDialog from 'stores/dialog'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { isEmpty, size } from 'lodash'
|
import { isEmpty, map, size } from 'lodash'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
|
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||||
|
|
||||||
const deleteForm = reactive({
|
const deleteForm = reactive({
|
||||||
server: '',
|
server: '',
|
||||||
|
@ -25,16 +26,23 @@ watch(
|
||||||
deleteForm.server = server
|
deleteForm.server = server
|
||||||
deleteForm.db = db
|
deleteForm.db = db
|
||||||
deleteForm.key = key
|
deleteForm.key = key
|
||||||
deleteForm.showAffected = false
|
|
||||||
deleteForm.loadingAffected = false
|
deleteForm.loadingAffected = false
|
||||||
deleteForm.affectedKeys = []
|
// deleteForm.async = true
|
||||||
deleteForm.async = true
|
|
||||||
loading.value = false
|
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 loading = ref(false)
|
||||||
|
const deleting = ref(false)
|
||||||
const scanAffectedKey = async () => {
|
const scanAffectedKey = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
@ -53,20 +61,21 @@ const resetAffected = () => {
|
||||||
deleteForm.affectedKeys = []
|
deleteForm.affectedKeys = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logLines = computed(() => {
|
||||||
|
return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k))
|
||||||
|
})
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const onConfirmDelete = async () => {
|
const onConfirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
deleting.value = true
|
||||||
const { server, db, key, async } = deleteForm
|
const { server, db, key, affectedKeys } = deleteForm
|
||||||
const success = await browserStore.deleteKeyPrefix(server, db, key, async)
|
browserStore.deleteKeys(server, db, affectedKeys).catch((e) => {})
|
||||||
if (success) {
|
|
||||||
$message.success(i18n.t('dialogue.handle_succ'))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$message.error(e.message)
|
$message.error(e.message)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
deleting.value = false
|
||||||
}
|
}
|
||||||
dialogStore.closeDeleteKeyDialog()
|
dialogStore.closeDeleteKeyDialog()
|
||||||
}
|
}
|
||||||
|
@ -88,29 +97,35 @@ const onClose = () => {
|
||||||
transform-origin="center">
|
transform-origin="center">
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-form-item :label="$t('dialogue.key.server')">
|
<n-grid :x-gap="10">
|
||||||
|
<n-form-item-gi :label="$t('dialogue.key.server')" :span="12">
|
||||||
<n-input :value="deleteForm.server" readonly />
|
<n-input :value="deleteForm.server" readonly />
|
||||||
</n-form-item>
|
</n-form-item-gi>
|
||||||
<n-form-item :label="$t('dialogue.key.db_index')">
|
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
|
||||||
<n-input :value="deleteForm.db.toString()" readonly />
|
<n-input :value="deleteForm.db.toString()" readonly />
|
||||||
</n-form-item>
|
</n-form-item-gi>
|
||||||
<n-form-item :label="$t('dialogue.key.key_expression')" required>
|
</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-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item :label="$t('dialogue.key.async_delete')" required>
|
<!-- <n-form-item :label="$t('dialogue.key.async_delete')" required>-->
|
||||||
<n-checkbox v-model:checked="deleteForm.async">
|
<!-- <n-checkbox v-model:checked="deleteForm.async">-->
|
||||||
{{ $t('dialogue.key.async_delete_title') }}
|
<!-- {{ $t('dialogue.key.async_delete_title') }}-->
|
||||||
</n-checkbox>
|
<!-- </n-checkbox>-->
|
||||||
</n-form-item>
|
<!-- </n-form-item>-->
|
||||||
<n-card
|
<n-card
|
||||||
v-if="deleteForm.showAffected"
|
v-if="deleteForm.showAffected"
|
||||||
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
||||||
|
embedded
|
||||||
size="small">
|
size="small">
|
||||||
<n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text />
|
<n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text />
|
||||||
<n-log
|
<n-log
|
||||||
v-else
|
v-else
|
||||||
:line-height="1.5"
|
:line-height="1.5"
|
||||||
:lines="deleteForm.affectedKeys"
|
:lines="logLines"
|
||||||
:rows="10"
|
:rows="10"
|
||||||
style="user-select: text; cursor: text" />
|
style="user-select: text; cursor: text" />
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
|
@ -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 IconButton from '@/components/common/IconButton.vue'
|
||||||
import useTabStore from 'stores/tab.js'
|
import useTabStore from 'stores/tab.js'
|
||||||
import { computed, nextTick, onMounted, reactive, ref, unref, watch } from 'vue'
|
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 Refresh from '@/components/icons/Refresh.vue'
|
||||||
import useDialogStore from 'stores/dialog.js'
|
import useDialogStore from 'stores/dialog.js'
|
||||||
import { useI18n } from 'vue-i18n'
|
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 { types } from '@/consts/support_redis_type.js'
|
||||||
import Plus from '@/components/icons/Plus.vue'
|
import Plus from '@/components/icons/Plus.vue'
|
||||||
import useConnectionStore from 'stores/connections.js'
|
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 themeVars = useThemeVars()
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
|
@ -33,6 +36,8 @@ const currentName = computed(() => get(tabStore.currentTab, 'name', ''))
|
||||||
const browserTreeRef = ref(null)
|
const browserTreeRef = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const fullyLoaded = ref(false)
|
const fullyLoaded = ref(false)
|
||||||
|
const inCheckState = ref(false)
|
||||||
|
const checkedCount = ref(0)
|
||||||
|
|
||||||
const selectedDB = computed(() => {
|
const selectedDB = computed(() => {
|
||||||
return browserStore.selectedDatabases[currentName.value] || 0
|
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 loadProgress = computed(() => {
|
||||||
const db = browserStore.getDatabase(currentName.value, selectedDB.value)
|
const db = browserStore.getDatabase(currentName.value, selectedDB.value)
|
||||||
if (db.maxKeys <= 0) {
|
if (db.maxKeys <= 0) {
|
||||||
|
@ -62,6 +78,12 @@ const loadProgress = computed(() => {
|
||||||
return (db.keys * 100) / Math.max(db.keys, db.maxKeys)
|
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 () => {
|
const onReload = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
@ -115,6 +137,10 @@ const onLoadAll = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onDeleteChecked = () => {
|
||||||
|
browserTreeRef.value?.deleteCheckedItems()
|
||||||
|
}
|
||||||
|
|
||||||
const onFlush = () => {
|
const onFlush = () => {
|
||||||
dialogStore.openFlushDBDialog(currentName.value, selectedDB.value)
|
dialogStore.openFlushDBDialog(currentName.value, selectedDB.value)
|
||||||
}
|
}
|
||||||
|
@ -146,6 +172,17 @@ const onMatchInput = (matchVal, filterVal) => {
|
||||||
onReload()
|
onReload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSelectOptions = (select) => {
|
||||||
|
switch (select) {
|
||||||
|
case 'flush':
|
||||||
|
onFlush()
|
||||||
|
break
|
||||||
|
case 'disconnect':
|
||||||
|
onDisconnect()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => browserStore.openedDB[currentName.value],
|
() => browserStore.openedDB[currentName.value],
|
||||||
async (db, prevDB) => {
|
async (db, prevDB) => {
|
||||||
|
@ -158,7 +195,7 @@ watch(
|
||||||
browserStore.closeDatabase(currentName.value, prevDB)
|
browserStore.closeDatabase(currentName.value, prevDB)
|
||||||
browserStore.setKeyFilter(currentName.value, {})
|
browserStore.setKeyFilter(currentName.value, {})
|
||||||
await browserStore.openDatabase(currentName.value, db)
|
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)
|
fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, db)
|
||||||
browserTreeRef.value?.refreshTree()
|
browserTreeRef.value?.refreshTree()
|
||||||
|
|
||||||
|
@ -232,6 +269,9 @@ onMounted(() => onReload())
|
||||||
<!-- tree view -->
|
<!-- tree view -->
|
||||||
<browser-tree
|
<browser-tree
|
||||||
ref="browserTreeRef"
|
ref="browserTreeRef"
|
||||||
|
v-model:checked-count="checkedCount"
|
||||||
|
:check-mode="inCheckState"
|
||||||
|
:db="browserStore.openedDB[currentName]"
|
||||||
:full-loaded="fullyLoaded"
|
:full-loaded="fullyLoaded"
|
||||||
:loading="loading && loadProgress <= 0"
|
:loading="loading && loadProgress <= 0"
|
||||||
:pattern="filterForm.filter"
|
:pattern="filterForm.filter"
|
||||||
|
@ -245,7 +285,8 @@ onMounted(() => onReload())
|
||||||
<!-- :stroke-width="3.5"-->
|
<!-- :stroke-width="3.5"-->
|
||||||
<!-- unselect-stroke-width="3"-->
|
<!-- unselect-stroke-width="3"-->
|
||||||
<!-- @update:value="onSwitchView" />-->
|
<!-- @update:value="onSwitchView" />-->
|
||||||
<div class="flex-box-h nav-pane-func">
|
<transition mode="out-in" name="fade">
|
||||||
|
<div v-if="!inCheckState" class="flex-box-h nav-pane-func">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="browserStore.openedDB[currentName]"
|
v-model:value="browserStore.openedDB[currentName]"
|
||||||
:consistent-menu-width="false"
|
:consistent-menu-width="false"
|
||||||
|
@ -276,19 +317,38 @@ onMounted(() => onReload())
|
||||||
<div class="flex-item-expand" style="min-width: 10px" />
|
<div class="flex-item-expand" style="min-width: 10px" />
|
||||||
<icon-button
|
<icon-button
|
||||||
:button-class="['nav-pane-func-btn']"
|
: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"
|
:icon="Delete"
|
||||||
:stroke-width="3.5"
|
:stroke-width="3.5"
|
||||||
size="20"
|
size="20"
|
||||||
t-tooltip="interface.flush_db"
|
t-tooltip="interface.delete_checked"
|
||||||
@click="onFlush" />
|
@click="onDeleteChecked" />
|
||||||
|
<div class="flex-item-expand ellipsis" style="text-align: center; margin: 0 5px">
|
||||||
|
<n-text>{{ checkedTip }}</n-text>
|
||||||
|
</div>
|
||||||
<icon-button
|
<icon-button
|
||||||
:button-class="['nav-pane-func-btn']"
|
:button-class="['nav-pane-func-btn']"
|
||||||
:icon="Unlink"
|
:icon="Close"
|
||||||
:stroke-width="3.5"
|
:stroke-width="3.5"
|
||||||
size="20"
|
size="20"
|
||||||
t-tooltip="interface.disconnect"
|
t-tooltip="interface.quit_check_mode"
|
||||||
@click="onDisconnect" />
|
@click="inCheckState = false" />
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -299,17 +359,29 @@ onMounted(() => onReload())
|
||||||
:deep(.toggle-btn) {
|
:deep(.toggle-btn) {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.filter-on) {
|
:deep(.toggle-on) {
|
||||||
border-color: v-bind('themeVars.iconColorDisabled');
|
border-color: v-bind('themeVars.iconColorDisabled');
|
||||||
background-color: v-bind('themeVars.iconColorDisabled');
|
background-color: v-bind('themeVars.iconColorDisabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.filter-off) {
|
:deep(.toggle-off) {
|
||||||
border-color: #0000;
|
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 {
|
.nav-pane-top {
|
||||||
//@include bottom-shadow(0.1);
|
//@include bottom-shadow(0.1);
|
||||||
color: v-bind('themeVars.iconColor');
|
color: v-bind('themeVars.iconColor');
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script setup>
|
<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 { ConnectionType } from '@/consts/connection_type.js'
|
||||||
import { NIcon, NSpace, useThemeVars } from 'naive-ui'
|
import { NIcon, NSpace, useThemeVars } from 'naive-ui'
|
||||||
import Key from '@/components/icons/Key.vue'
|
import Key from '@/components/icons/Key.vue'
|
||||||
import Binary from '@/components/icons/Binary.vue'
|
import Binary from '@/components/icons/Binary.vue'
|
||||||
import Database from '@/components/icons/Database.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 { useI18n } from 'vue-i18n'
|
||||||
import Refresh from '@/components/icons/Refresh.vue'
|
import Refresh from '@/components/icons/Refresh.vue'
|
||||||
import CopyLink from '@/components/icons/CopyLink.vue'
|
import CopyLink from '@/components/icons/CopyLink.vue'
|
||||||
|
@ -26,21 +26,40 @@ import { useRender } from '@/utils/render.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
server: String,
|
server: String,
|
||||||
|
db: Number,
|
||||||
keyView: String,
|
keyView: String,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
pattern: String,
|
pattern: String,
|
||||||
fullLoaded: Boolean,
|
fullLoaded: Boolean,
|
||||||
|
checkMode: Boolean,
|
||||||
|
checkedCount: Number,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:checked-count'])
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const render = useRender()
|
const render = useRender()
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const expandedKeys = ref([props.server])
|
const expandedKeys = ref([])
|
||||||
|
const checkedKeys = reactive({
|
||||||
|
keys: [],
|
||||||
|
redisKeys: [],
|
||||||
|
})
|
||||||
const connectionStore = useConnectionStore()
|
const connectionStore = useConnectionStore()
|
||||||
const browserStore = useBrowserStore()
|
const browserStore = useBrowserStore()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
if (!props.checkMode) {
|
||||||
|
resetCheckedKey()
|
||||||
|
}
|
||||||
|
emit('update:checked-count', size(checkedKeys.keys))
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {ComputedRef<string[]>}
|
* @type {ComputedRef<string[]>}
|
||||||
|
@ -48,9 +67,9 @@ const dialogStore = useDialogStore()
|
||||||
const selectedKeys = computed(() => {
|
const selectedKeys = computed(() => {
|
||||||
const tab = find(tabStore.tabList, { name: props.server })
|
const tab = find(tabStore.tabList, { name: props.server })
|
||||||
if (tab != null) {
|
if (tab != null) {
|
||||||
return get(tab, 'selectedKeys', [props.server])
|
return get(tab, 'selectedKeys', [])
|
||||||
}
|
}
|
||||||
return [props.server]
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = computed(() => {
|
const data = computed(() => {
|
||||||
|
@ -170,6 +189,11 @@ const resetExpandKey = (server, db, includeDB) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetCheckedKey = () => {
|
||||||
|
checkedKeys.keys = []
|
||||||
|
checkedKeys.redisKeys = []
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelectContextMenu = (key) => {
|
const handleSelectContextMenu = (key) => {
|
||||||
contextMenuParam.show = false
|
contextMenuParam.show = false
|
||||||
const selectedKey = get(selectedKeys.value, 0)
|
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) => {
|
const onUpdateExpanded = (value, option, meta) => {
|
||||||
expandedKeys.value = value
|
expandedKeys.value = value
|
||||||
if (!meta.node) {
|
if (!meta.node) {
|
||||||
|
@ -273,30 +323,17 @@ const onUpdateExpanded = (value, option, meta) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateSelectedKeys = (keys, options) => {
|
/**
|
||||||
try {
|
*
|
||||||
if (!isEmpty(options)) {
|
* @param {string[]} keys
|
||||||
// prevent load duplicate key
|
* @param {TreeOption[]} options
|
||||||
for (const node of options) {
|
*/
|
||||||
if (node.type === ConnectionType.RedisValue) {
|
const onUpdateCheckedKeys = (keys, options) => {
|
||||||
const { key, db } = node
|
checkedKeys.keys = keys
|
||||||
const redisKey = node.redisKeyCode || node.redisKey
|
checkedKeys.redisKeys = map(
|
||||||
if (!includes(selectedKeys.value, key)) {
|
filter(options, (o) => o.type === ConnectionType.RedisValue),
|
||||||
browserStore.loadKeySummary({
|
(o) => o.redisKeyCode || o.redisKey,
|
||||||
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 renderPrefix = ({ option }) => {
|
const renderPrefix = ({ option }) => {
|
||||||
|
@ -449,8 +486,10 @@ const nodeProps = ({ option }) => {
|
||||||
console.warn('TODO: alert to ignore double click when loading')
|
console.warn('TODO: alert to ignore double click when loading')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!props.checkMode) {
|
||||||
// default handle is expand current node
|
// default handle is expand current node
|
||||||
nextTick().then(() => expandKey(option.key))
|
nextTick().then(() => expandKey(option.key))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onContextmenu(e) {
|
onContextmenu(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -484,6 +523,13 @@ defineExpose({
|
||||||
resetExpandKey,
|
resetExpandKey,
|
||||||
refreshTree: () => {
|
refreshTree: () => {
|
||||||
treeKey.value = Date.now()
|
treeKey.value = Date.now()
|
||||||
|
expandedKeys.value = []
|
||||||
|
resetCheckedKey()
|
||||||
|
},
|
||||||
|
deleteCheckedItems: () => {
|
||||||
|
if (!isEmpty(checkedKeys.redisKeys)) {
|
||||||
|
dialogStore.openDeleteKeyDialog(props.server, props.db, checkedKeys.redisKeys)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -499,6 +545,9 @@ defineExpose({
|
||||||
:block-line="true"
|
:block-line="true"
|
||||||
:block-node="true"
|
:block-node="true"
|
||||||
:cancelable="false"
|
:cancelable="false"
|
||||||
|
:cascade="true"
|
||||||
|
:checkable="props.checkMode"
|
||||||
|
:checked-keys="checkedKeys.keys"
|
||||||
:data="data"
|
:data="data"
|
||||||
:expand-on-click="false"
|
:expand-on-click="false"
|
||||||
:expanded-keys="expandedKeys"
|
:expanded-keys="expandedKeys"
|
||||||
|
@ -510,10 +559,12 @@ defineExpose({
|
||||||
:render-suffix="renderSuffix"
|
:render-suffix="renderSuffix"
|
||||||
:selected-keys="selectedKeys"
|
:selected-keys="selectedKeys"
|
||||||
:show-irrelevant-nodes="false"
|
:show-irrelevant-nodes="false"
|
||||||
|
check-strategy="child"
|
||||||
class="fill-height"
|
class="fill-height"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@update:selected-keys="onUpdateSelectedKeys"
|
@update:selected-keys="onUpdateSelectedKeys"
|
||||||
@update:expanded-keys="onUpdateExpanded" />
|
@update:expanded-keys="onUpdateExpanded"
|
||||||
|
@update:checked-keys="onUpdateCheckedKeys" />
|
||||||
<n-dropdown
|
<n-dropdown
|
||||||
:options="contextMenuParam.options"
|
:options="contextMenuParam.options"
|
||||||
:render-label="renderContextLabel"
|
:render-label="renderContextLabel"
|
||||||
|
|
|
@ -69,6 +69,9 @@
|
||||||
"delete_key": "Delete Key",
|
"delete_key": "Delete Key",
|
||||||
"batch_delete_key": "Batch Delete Keys",
|
"batch_delete_key": "Batch Delete Keys",
|
||||||
"flush_db": "Flush Database",
|
"flush_db": "Flush Database",
|
||||||
|
"check_mode": "Check Mode",
|
||||||
|
"quit_check_mode": "Quit Check Mode",
|
||||||
|
"delete_checked": "Delete Checked Items",
|
||||||
"copy_value": "Copy Value",
|
"copy_value": "Copy Value",
|
||||||
"edit_value": "Edit Value",
|
"edit_value": "Edit Value",
|
||||||
"save_update": "Save Update",
|
"save_update": "Save Update",
|
||||||
|
@ -132,6 +135,8 @@
|
||||||
"remove_tip": "{type} \"{name}\" will be deleted",
|
"remove_tip": "{type} \"{name}\" will be deleted",
|
||||||
"remove_group_tip": "Group \"{name}\" and all connections in it will be deleted",
|
"remove_group_tip": "Group \"{name}\" and all connections in it will be deleted",
|
||||||
"delete_key_succ": "\"{key}\" has been 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",
|
"rename_binary_key_fail": "Rename binary key name is unsupported",
|
||||||
"handle_succ": "Success!",
|
"handle_succ": "Success!",
|
||||||
"reload_succ": "Reloaded!",
|
"reload_succ": "Reloaded!",
|
||||||
|
|
|
@ -69,6 +69,9 @@
|
||||||
"delete_key": "删除键",
|
"delete_key": "删除键",
|
||||||
"batch_delete_key": "批量删除键",
|
"batch_delete_key": "批量删除键",
|
||||||
"flush_db": "清空数据库",
|
"flush_db": "清空数据库",
|
||||||
|
"check_mode": "勾选模式",
|
||||||
|
"quit_check_mode": "退出勾选模式",
|
||||||
|
"delete_checked": "删除勾选项",
|
||||||
"copy_value": "复制值",
|
"copy_value": "复制值",
|
||||||
"edit_value": "修改值",
|
"edit_value": "修改值",
|
||||||
"save_update": "保存修改",
|
"save_update": "保存修改",
|
||||||
|
@ -132,6 +135,8 @@
|
||||||
"remove_tip": "{type} \"{name}\" 将会被删除",
|
"remove_tip": "{type} \"{name}\" 将会被删除",
|
||||||
"remove_group_tip": "分组 \"{name}\"及其所有连接将会被删除",
|
"remove_group_tip": "分组 \"{name}\"及其所有连接将会被删除",
|
||||||
"delete_key_succ": "{key} 已被删除",
|
"delete_key_succ": "{key} 已被删除",
|
||||||
|
"deleting_key": "正在删除键:{key} ({index}/{count})",
|
||||||
|
"delete_completed": "已完成删除操作,成功{success}个,失败{fail}个",
|
||||||
"rename_binary_key_fail": "不支持重命名二进制键名",
|
"rename_binary_key_fail": "不支持重命名二进制键名",
|
||||||
"handle_succ": "操作成功",
|
"handle_succ": "操作成功",
|
||||||
"reload_succ": "已重新载入",
|
"reload_succ": "已重新载入",
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
CloseConnection,
|
CloseConnection,
|
||||||
ConvertValue,
|
ConvertValue,
|
||||||
DeleteKey,
|
DeleteKey,
|
||||||
|
DeleteOneKey,
|
||||||
FlushDB,
|
FlushDB,
|
||||||
GetCmdHistory,
|
GetCmdHistory,
|
||||||
GetKeyDetail,
|
GetKeyDetail,
|
||||||
|
@ -53,6 +54,7 @@ import { ConnectionType } from '@/consts/connection_type.js'
|
||||||
import useConnectionStore from 'stores/connections.js'
|
import useConnectionStore from 'stores/connections.js'
|
||||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
||||||
import { isRedisGlob } from '@/utils/glob_pattern.js'
|
import { isRedisGlob } from '@/utils/glob_pattern.js'
|
||||||
|
import { i18nGlobal } from '@/utils/i18n.js'
|
||||||
|
|
||||||
const useBrowserStore = defineStore('browser', {
|
const useBrowserStore = defineStore('browser', {
|
||||||
/**
|
/**
|
||||||
|
@ -1841,43 +1843,46 @@ const useBrowserStore = defineStore('browser', {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* delete keys with prefix
|
* delete multiple keys
|
||||||
* @param {string} connName
|
* @param {string} server
|
||||||
* @param {number} db
|
* @param {number} db
|
||||||
* @param {string} prefix
|
* @param {string[]|number[][]} keys
|
||||||
* @param {boolean} async
|
* @return {Promise<void>}
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
*/
|
||||||
async deleteKeyPrefix(connName, db, prefix, async) {
|
async deleteKeys(server, db, keys) {
|
||||||
if (isEmpty(prefix)) {
|
const delMsgRef = $message.loading('', { duration: 0 })
|
||||||
return false
|
let progress = 0
|
||||||
}
|
let count = size(keys)
|
||||||
try {
|
let deletedCount = 0,
|
||||||
if (!endsWith(prefix, '*')) {
|
failCount = 0
|
||||||
prefix += '*'
|
for (const key of keys) {
|
||||||
}
|
delMsgRef.content = i18nGlobal.t('dialogue.deleting_key', {
|
||||||
const { data, success, msg } = await DeleteKey(connName, db, prefix, async)
|
key: decodeRedisKey(key),
|
||||||
|
index: ++progress,
|
||||||
|
count,
|
||||||
|
})
|
||||||
|
const { success } = await DeleteOneKey(server, db, key)
|
||||||
if (success) {
|
if (success) {
|
||||||
// const { deleted: keys = [] } = data
|
this._deleteKeyNode(server, db, key, false)
|
||||||
// for (const key of keys) {
|
deletedCount += 1
|
||||||
// await this._deleteKeyNode(connName, db, key)
|
} else {
|
||||||
// }
|
failCount += 1
|
||||||
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)
|
delMsgRef.destroy()
|
||||||
this._tidyNode(connName, db, prefix, true)
|
// refresh model data
|
||||||
this._updateDBMaxKeys(connName, db, -deleteCount)
|
this._tidyNode(server, db, '', true)
|
||||||
return 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 }))
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from "pinia";
|
||||||
import useConnectionStore from './connections.js'
|
import useConnectionStore from "./connections.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* connection dialog type
|
* connection dialog type
|
||||||
|
@ -160,6 +160,12 @@ const useDialogStore = defineStore('dialog', {
|
||||||
this.renameDialogVisible = false
|
this.renameDialogVisible = false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} server
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string | string[]} key
|
||||||
|
*/
|
||||||
openDeleteKeyDialog(server, db, key) {
|
openDeleteKeyDialog(server, db, key) {
|
||||||
this.deleteKeyParam.server = server
|
this.deleteKeyParam.server = server
|
||||||
this.deleteKeyParam.db = db
|
this.deleteKeyParam.db = db
|
||||||
|
|
|
@ -10,6 +10,7 @@ const useTabStore = defineStore('tab', {
|
||||||
* @property {string} [title] tab title
|
* @property {string} [title] tab title
|
||||||
* @property {string} [icon] tab icon
|
* @property {string} [icon] tab icon
|
||||||
* @property {string[]} selectedKeys
|
* @property {string[]} selectedKeys
|
||||||
|
* @property {string[]} checkdeKeys
|
||||||
* @property {string} [type] key type
|
* @property {string} [type] key type
|
||||||
* @property {*} [value] key value
|
* @property {*} [value] key value
|
||||||
* @property {string} [server] server name
|
* @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} server
|
||||||
* @param {string|string[]} [keys]
|
* @param {string|string[]} [keys]
|
||||||
*/
|
*/
|
||||||
|
@ -647,7 +648,7 @@ const useTabStore = defineStore('tab', {
|
||||||
if (tab != null) {
|
if (tab != null) {
|
||||||
if (keys == null) {
|
if (keys == null) {
|
||||||
// select nothing
|
// select nothing
|
||||||
tab.selectedKeys = [server]
|
tab.selectedKeys = []
|
||||||
} else if (typeof keys === 'string') {
|
} else if (typeof keys === 'string') {
|
||||||
tab.selectedKeys = [keys]
|
tab.selectedKeys = [keys]
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue