diff --git a/frontend/src/components/fleet/secrets/SecretBundleSheet.tsx b/frontend/src/components/fleet/secrets/SecretBundleSheet.tsx index d4b10925..c747547d 100644 --- a/frontend/src/components/fleet/secrets/SecretBundleSheet.tsx +++ b/frontend/src/components/fleet/secrets/SecretBundleSheet.tsx @@ -1,9 +1,10 @@ import { useEffect, useMemo, useState } from 'react'; -import { Eye, EyeOff, Plus, Trash2, Copy, Loader2 } from 'lucide-react'; +import { Eye, EyeOff, Plus, Trash2, Copy, Loader2, Download } from 'lucide-react'; import { SystemSheet, SheetSection, type SystemSheetTab } from '@/components/ui/system-sheet'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { toast } from '@/components/ui/toast-store'; +import { useNodes } from '@/context/NodeContext'; import { type SecretSummary, type SecretVersionSummary, @@ -11,6 +12,7 @@ import { updateSecret, getSecret, listSecretVersions, + importFromStack, } from '@/lib/secretsApi'; import { copyToClipboard } from '@/lib/clipboard'; @@ -58,6 +60,7 @@ function rowsToKv(rows: KvRow[]): { kv: Record } | { error: stri export function SecretBundleSheet({ open, onOpenChange, secret, onSaved }: Props) { const mode: Mode = secret ? 'edit' : 'create'; + const { nodes } = useNodes(); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [rows, setRows] = useState([]); @@ -67,6 +70,11 @@ export function SecretBundleSheet({ open, onOpenChange, secret, onSaved }: Props const [loading, setLoading] = useState(false); const [versions, setVersions] = useState([]); const [versionsLoading, setVersionsLoading] = useState(false); + const [importOpen, setImportOpen] = useState(false); + const [importNodeId, setImportNodeId] = useState(null); + const [importStackName, setImportStackName] = useState(''); + const [importEnvFile, setImportEnvFile] = useState('.env'); + const [importing, setImporting] = useState(false); useEffect(() => { if (!open) return; @@ -94,6 +102,10 @@ export function SecretBundleSheet({ open, onOpenChange, secret, onSaved }: Props setNote(''); setActiveTab('keys'); } + setImportOpen(false); + setImportNodeId(null); + setImportStackName(''); + setImportEnvFile('.env'); return () => { cancelled = true; }; }, [open, secret]); @@ -173,6 +185,56 @@ export function SecretBundleSheet({ open, onOpenChange, secret, onSaved }: Props } } + async function handleImportFromStack() { + if (!secret) return; + if (importNodeId === null) { + toast.error('Pick a node'); + return; + } + const stackName = importStackName.trim(); + if (!stackName) { + toast.error('Stack name is required'); + return; + } + const envFile = importEnvFile.trim() || '.env'; + const nodeId = importNodeId; + setImporting(true); + try { + const { kv } = await importFromStack(secret.id, { + nodeId, + stackName, + envFileBasename: envFile, + }); + const incomingKeys = Object.keys(kv); + if (incomingKeys.length === 0) { + toast.error(`No keys found in ${envFile} on the selected stack`); + return; + } + setRows((prev) => { + const incomingSet = new Set(incomingKeys); + const seenInRows = new Set(); + const next = prev.map((r) => { + const k = r.key.trim(); + if (k) seenInRows.add(k); + return k && incomingSet.has(k) ? { ...r, value: kv[k] } : r; + }); + for (const k of incomingKeys) { + if (!seenInRows.has(k)) { + next.push({ id: makeRowId(), key: k, value: kv[k], reveal: false }); + } + } + return next; + }); + const nodeName = nodes.find((n) => n.id === nodeId)?.name ?? `node ${nodeId}`; + toast.success(`Imported ${incomingKeys.length} ${incomingKeys.length === 1 ? 'key' : 'keys'} from ${nodeName}/${stackName}`); + setImportOpen(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to import env from stack'); + } finally { + setImporting(false); + } + } + return ( ))} - +
+ + {mode === 'edit' && ( + + )} +
+ {mode === 'edit' && importOpen && ( +
+

+ Read an env file from a deployed stack · existing keys overlay, new keys append +

+
+
+ + +
+
+ + setImportStackName(e.target.value)} + placeholder="my-app" + className="font-mono" + /> +
+
+ + setImportEnvFile(e.target.value)} + placeholder=".env" + className="font-mono" + /> +
+
+
+ + +
+
+ )}