diff --git a/dev_server.log b/dev_server.log new file mode 100644 index 0000000..e66ede1 --- /dev/null +++ b/dev_server.log @@ -0,0 +1,6 @@ +$ vite --host 0.0.0.0 --port 3000 + + VITE v5.4.21 ready in 302 ms + + ➜ Local: http://localhost:3000/ + ➜ Network: http://192.168.0.2:3000/ diff --git a/frontend/src/flow/GlobalsManager.tsx b/frontend/src/flow/GlobalsManager.tsx index 778dc6d..1aa4338 100644 --- a/frontend/src/flow/GlobalsManager.tsx +++ b/frontend/src/flow/GlobalsManager.tsx @@ -32,6 +32,138 @@ export function GlobalsManager({ ); const [showSecretMap, setShowSecretMap] = useState>({}); + const [searchQuery, setSearchQuery] = useState(""); + const [importText, setImportText] = useState(""); + const [importError, setImportError] = useState(null); + + const filteredItems = useMemo(() => { + const q = searchQuery.trim().toLowerCase(); + if (!q) return items; + return items.filter((item) => { + return ( + item.key.toLowerCase().includes(q) || + item.value.toLowerCase().includes(q) + ); + }); + }, [items, searchQuery]); + + const handleClearAllGlobals = () => { + if (globals.length === 0) return; + if (!confirm("Are you sure you want to delete ALL global variables? This action cannot be undone.")) return; + onGlobalsChange([]); + setSelectedId(secrets[0]?.id ?? null); + }; + + const handleClearAllSecrets = () => { + if (secrets.length === 0) return; + if (!confirm("Are you sure you want to delete ALL secrets? This action cannot be undone.")) return; + onSecretsChange([]); + setSelectedId(globals[0]?.id ?? null); + }; + + const handleExportAll = () => { + const env = { + globals: globals.map(({ key, value }) => ({ key, value })), + secrets: secrets.map(({ key, value }) => ({ key, value })), + }; + const data = JSON.stringify(env, null, 2); + navigator.clipboard.writeText(data).then(() => { + alert("Environment variables copied to clipboard!"); + }); + }; + + const handleImportAll = (replace: boolean) => { + setImportError(null); + if (!importText.trim()) { + setImportError("Please paste a JSON environment configuration first."); + return; + } + try { + const parsed = JSON.parse(importText); + if (typeof parsed !== "object" || parsed === null) { + throw new Error("Input must be a JSON object containing 'globals' and/or 'secrets'."); + } + + const importedGlobals: GlobalVar[] = []; + const importedSecrets: SecretVar[] = []; + + if (parsed.globals) { + if (!Array.isArray(parsed.globals)) { + throw new Error("'globals' must be an array."); + } + parsed.globals.forEach((g: any, index: number) => { + if (!g || typeof g !== "object" || typeof g.key !== "string") { + throw new Error(`Invalid global variable definition at index ${index}`); + } + importedGlobals.push({ + id: cryptoId(), + key: g.key, + value: typeof g.value === "string" ? g.value : JSON.stringify(g.value), + }); + }); + } + + if (parsed.secrets) { + if (!Array.isArray(parsed.secrets)) { + throw new Error("'secrets' must be an array."); + } + parsed.secrets.forEach((s: any, index: number) => { + if (!s || typeof s !== "object" || typeof s.key !== "string") { + throw new Error(`Invalid secret definition at index ${index}`); + } + importedSecrets.push({ + id: cryptoId(), + key: s.key, + value: typeof s.value === "string" ? s.value : JSON.stringify(s.value), + }); + }); + } + + if (importedGlobals.length === 0 && importedSecrets.length === 0) { + throw new Error("No variables found to import. Verify your JSON format."); + } + + if (replace) { + if (!confirm("Are you sure you want to REPLACE all current globals and secrets with the imported ones?")) { + return; + } + onGlobalsChange(importedGlobals); + onSecretsChange(importedSecrets); + setSelectedId(importedGlobals[0]?.id ?? importedSecrets[0]?.id ?? null); + } else { + // Merge-import + const nextGlobals = [...globals]; + importedGlobals.forEach((ig) => { + const idx = nextGlobals.findIndex((g) => g.key.toLowerCase() === ig.key.toLowerCase()); + if (idx >= 0) { + nextGlobals[idx] = ig; + } else { + nextGlobals.push(ig); + } + }); + + const nextSecrets = [...secrets]; + importedSecrets.forEach((is) => { + const idx = nextSecrets.findIndex((s) => s.key.toLowerCase() === is.key.toLowerCase()); + if (idx >= 0) { + nextSecrets[idx] = is; + } else { + nextSecrets.push(is); + } + }); + + onGlobalsChange(nextGlobals); + onSecretsChange(nextSecrets); + setSelectedId(importedGlobals[0]?.id ?? importedSecrets[0]?.id ?? null); + } + + setImportText(""); + alert("Environment successfully imported!"); + } catch (err) { + setImportError(err instanceof Error ? err.message : "Failed to parse JSON"); + } + }; + const selected = useMemo(() => { return items.find((item) => item.id === selectedId) ?? null; }, [items, selectedId]); @@ -164,13 +296,34 @@ export function GlobalsManager({ + {/* Sidebar Search */} +
+
+ / + setSearchQuery(e.target.value)} + placeholder="search variables…" + className="flex-1 bg-transparent py-1 font-mono text-[10px] text-[hsl(var(--ink))] placeholder:text-[hsl(var(--ink-faint))] outline-none" + /> + {searchQuery && ( + + )} +
+
+
- {items.length === 0 && ( + {filteredItems.length === 0 && (
- No globals or secrets configured yet. + No variables found.
)} - {items.map((item) => { + {filteredItems.map((item) => { const isSel = selectedId === item.id; const bad = !!validateKey(item.key); return ( @@ -202,6 +355,24 @@ export function GlobalsManager({ ); })}
+ + {/* Sidebar Bulk Clear Actions */} +
+ + +
{/* Editor Area */} @@ -211,7 +382,7 @@ export function GlobalsManager({

How to Use Environment Variables

-
+

Add workspace variables here to substitute secrets (API keys, passwords) and dynamic values in your graph without hardcoding.

@@ -234,6 +405,60 @@ export function GlobalsManager({ Placeholders are replaced dynamically during browser flow execution and exported code (Python & Javascript).

+ +

+ Backup & Portability +

+
+

+ Backup or share your workspace environment. API Keys/secrets values are included in plaintext in the JSON. +

+ + +
+ + Import Environment + +