diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index 0fd310d0a0..51f01f187a 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -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(() => { + diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index dc2172eec6..3211af0ffc 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index d64b291aa9..fd1ca7ec86 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1270,6 +1270,8 @@ export default { viewMember: "查看完整内容", copyMember: "复制成员", copied: "已复制", + copyInsertStatement: "复制插入语句", + copyInsertStatementBinary: "二进制数据无法生成插入语句", editMember: "编辑", jsonView: "JSON 视图", rawContent: "原始内容",