import { Check, CheckCircle2, CircleAlert, Copy, FileJson, Loader2, Play, X } from "lucide-react"; import type { ReactNode } from "react"; import { useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { api, errorMessage } from "../api"; import type { CommandResult } from "../types"; import { Alert, JsonBlock } from "./ui"; type ActionTone = "neutral" | "warn" | "danger"; export function ActionButton({ label, path, payload, icon, compact = false, tone = "danger", disabled = false, onDone, onError, secretField }: { label: string; path: string; payload: () => Record; icon?: ReactNode; compact?: boolean; tone?: ActionTone; // disabled keeps a form from opening the confirm flow at all while its own // validation is unhappy, so the operator fixes the field instead of reading a // backend rejection. disabled?: boolean; onDone?: () => void; // onError lets a page react to a failure the operator cannot fix by editing the // form — an optimistic-locking 409, say — and replace the raw backend text with // an explanation by returning it. onError?: (error: unknown) => string | undefined; // secretField names a key in result.details that holds a one-time secret // (e.g. a bot token) -- when present, it's pulled out of the generic JSON // dump and rendered instead as its own copy-to-clipboard callout, so it // doesn't get lost among the other fields. secretField?: string; }) { const [open, setOpen] = useState(false); const [reason, setReason] = useState(""); const [result, setResult] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [secretCopied, setSecretCopied] = useState(false); function reset() { setReason(""); setResult(null); setError(""); setSecretCopied(false); } async function run(confirm: boolean) { if (!reason.trim()) { setError("Please enter an operation reason"); return; } setBusy(true); setError(""); try { const body = { ...payload(), reason, confirm }; const commandResult = await api.action(path, body); setResult(commandResult); if (confirm) { onDone?.(); } } catch (err) { setError(onError?.(err) || errorMessage(err)); } finally { setBusy(false); } } const canConfirm = result?.dry_run && !result.error; const triggerClass = `btn ${tone === "danger" ? "danger" : tone === "warn" ? "warn" : ""} ${compact ? "compact-btn" : ""}`; const previewPayload = useMemo(() => { try { return payload(); } catch (err) { return { payload_error: errorMessage(err) }; } }, [open, payload]); const secretValue = secretField && result?.details && typeof result.details[secretField] === "string" ? (result.details[secretField] as string) : ""; const visibleDetails = secretValue && result?.details ? Object.fromEntries(Object.entries(result.details).filter(([key]) => key !== secretField)) : result?.details; async function copySecret() { await navigator.clipboard.writeText(secretValue); setSecretCopied(true); } return ( <> {open && createPortal(
{"Action Flow"}

{label}

1{"Enter reason"}
2{"Dry-run check"}
3{"Confirm execution"}