feat(phase4e): batch delete with multi-select
- KeyList: multi-select mode with checkbox toggle - Batch delete bar shows selected count + delete button - RedisService: batchDelete using pipeline - Preload: batchDelete IPC channel
This commit is contained in:
@@ -179,4 +179,7 @@ export function registerIpcHandlers(): void {
|
||||
ipcMain.handle('redis:memoryUsage', async (_e, id: string, key: string) => {
|
||||
return redisService.memoryUsage(id, key)
|
||||
})
|
||||
ipcMain.handle('redis:batchDelete', async (_e, id: string, keys: string[]) => {
|
||||
return redisService.batchDelete(id, keys)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,3 +278,15 @@ export async function memoryUsage(id: string, key: string): Promise<number | nul
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.memory('USAGE', key) as Promise<number | null>
|
||||
}
|
||||
|
||||
export async function batchDelete(id: string, keys: string[]): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
if (keys.length === 0) return 0
|
||||
const pipeline = client.pipeline()
|
||||
for (const key of keys) {
|
||||
pipeline.del(key)
|
||||
}
|
||||
const results = await pipeline.exec()
|
||||
return results?.filter(([err]) => !err).length ?? 0
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -86,6 +86,7 @@ export interface ElectronAPI {
|
||||
slowLogGet: (id: string, count: number) => Promise<any[]>
|
||||
slowLogLen: (id: string) => Promise<number>
|
||||
memoryUsage: (id: string, key: string) => Promise<number | null>
|
||||
batchDelete: (id: string, keys: string[]) => Promise<number>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ const electronAPI = {
|
||||
// Memory
|
||||
memoryUsage: (id: string, key: string): Promise<number | null> =>
|
||||
ipcRenderer.invoke('redis:memoryUsage', id, key),
|
||||
batchDelete: (id: string, keys: string[]): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:batchDelete', id, keys),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
const filterText = ref('')
|
||||
const showTree = ref(false)
|
||||
const multiSelect = ref(false)
|
||||
const selectedKeys = ref<Set<string>>(new Set())
|
||||
|
||||
async function loadMore() {
|
||||
if (!connStore.activeId) return
|
||||
@@ -17,18 +20,43 @@ async function loadMore() {
|
||||
|
||||
async function refreshKeys() {
|
||||
if (!connStore.activeId) return
|
||||
selectedKeys.value.clear()
|
||||
await keyStore.scanKeys(connStore.activeId, '0', true)
|
||||
}
|
||||
|
||||
async function selectKey(keyName: string) {
|
||||
if (!connStore.activeId) return
|
||||
if (multiSelect.value) {
|
||||
if (selectedKeys.value.has(keyName)) {
|
||||
selectedKeys.value.delete(keyName)
|
||||
} else {
|
||||
selectedKeys.value.add(keyName)
|
||||
}
|
||||
return
|
||||
}
|
||||
keyStore.selectedKey = keyName
|
||||
await keyStore.loadKeyData(connStore.activeId, keyName)
|
||||
}
|
||||
|
||||
function onPatternChange() {
|
||||
if (!connStore.activeId) return
|
||||
keyStore.scanKeys(connStore.activeId, '0', true)
|
||||
async function batchDelete() {
|
||||
if (!connStore.activeId || selectedKeys.value.size === 0) return
|
||||
const count = selectedKeys.value.size
|
||||
try {
|
||||
await ElMessageBox.confirm(`Delete ${count} key(s)?`, 'Confirm')
|
||||
const deleted = await window.electronAPI.redis.batchDelete(connStore.activeId, Array.from(selectedKeys.value))
|
||||
ElMessage.success(`Deleted ${deleted} key(s)`)
|
||||
selectedKeys.value.clear()
|
||||
await refreshKeys()
|
||||
} catch (err: any) {
|
||||
if (err !== 'cancel') ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMultiSelect() {
|
||||
multiSelect.value = !multiSelect.value
|
||||
if (!multiSelect.value) {
|
||||
selectedKeys.value.clear()
|
||||
}
|
||||
}
|
||||
|
||||
const filteredKeys = ref<string[]>([])
|
||||
@@ -70,9 +98,23 @@ watch(filterText, (val) => {
|
||||
>
|
||||
<el-icon><List /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="multiSelect ? 'danger' : 'default'"
|
||||
@click="toggleMultiSelect"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<div v-if="multiSelect && selectedKeys.size > 0" class="batch-bar">
|
||||
<span class="batch-count">{{ selectedKeys.size }} selected</span>
|
||||
<el-button size="small" type="danger" @click="batchDelete">
|
||||
Delete Selected
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="key-content">
|
||||
<!-- Flat list -->
|
||||
<div v-if="!showTree" class="key-flat-list">
|
||||
@@ -80,9 +122,19 @@ watch(filterText, (val) => {
|
||||
v-for="keyName in filteredKeys"
|
||||
:key="keyName"
|
||||
class="key-item"
|
||||
:class="{ active: keyStore.selectedKey === keyName }"
|
||||
:class="{
|
||||
active: keyStore.selectedKey === keyName && !multiSelect,
|
||||
selected: selectedKeys.has(keyName),
|
||||
}"
|
||||
@click="selectKey(keyName)"
|
||||
>
|
||||
<input
|
||||
v-if="multiSelect"
|
||||
type="checkbox"
|
||||
class="key-checkbox"
|
||||
:checked="selectedKeys.has(keyName)"
|
||||
@click.stop
|
||||
/>
|
||||
<el-icon class="key-icon"><Document /></el-icon>
|
||||
<span class="key-name">{{ keyName }}</span>
|
||||
</div>
|
||||
@@ -90,7 +142,23 @@ watch(filterText, (val) => {
|
||||
|
||||
<!-- Tree view -->
|
||||
<div v-else class="key-tree">
|
||||
<div v-for="keyName in filteredKeys" :key="keyName" class="key-item" @click="selectKey(keyName)">
|
||||
<div
|
||||
v-for="keyName in filteredKeys"
|
||||
:key="keyName"
|
||||
class="key-item"
|
||||
:class="{
|
||||
active: keyStore.selectedKey === keyName && !multiSelect,
|
||||
selected: selectedKeys.has(keyName),
|
||||
}"
|
||||
@click="selectKey(keyName)"
|
||||
>
|
||||
<input
|
||||
v-if="multiSelect"
|
||||
type="checkbox"
|
||||
class="key-checkbox"
|
||||
:checked="selectedKeys.has(keyName)"
|
||||
@click.stop
|
||||
/>
|
||||
<el-icon class="key-icon"><Document /></el-icon>
|
||||
<span class="key-name">{{ keyName }}</span>
|
||||
</div>
|
||||
@@ -133,6 +201,22 @@ watch(filterText, (val) => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.batch-bar {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--accent-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.batch-count {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.key-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -160,6 +244,18 @@ watch(filterText, (val) => {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.key-item.selected {
|
||||
background: rgba(245, 108, 108, 0.08);
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.key-checkbox {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--accent-light);
|
||||
}
|
||||
|
||||
.key-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user