import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, KeyRound, Loader2, Plus, X } from "lucide-react"; import type { CompanySecret, SecretVersionSelector } from "@paperclipai/shared"; import { secretsApi } from "../api/secrets"; import { queryKeys } from "../lib/queryKeys"; import { useCompany } from "../context/CompanyContext"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { cn } from "../lib/utils"; export interface SecretBindingValue { secretId: string; version?: SecretVersionSelector; } interface SecretBindingPickerProps { value: SecretBindingValue | null; onChange: (next: SecretBindingValue | null) => void; label?: string; placeholder?: string; allowVersionSelector?: boolean; emptyHint?: string; className?: string; disabled?: boolean; /** * Optional whitelist of secret statuses to show. Defaults to "active". * Pass null to disable the filter and show every secret in the company. */ statusFilter?: Array | null; } const VERSION_LATEST: SecretVersionSelector = "latest"; function describeSecret(secret: CompanySecret): string { const provider = secret.provider.replaceAll("_", " "); if (secret.managedMode === "external_reference") { return `External · ${provider}`; } return provider; } function statusTone(status: CompanySecret["status"]): string { switch (status) { case "active": return "text-emerald-600 dark:text-emerald-400"; case "disabled": return "text-amber-600 dark:text-amber-400"; case "archived": return "text-muted-foreground"; case "deleted": return "text-destructive"; default: return "text-muted-foreground"; } } export function SecretBindingPicker({ value, onChange, label = "Secret", placeholder = "Select secret", allowVersionSelector = true, emptyHint = "No matching secrets. Create one to bind it here.", className, disabled, statusFilter = ["active"], }: SecretBindingPickerProps) { const queryClient = useQueryClient(); const { selectedCompanyId } = useCompany(); const [createOpen, setCreateOpen] = useState(false); const [createName, setCreateName] = useState(""); const [createValue, setCreateValue] = useState(""); const [createDescription, setCreateDescription] = useState(""); const [createError, setCreateError] = useState(null); const secretsQuery = useQuery({ queryKey: selectedCompanyId ? queryKeys.secrets.list(selectedCompanyId) : ["secrets", "__disabled__"], queryFn: () => secretsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); const filteredSecrets = useMemo(() => { const all = secretsQuery.data ?? []; if (statusFilter === null) return all; return all.filter((secret) => statusFilter.includes(secret.status)); }, [secretsQuery.data, statusFilter]); const selectedSecret = useMemo(() => { if (!value) return null; return (secretsQuery.data ?? []).find((secret) => secret.id === value.secretId) ?? null; }, [secretsQuery.data, value]); const selectedMissing = Boolean(value && !selectedSecret); const createMutation = useMutation({ mutationFn: () => secretsApi.create(selectedCompanyId!, { name: createName.trim(), value: createValue, description: createDescription.trim() || null, }), onSuccess: (created) => { queryClient.invalidateQueries({ queryKey: queryKeys.secrets.list(selectedCompanyId!) }); onChange({ secretId: created.id, version: VERSION_LATEST }); setCreateOpen(false); setCreateName(""); setCreateValue(""); setCreateDescription(""); setCreateError(null); }, onError: (error) => { setCreateError(error instanceof Error ? error.message : "Failed to create secret"); }, }); const versionDisplay = (selector: SecretVersionSelector | undefined) => { if (selector === undefined || selector === VERSION_LATEST) return "latest"; return `v${selector}`; }; return (
{label ? (
{label} {value ? ( ) : null}
) : null}
{allowVersionSelector ? ( ) : null}
{selectedSecret ? (

{selectedSecret.status !== "active" ? `Status: ${selectedSecret.status}. ` : null} Bound to {versionDisplay(value?.version)} · {selectedSecret.key}

) : selectedMissing ? (

The previously selected secret is no longer available. Pick another or remove the binding.

) : (filteredSecrets.length === 0 && !secretsQuery.isPending) ? (

{emptyHint}

) : null} Create new secret
setCreateName(event.target.value)} placeholder="OPENAI_API_KEY" autoFocus />