import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { KeyRound, Trash2, Link2, FileCog } from "lucide-react"; import { toast } from "sonner"; import { Badge, Button, Card, Input } from "@/components/ui"; import { secretsApi, type SecretEntry, type SecretKind } from "@/api/secrets"; import { editorApi } from "@/api/editor"; import { apiErrorMessage } from "@/api/client"; /** * Per-stack file-based secrets & configs. Create stores a file in the stack * dir; attach references it from a service in the compose file (redeploy to * apply). Admin-only on the backend. */ export function SecretsPanel({ stackId, yaml, agentId, isAdmin, onChanged, }: { stackId: string; yaml: string; agentId?: number; isAdmin: boolean; onChanged?: () => void; }) { const qc = useQueryClient(); const key = ["secrets", agentId ?? "local", stackId]; const list = useQuery({ queryKey: key, queryFn: () => secretsApi.list(stackId, agentId), enabled: isAdmin, }); const services = useQuery({ queryKey: ["editor-services", stackId, agentId, yaml.length], queryFn: () => editorApi.services(yaml), enabled: isAdmin, }); const [kind, setKind] = useState("secret"); const [name, setName] = useState(""); const [content, setContent] = useState(""); const invalidate = () => qc.invalidateQueries({ queryKey: key }); const create = useMutation({ mutationFn: () => secretsApi.write(stackId, { kind, name: name.trim(), content }, agentId), onSuccess: () => { toast.success(`${kind} "${name}" saved`); setName(""); setContent(""); invalidate(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const remove = useMutation({ mutationFn: (s: SecretEntry) => secretsApi.remove(stackId, s.kind, s.name, agentId), onSuccess: () => { toast.success("Deleted"); invalidate(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const attach = useMutation({ mutationFn: (v: { s: SecretEntry; service: string; target?: string }) => secretsApi.attach(stackId, { kind: v.s.kind, name: v.s.name, service: v.service, target: v.target }, agentId), onSuccess: () => { toast.success("Attached — redeploy the stack to apply"); onChanged?.(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const detach = useMutation({ mutationFn: (v: { s: SecretEntry; service: string }) => secretsApi.detach(stackId, { kind: v.s.kind, name: v.s.name, service: v.service }, agentId), onSuccess: () => { toast.success("Detached — redeploy the stack to apply"); onChanged?.(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); if (!isAdmin) return (

Secrets are visible to admins only.

); const items = list.data ?? []; const svcList = services.data ?? []; return (
New secret / config