import { useEffect, useMemo, useState } from "react"; import { Link, useLocation, useParams } from "react-router-dom"; import { LuArrowLeft, LuPlay, LuSave } from "react-icons/lu"; import { errorMessage } from "../api/client.js"; import { useDryRunScript, useOwners, useSaveScript, useScript, } from "../api/hooks.js"; import { CodeEditor } from "../components/CodeEditor.jsx"; import { LogViewer } from "../components/LogViewer.jsx"; import { ScriptMetaPanel } from "../components/ScriptMetaPanel.jsx"; import { StatusBadge } from "../lib/format.jsx"; import { DEFAULT_INPUT_CONTEXT, NEW_SCRIPT_TEMPLATE, contextFromMeta, prettyJson, } from "../lib/script.js"; export function ScriptDryRunPage() { const { name: rawName } = useParams(); const name = decodeURIComponent(rawName ?? ""); const location = useLocation(); const stateContent = location.state?.content; const existing = useScript(name, stateContent == null); const dryRun = useDryRunScript(); const save = useSaveScript(); const { data: owners = [] } = useOwners(); const [owner, setOwner] = useState("default"); const [content, setContent] = useState(""); const [inputJson, setInputJson] = useState(DEFAULT_INPUT_CONTEXT); const [inputTouched, setInputTouched] = useState(false); const [outputJson, setOutputJson] = useState(""); const [logs, setLogs] = useState([]); const [runStatus, setRunStatus] = useState(null); const [parseError, setParseError] = useState(null); const [contentReady, setContentReady] = useState(false); useEffect(() => { if (stateContent != null) { setContent(stateContent); setContentReady(true); return; } if (existing.isLoading) return; if (existing.data?.content != null) { setContent(existing.data.content); setContentReady(true); return; } if (existing.isError) { setContent(NEW_SCRIPT_TEMPLATE); setContentReady(true); } }, [stateContent, existing.data, existing.isLoading, existing.isError]); const backHref = `/scripts/${encodeURIComponent(name)}/edit`; const lastRun = dryRun.data; const meta = lastRun?.meta ?? existing.data?.meta ?? null; const metaError = lastRun?.metaError ?? existing.data?.metaError ?? null; useEffect(() => { if (inputTouched) return; if (!existing.data?.meta) return; setInputJson(prettyJson(contextFromMeta(existing.data.meta))); }, [existing.data?.meta, inputTouched]); useEffect(() => { if (!lastRun) return; setRunStatus(lastRun.status); setLogs(lastRun.logs ?? []); if (lastRun.status === "success") { setOutputJson(prettyJson(lastRun.output)); } else { setOutputJson(prettyJson({ error: lastRun.error ?? "run failed" })); } }, [lastRun]); const runDisabledReason = useMemo(() => { if (!contentReady) return "loading script"; if (!name) return "missing script name"; if (dryRun.isPending) return "running"; return null; }, [contentReady, name, dryRun.isPending]); function parseInputContext() { let parsed; try { parsed = JSON.parse(inputJson); } catch (err) { throw new Error(err instanceof Error ? err.message : "invalid JSON"); } if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error('input context must be a JSON object with "data" and/or "config"'); } return { data: "data" in parsed ? parsed.data : null, config: "config" in parsed ? parsed.config : null, }; } function onRun() { setParseError(null); let ctx; try { ctx = parseInputContext(); } catch (err) { setParseError(err instanceof Error ? err.message : String(err)); return; } dryRun.mutate({ name, content, data: ctx.data, config: ctx.config, owner, }); } function onSave() { save.mutate({ name, content }); } return (
{parseError}
) : null} {dryRun.isError ? ({errorMessage(dryRun.error)}
) : null} {save.isError ? ({errorMessage(save.error)}
) : null} {save.isSuccess ? (Script saved
) : null}