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: "原始内容",