feat(redis): support renaming keys

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-18 15:23:08 +08:00
1 parent 61103523de
commit 168f2a472b
21 files changed
+297 -2

No files matched your search

@@ -875,6 +875,38 @@ function onKeyDeleted(keyRaw: string) {
removeKnownKey(keyRaw);
}
function onKeyRenamed(oldKeyRaw: string, newKeyRaw: string, newKeyDisplay: string) {
connectionStore.invalidateCompletionCache(props.connectionId, String(props.db));
if (isSearchMode.value) {
void loadKeys();
return;
}
const previous = flatKeys.value.find((key) => key.key_raw === oldKeyRaw);
if (!previous) {
void loadKeys();
return;
}
loadedKeyRaws.delete(oldKeyRaw);
loadedKeyRaws.add(newKeyRaw);
flatKeys.value = flatKeys.value.map((key) => (key.key_raw === oldKeyRaw ? { ...key, key_raw: newKeyRaw, key_display: newKeyDisplay } : key));
if (selectedKeyRaw.value === oldKeyRaw) selectedKeyRaw.value = newKeyRaw;
if (checkedKeys.value.has(oldKeyRaw)) {
const nextCheckedKeys = new Set(checkedKeys.value);
nextCheckedKeys.delete(oldKeyRaw);
nextCheckedKeys.add(newKeyRaw);
checkedKeys.value = nextCheckedKeys;
}
if (useFlatKeySearchRows.value) {
treeKeys.value = [];
treeIndex = null;
refreshSelectedGroupLeafCounts();
} else {
rebuildTree(false);
}
}
function redisValueToKeyInfo(value: RedisValue): RedisKeyInfo {
return {
key_display: value.key_display,
@@ -2093,7 +2125,19 @@ defineExpose({ focusSearch, insertCommand, executeCommand: executeAiCommand });
</div>
<TabsContent value="detail" class="m-0 min-h-0 flex-1 flex flex-col">
<RedisValueViewer v-if="selectedKey" ref="valueViewerRef" :key="selectedKey.key_raw" :connection-id="connectionId" :db="db" :key-display="selectedKey.key_display" :key-raw="selectedKey.key_raw" :metadata="selectedKey" @deleted="onKeyDeleted" @loaded="onKeyLoaded" />
<RedisValueViewer
v-if="selectedKey"
ref="valueViewerRef"
:key="selectedKey.key_raw"
:connection-id="connectionId"
:db="db"
:key-display="selectedKey.key_display"
:key-raw="selectedKey.key_raw"
:metadata="selectedKey"
@deleted="onKeyDeleted"
@renamed="onKeyRenamed"
@loaded="onKeyLoaded"
/>
<div v-else class="flex-1 flex items-center justify-center text-xs text-muted-foreground">
{{ t("redis.selectKeyForDetail") }}
</div>
@@ -55,6 +55,7 @@ import TextContentSearchBar from "@/components/common/TextContentSearchBar.vue";
import { decodeJsonUnicodeEscapes, formatJsonSource, mapDisplayToRaw } from "@/lib/common/safeJsonFormat";
import { unixSecondsToCalendarDateTime } from "@/components/ui/date-time-picker/dateTimePicker";
import { applyRedisExpiryPolicy, type RedisExpiryMode, redisExpiryModeForTtl, validateRedisExpiry } from "@/lib/redis/redisExpiry";
import { redisKeyRawToText, redisKeyTextToDisplay, redisKeyTextToRaw } from "@/lib/redis/redisCommandSession";
const { t, locale } = useI18n();
const { toast } = useToast();
@@ -74,7 +75,7 @@ const redisExpiryTransport = {
setExpireAt: api.redisSetExpireAt,
};
const emit = defineEmits<{ deleted: [keyRaw: string]; loaded: [value: RedisValue] }>();
const emit = defineEmits<{ deleted: [keyRaw: string]; loaded: [value: RedisValue]; renamed: [oldKeyRaw: string, newKeyRaw: string, newKeyDisplay: string] }>();
const REDIS_JSON_WRAP_STORAGE_KEY = "dbx-redis-json-word-wrap";
const REDIS_JSON_UNICODE_MODE_STORAGE_KEY = "dbx-redis-json-unicode-mode";
@@ -90,6 +91,12 @@ const REDIS_STREAM_MIN_ROW_HEIGHT = 96;
const data = ref<RedisValue | null>(null);
const loading = ref(false);
const loadingMore = ref(false);
const showRenameKeyDialog = ref(false);
const renamingKey = ref(false);
const renameKeyName = ref("");
const renameKeyError = ref("");
const renameKeyInitialName = ref("");
const renameKeyInitialRaw = ref("");
let loadRequestId = 0;
const streamTab = ref<"entries" | "groups">("entries");
const streamEntries = ref<RedisStreamEntry[]>([]);
@@ -1474,6 +1481,50 @@ function requestDeleteKey() {
showDeleteConfirm.value = true;
}
function openRenameKeyDialog() {
if (!data.value || renamingKey.value) return;
// Display text escapes backslashes and binary bytes. Prefer the original
// UTF-8 text when available so confirming an unchanged name is a true no-op.
renameKeyName.value = redisKeyRawToText(props.keyRaw) ?? data.value.key_display;
renameKeyInitialName.value = renameKeyName.value;
renameKeyInitialRaw.value = props.keyRaw;
renameKeyError.value = "";
showRenameKeyDialog.value = true;
}
function handleRenameKeyDialogOpenChange(open: boolean) {
if (renamingKey.value) return;
showRenameKeyDialog.value = open;
if (!open) renameKeyError.value = "";
}
async function renameKey() {
if (renamingKey.value) return;
if (!renameKeyName.value) {
renameKeyError.value = t("redis.renameKeyNameRequired");
return;
}
const newKeyRaw = renameKeyName.value === renameKeyInitialName.value ? renameKeyInitialRaw.value : redisKeyTextToRaw(renameKeyName.value);
if (newKeyRaw === props.keyRaw) {
showRenameKeyDialog.value = false;
return;
}
renamingKey.value = true;
renameKeyError.value = "";
try {
await api.redisRenameKey(props.connectionId, props.db, props.keyRaw, newKeyRaw);
toast(t("redis.renameKeySuccess", { oldName: props.keyDisplay, newName: renameKeyName.value }), 2000);
showRenameKeyDialog.value = false;
emit("renamed", props.keyRaw, newKeyRaw, redisKeyTextToDisplay(renameKeyName.value));
} catch (error) {
renameKeyError.value = error instanceof Error ? error.message : String(error);
} finally {
renamingKey.value = false;
}
}
async function copyValue() {
if (!data.value) return;
// Copy the decompressed text while the Decompressed view is showing it.
@@ -2497,6 +2548,7 @@ defineExpose({ focusSearch });
</div>
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('grid.copyValue')" :aria-label="t('grid.copyValue')" @click="copyValue"><Copy class="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('redis.copyInsertStatement')" :aria-label="t('redis.copyInsertStatement')" @click="copyInsertStatement"><ClipboardCopy class="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('redis.renameKey')" :aria-label="t('redis.renameKey')" @click="openRenameKeyDialog"><Pencil class="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-destructive" @click="requestDeleteKey"><Trash2 class="h-3.5 w-3.5" /></Button>
</div>
@@ -3184,6 +3236,27 @@ defineExpose({ focusSearch });
<DangerConfirmDialog v-model:open="showDeleteConfirm" :message="t('dangerDialog.deleteMessage')" :details="deleteDetails" :confirm-label="t('dangerDialog.deleteConfirm')" @confirm="confirmDelete" />
<Dialog :open="showRenameKeyDialog" @update:open="handleRenameKeyDialogOpenChange">
<DialogContent class="sm:max-w-md" :show-close-button="!renamingKey" :style="editorFontFamilyStyle">
<DialogHeader>
<DialogTitle>{{ t("redis.renameKey") }}</DialogTitle>
</DialogHeader>
<label class="grid gap-1.5 text-xs font-medium">
<span>{{ t("redis.renameKeyName") }}</span>
<Input v-model="renameKeyName" class="dbx-editor-font-family" :disabled="renamingKey" autofocus spellcheck="false" @keydown.enter.prevent="renameKey" />
</label>
<p v-if="renameKeyError" class="text-xs text-destructive">{{ renameKeyError }}</p>
<DialogFooter>
<Button variant="ghost" :disabled="renamingKey" @click="showRenameKeyDialog = false">{{ t("dangerDialog.cancel") }}</Button>
<Button :disabled="renamingKey || !renameKeyName" @click="renameKey">
<Loader2 v-if="renamingKey" class="mr-1 h-3.5 w-3.5 animate-spin" />
<Pencil v-else class="mr-1 h-3.5 w-3.5" />
{{ t("redis.renameKey") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog :open="showHashFieldTtlDialog" @update:open="handleHashFieldTtlOpenChange">
<DialogContent class="w-[calc(100vw-2rem)] sm:max-w-[460px]">
<DialogHeader>