feat(redis): delete leaf keys from the key list

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-21 12:20:57 +08:00
1 parent 25d5b885ba
commit b09fd35cd8
10 files changed
+58 -2

No files matched your search

@@ -400,11 +400,12 @@ function resetApiMocks() {
mocks.canBuildRedisFuzzyTree.mockImplementation((loadedKeyCount: number) => loadedKeyCount <= 200_000); mocks.canBuildRedisFuzzyTree.mockImplementation((loadedKeyCount: number) => loadedKeyCount <= 200_000);
} }
function mountBrowser() { function mountBrowser(withDeleteDetails = false) {
const host = document.createElement("div"); const host = document.createElement("div");
document.body.append(host); document.body.append(host);
const app = createApp(RedisKeyBrowser, { connectionId: "connection", db: 0, blockDangerousRedisCommands: false }); const app = createApp(RedisKeyBrowser, { connectionId: "connection", db: 0, blockDangerousRedisCommands: false });
app.use(createI18n({ legacy: false, locale: "en", messages: { en: {} }, missingWarn: false, fallbackWarn: false })); const messages = { en: { redis: { deleteGroupDetails: withDeleteDetails ? "{target}\n{count} keys" : "redis.deleteGroupDetails" } } };
app.use(createI18n({ legacy: false, locale: "en", messages, missingWarn: false, fallbackWarn: false }));
app.mount(host); app.mount(host);
mountedApps.push({ unmount: () => app.unmount(), host }); mountedApps.push({ unmount: () => app.unmount(), host });
return host; return host;
@@ -1126,6 +1127,29 @@ describe("RedisKeyBrowser expiry creation", () => {
}); });
describe("RedisKeyBrowser fuzzy key hierarchy", () => { describe("RedisKeyBrowser fuzzy key hierarchy", () => {
it("deletes a leaf directly from the key list after confirmation", async () => {
const key = { key_display: "session:current", key_raw: "c2Vzc2lvbjpjdXJyZW50", key_type: "string", ttl: -1 };
mocks.redisScanKeysBatch.mockResolvedValue({ cursor: 0, keys: [key], total_keys: 1 });
mocks.redisDeleteKeys.mockResolvedValue(1);
mountBrowser(true);
await settle();
groupRow("session").dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
await settle();
const deleteButton = requiredElement<HTMLButtonElement>('button[title="redis.deleteKey"]');
deleteButton.click();
await settle();
expect(mocks.redisDeleteKeys).not.toHaveBeenCalled();
expect(requiredElement<HTMLElement>("[data-test-danger-details]").textContent).toContain("session:current");
requiredElement<HTMLButtonElement>("[data-test-danger-confirm]").click();
await settle();
expect(mocks.redisDeleteKeys).toHaveBeenCalledWith("connection", 0, [key.key_raw]);
expect(document.body.textContent).not.toContain("session:current");
});
it("preserves the cursor and finds a sparse fuzzy match after a bounded continuation", async () => { it("preserves the cursor and finds a sparse fuzzy match after a bounded continuation", async () => {
mocks.redisScanPageSize = 1_000; mocks.redisScanPageSize = 1_000;
mountBrowser(); mountBrowser();
@@ -1046,6 +1046,18 @@ function requestGroupDelete(node: RedisKeyTreeNode, event: Event) {
showDangerConfirm.value = true; showDangerConfirm.value = true;
} }
function requestKeyDelete(node: RedisKeyTreeNode, event: Event) {
event.stopPropagation();
if (node.kind !== "leaf" || selectionBusy.value) return;
pendingDanger.value = {
kind: "delete-keys",
title: node.fullKeyDisplay,
keyRaws: [node.keyRaw],
loadedSearchResults: false,
};
showDangerConfirm.value = true;
}
async function copyRedisKeyName(keyName: string) { async function copyRedisKeyName(keyName: string) {
try { try {
await copyToClipboard(keyName); await copyToClipboard(keyName);
@@ -2192,6 +2204,18 @@ defineExpose({ focusSearch, insertCommand, executeCommand: executeAiCommand });
<Button v-if="row.node.kind === 'group' && !isFuzzyHierarchyView" variant="ghost" size="icon" class="h-5 w-5 shrink-0 text-destructive opacity-0 group-hover:opacity-100" :title="t('redis.deleteGroup')" :disabled="selectionBusy" @click="requestGroupDelete(row.node, $event)"> <Button v-if="row.node.kind === 'group' && !isFuzzyHierarchyView" variant="ghost" size="icon" class="h-5 w-5 shrink-0 text-destructive opacity-0 group-hover:opacity-100" :title="t('redis.deleteGroup')" :disabled="selectionBusy" @click="requestGroupDelete(row.node, $event)">
<Trash2 class="h-3 w-3" /> <Trash2 class="h-3 w-3" />
</Button> </Button>
<Button
v-else-if="row.node.kind === 'leaf'"
variant="ghost"
size="icon"
class="h-5 w-5 shrink-0 text-destructive opacity-0 group-hover:opacity-100"
:title="t('redis.deleteKey')"
:aria-label="t('redis.deleteKey')"
:disabled="selectionBusy"
@click="requestKeyDelete(row.node, $event)"
>
<Trash2 class="h-3 w-3" />
</Button>
</div> </div>
</div> </div>
</CustomContextMenu> </CustomContextMenu>
+1
View File
@@ -4482,6 +4482,7 @@ export default {
loadedGroupKeysPartial: "{count} keys loaded so far — this folder may contain more", loadedGroupKeysPartial: "{count} keys loaded so far — this folder may contain more",
fuzzyTreeLimit: "{count} fuzzy matches are shown as a flat list to keep the browser responsive. Refine the search to view the hierarchy.", fuzzyTreeLimit: "{count} fuzzy matches are shown as a flat list to keep the browser responsive. Refine the search to view the hierarchy.",
deleteGroup: "Delete group", deleteGroup: "Delete group",
deleteKey: "Delete key",
deleteGroupDetails: "{target}\n{count} keys", deleteGroupDetails: "{target}\n{count} keys",
deleteLoadedSearchKeysDetails: "{target}\n{count} loaded matching keys", deleteLoadedSearchKeysDetails: "{target}\n{count} loaded matching keys",
flushDb: "Clear current DB", flushDb: "Clear current DB",
+1
View File
@@ -4314,6 +4314,7 @@ export default withEnglishFallback({
loadedGroupKeysPartial: "{count} claves cargadas hasta ahora — esta carpeta puede contener más", loadedGroupKeysPartial: "{count} claves cargadas hasta ahora — esta carpeta puede contener más",
fuzzyTreeLimit: "Se muestran {count} coincidencias difusas como lista plana para mantener el navegador responsivo. Refina la búsqueda para ver la jerarquía.", fuzzyTreeLimit: "Se muestran {count} coincidencias difusas como lista plana para mantener el navegador responsivo. Refina la búsqueda para ver la jerarquía.",
deleteGroup: "Eliminar grupo", deleteGroup: "Eliminar grupo",
deleteKey: "Eliminar clave",
deleteGroupDetails: "{target}\n{count} claves", deleteGroupDetails: "{target}\n{count} claves",
deleteLoadedSearchKeysDetails: "{target}\n{count} claves coincidentes cargadas", deleteLoadedSearchKeysDetails: "{target}\n{count} claves coincidentes cargadas",
flushDb: "Limpiar DB actual", flushDb: "Limpiar DB actual",
+1
View File
@@ -4312,6 +4312,7 @@ export default withEnglishFallback({
loadedGroupKeysPartial: "{count} chiavi caricate finora — questa cartella potrebbe contenerne altre", loadedGroupKeysPartial: "{count} chiavi caricate finora — questa cartella potrebbe contenerne altre",
fuzzyTreeLimit: "{count} corrispondenze fuzzy sono mostrate come elenco piatto per mantenere il browser reattivo. Restringi la ricerca per vedere la gerarchia.", fuzzyTreeLimit: "{count} corrispondenze fuzzy sono mostrate come elenco piatto per mantenere il browser reattivo. Restringi la ricerca per vedere la gerarchia.",
deleteGroup: "Elimina gruppo", deleteGroup: "Elimina gruppo",
deleteKey: "Elimina chiave",
deleteGroupDetails: "{target}\n{count} chiavi", deleteGroupDetails: "{target}\n{count} chiavi",
deleteLoadedSearchKeysDetails: "{target}\n{count} chiavi corrispondenti caricate", deleteLoadedSearchKeysDetails: "{target}\n{count} chiavi corrispondenti caricate",
flushDb: "Pulisci DB corrente", flushDb: "Pulisci DB corrente",
+1
View File
@@ -4342,6 +4342,7 @@ export default withEnglishFallback({
loadedGroupKeysPartial: "これまでに読み込まれたキー {count} 件 — このフォルダーにはさらにキーが存在する可能性があります", loadedGroupKeysPartial: "これまでに読み込まれたキー {count} 件 — このフォルダーにはさらにキーが存在する可能性があります",
fuzzyTreeLimit: "ブラウザーの応答性を保つため、あいまい一致 {count} 件をフラットな一覧で表示しています。階層を表示するには検索条件を絞り込んでください。", fuzzyTreeLimit: "ブラウザーの応答性を保つため、あいまい一致 {count} 件をフラットな一覧で表示しています。階層を表示するには検索条件を絞り込んでください。",
deleteGroup: "グループを削除", deleteGroup: "グループを削除",
deleteKey: "キーを削除",
deleteGroupDetails: "{target}\n{count}個のキー", deleteGroupDetails: "{target}\n{count}個のキー",
deleteLoadedSearchKeysDetails: "{target}\n読み込み済みの一致キー {count} 件", deleteLoadedSearchKeysDetails: "{target}\n読み込み済みの一致キー {count} 件",
flushDb: "現在のDBをクリア", flushDb: "現在のDBをクリア",
+1
View File
@@ -3952,6 +3952,7 @@ export default withEnglishFallback({
loadedGroupKeysPartial: "지금까지 로드된 {count}개 키 — 이 폴더에 더 많은 키가 있을 수 있습니다", loadedGroupKeysPartial: "지금까지 로드된 {count}개 키 — 이 폴더에 더 많은 키가 있을 수 있습니다",
fuzzyTreeLimit: "{count}개의 퍼지 일치가 브라우저 응답성을 유지하기 위해 평면 목록으로 표시됩니다. 계층 구조를 보려면 검색을 좁히세요.", fuzzyTreeLimit: "{count}개의 퍼지 일치가 브라우저 응답성을 유지하기 위해 평면 목록으로 표시됩니다. 계층 구조를 보려면 검색을 좁히세요.",
deleteGroup: "그룹 삭제", deleteGroup: "그룹 삭제",
deleteKey: "키 삭제",
deleteGroupDetails: "{target}\n키 {count}개", deleteGroupDetails: "{target}\n키 {count}개",
deleteLoadedSearchKeysDetails: "{target}\n로드된 일치 키 {count}개", deleteLoadedSearchKeysDetails: "{target}\n로드된 일치 키 {count}개",
flushDb: "현재 DB 비우기", flushDb: "현재 DB 비우기",
+1
View File
@@ -4314,6 +4314,7 @@ export default withEnglishFallback({
loadedGroupKeysPartial: "{count} chaves carregadas até agora — esta pasta pode conter mais", loadedGroupKeysPartial: "{count} chaves carregadas até agora — esta pasta pode conter mais",
fuzzyTreeLimit: "{count} correspondências difusas são mostradas como uma lista plana para manter o navegador responsivo. Refine a busca para ver a hierarquia.", fuzzyTreeLimit: "{count} correspondências difusas são mostradas como uma lista plana para manter o navegador responsivo. Refine a busca para ver a hierarquia.",
deleteGroup: "Excluir grupo", deleteGroup: "Excluir grupo",
deleteKey: "Excluir chave",
deleteGroupDetails: "{target}\n{count} chaves", deleteGroupDetails: "{target}\n{count} chaves",
deleteLoadedSearchKeysDetails: "{target}\n{count} chaves correspondentes carregadas", deleteLoadedSearchKeysDetails: "{target}\n{count} chaves correspondentes carregadas",
flushDb: "Limpar DB atual", flushDb: "Limpar DB atual",
+1
View File
@@ -4469,6 +4469,7 @@ export default withEnglishFallback({
loadedGroupKeysPartial: "目前已加载 {count} 个 key —— 该文件夹下可能还有更多", loadedGroupKeysPartial: "目前已加载 {count} 个 key —— 该文件夹下可能还有更多",
fuzzyTreeLimit: "已加载 {count} 个模糊匹配 key。为保持浏览器响应,当前以扁平列表展示;请缩小搜索范围以查看层级。", fuzzyTreeLimit: "已加载 {count} 个模糊匹配 key。为保持浏览器响应,当前以扁平列表展示;请缩小搜索范围以查看层级。",
deleteGroup: "删除分组", deleteGroup: "删除分组",
deleteKey: "删除 key",
deleteGroupDetails: "{target}\n{count} 个 key", deleteGroupDetails: "{target}\n{count} 个 key",
deleteLoadedSearchKeysDetails: "{target}\n已加载的 {count} 个匹配 key", deleteLoadedSearchKeysDetails: "{target}\n已加载的 {count} 个匹配 key",
flushDb: "清空当前 DB", flushDb: "清空当前 DB",
+1
View File
@@ -3641,6 +3641,7 @@ export default withEnglishFallback({
loadedGroupKeysPartial: "目前已載入 {count} 個 key —— 此資料夾下可能還有更多", loadedGroupKeysPartial: "目前已載入 {count} 個 key —— 此資料夾下可能還有更多",
fuzzyTreeLimit: "已載入 {count} 個模糊相符 key。為保持瀏覽器回應,目前以扁平清單顯示;請縮小搜尋範圍以查看層級。", fuzzyTreeLimit: "已載入 {count} 個模糊相符 key。為保持瀏覽器回應,目前以扁平清單顯示;請縮小搜尋範圍以查看層級。",
deleteGroup: "刪除群組", deleteGroup: "刪除群組",
deleteKey: "刪除 key",
deleteGroupDetails: "{target}\n{count} 個 key", deleteGroupDetails: "{target}\n{count} 個 key",
deleteLoadedSearchKeysDetails: "{target}\n已載入的 {count} 個相符 key", deleteLoadedSearchKeysDetails: "{target}\n已載入的 {count} 個相符 key",
flushDb: "清空目前 DB", flushDb: "清空目前 DB",