feat(redis): add copy insert statement button for Redis key value migration

Generates a Redis insert command (SET/RPUSH/SADD/ZADD/HSET/XADD/JSON.SET) from
the current key's value and copies it to clipboard. Supports TTL, pagination
hints, and binary value handling.
This commit is contained in:
t8y2
2026-06-05 12:15:53 +08:00
parent 6dadb4ddb7
commit ea45e1c331
3 changed files with 114 additions and 1 deletions
@@ -2,7 +2,20 @@
import { computed, ref, onBeforeUnmount, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { DynamicScroller, DynamicScrollerItem, RecycleScroller } from "vue-virtual-scroller";
import { Braces, Copy, Eye, FileText, Trash2, Save, RefreshCw, Plus, Loader2, Pencil, WrapText } from "@lucide/vue";
import {
Braces,
Copy,
Eye,
FileText,
Terminal,
Trash2,
Save,
RefreshCw,
Plus,
Loader2,
Pencil,
WrapText,
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -298,6 +311,94 @@ async function copyText(text: string) {
}
}
function escapeRedisArg(val: string): string {
return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
}
function generateInsertStatements(): string | null {
if (!data.value) return null;
if (data.value.value_is_binary) return null;
const key = data.value.key_display;
const type = data.value.key_type;
const commands: string[] = [];
const isCollection = ["list", "set", "zset", "hash"].includes(type);
if (isCollection) {
const total = data.value.total;
const loaded = collectionItems.value.length;
if (total != null && total > loaded) {
commands.push(`-- Note: Only ${loaded} of ${total} items included`);
}
}
switch (type) {
case "string":
commands.push(`SET ${escapeRedisArg(key)} ${escapeRedisArg(String(data.value.value))}`);
break;
case "list": {
const items = collectionItems.value.map((v) => escapeRedisArg(String(v))).join(" ");
commands.push(`RPUSH ${escapeRedisArg(key)} ${items}`);
break;
}
case "set": {
const members = collectionItems.value.map((v) => escapeRedisArg(String(v))).join(" ");
commands.push(`SADD ${escapeRedisArg(key)} ${members}`);
break;
}
case "zset": {
const pairs = collectionItems.value.map((v) => `${v.score} ${escapeRedisArg(String(v.member))}`).join(" ");
commands.push(`ZADD ${escapeRedisArg(key)} ${pairs}`);
break;
}
case "hash": {
const pairs = collectionItems.value
.map((v) => `${escapeRedisArg(String(v.field))} ${escapeRedisArg(String(v.value))}`)
.join(" ");
commands.push(`HSET ${escapeRedisArg(key)} ${pairs}`);
break;
}
case "stream": {
const entries = Array.isArray(data.value.value) ? data.value.value : [];
for (const entry of entries) {
const fields = Object.entries((entry as any).fields ?? {})
.map(([f, v]) => `${escapeRedisArg(f)} ${escapeRedisArg(String(v))}`)
.join(" ");
commands.push(`XADD ${escapeRedisArg(key)} * ${fields}`);
}
break;
}
default:
if (type === "ReJSON-RL" || type === "JSON") {
const json = JSON.stringify(data.value.value);
commands.push(`JSON.SET ${escapeRedisArg(key)} $ '${json}'`);
}
break;
}
if (data.value.ttl > 0) {
commands.push(`EXPIRE ${escapeRedisArg(key)} ${data.value.ttl}`);
}
return commands.join("\n");
}
async function copyInsertStatement() {
if (!data.value) return;
if (data.value.value_is_binary) {
toast(t("redis.copyInsertStatementBinary"), 3000);
return;
}
const stmt = generateInsertStatements();
if (!stmt) return;
try {
await copyToClipboard(stmt);
toast(t("redis.copyInsertStatement"), 2000);
} catch (e: any) {
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
}
}
function copyMember(value: unknown) {
void copyText(formatRedisMemberDetail(value).text);
}
@@ -623,6 +724,14 @@ onBeforeUnmount(() => {
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" @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')"
@click="copyInsertStatement"
><Terminal 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>
+2
View File
@@ -1295,6 +1295,8 @@ export default {
viewMember: "View full value",
copyMember: "Copy member",
copied: "Copied",
copyInsertStatement: "Copy Insert Statement",
copyInsertStatementBinary: "Cannot generate insert statement for binary data",
editMember: "Edit",
jsonView: "JSON view",
rawContent: "Raw content",
+2
View File
@@ -1270,6 +1270,8 @@ export default {
viewMember: "查看完整内容",
copyMember: "复制成员",
copied: "已复制",
copyInsertStatement: "复制插入语句",
copyInsertStatementBinary: "二进制数据无法生成插入语句",
editMember: "编辑",
jsonView: "JSON 视图",
rawContent: "原始内容",