diff --git a/app/src/app/(app)/runs/[runId]/page.tsx b/app/src/app/(app)/runs/[runId]/page.tsx index 9c4a421..f69a043 100644 --- a/app/src/app/(app)/runs/[runId]/page.tsx +++ b/app/src/app/(app)/runs/[runId]/page.tsx @@ -1,14 +1,23 @@ export const dynamic = "force-dynamic"; import type { Metadata } from "next"; +import Link from "next/link"; import { eq } from "drizzle-orm"; import { notFound } from "next/navigation"; +import { RotateCw } from "lucide-react"; import { getDb } from "@/lib/db/client"; import { runs } from "@/lib/db/schema"; import { getScript } from "@/lib/config/load"; import { readLogTail } from "@/lib/runner/log-file"; import { RunTerminal } from "@/components/runs/run-terminal"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { buttonVariants } from "@/components/ui/button"; +import { + Card, + CardAction, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; interface RunDetailPageProps { params: Promise<{ runId: string }>; @@ -61,6 +70,17 @@ export default async function RunDetailPage({ params }: RunDetailPageProps) { {run.scriptName} + {script && ( + + + + Re-run + + + )}
Triggered by
diff --git a/app/src/app/(app)/scripts/[scriptId]/page.tsx b/app/src/app/(app)/scripts/[scriptId]/page.tsx index d8a3ea5..384bd3f 100644 --- a/app/src/app/(app)/scripts/[scriptId]/page.tsx +++ b/app/src/app/(app)/scripts/[scriptId]/page.tsx @@ -1,9 +1,12 @@ export const dynamic = "force-dynamic"; import type { Metadata } from "next"; +import { eq } from "drizzle-orm"; import { notFound } from "next/navigation"; import { getScript } from "@/lib/config/load"; import { serializeScriptForClient } from "@/lib/config/serialize"; +import { getDb } from "@/lib/db/client"; +import { runs } from "@/lib/db/schema"; import { DynamicForm } from "@/components/forms/dynamic-form"; import { Card, @@ -15,6 +18,7 @@ import { interface ScriptPageProps { params: Promise<{ scriptId: string }>; + searchParams: Promise<{ fromRun?: string }>; } export async function generateMetadata({ @@ -25,13 +29,31 @@ export async function generateMetadata({ return { title: script?.name ?? "Script not found" }; } -export default async function ScriptPage({ params }: ScriptPageProps) { +export default async function ScriptPage({ + params, + searchParams, +}: ScriptPageProps) { const { scriptId } = await params; + const { fromRun } = await searchParams; const script = getScript(scriptId); if (!script) notFound(); const clientScript = serializeScriptForClient(script); + // Only trust a previous run's variables as prefill if it's actually a run of this same + // script - a `fromRun` id for a different script's run wouldn't line up with these variables. + let initialValues: Record | undefined; + if (fromRun) { + const previousRun = getDb() + .select({ scriptId: runs.scriptId, variables: runs.variables }) + .from(runs) + .where(eq(runs.id, fromRun)) + .get(); + if (previousRun?.scriptId === scriptId) { + initialValues = previousRun.variables; + } + } + return (
@@ -42,7 +64,7 @@ export default async function ScriptPage({ params }: ScriptPageProps) { )} - +
diff --git a/app/src/components/forms/dynamic-form.tsx b/app/src/components/forms/dynamic-form.tsx index 7a34518..b0ef0cb 100644 --- a/app/src/components/forms/dynamic-form.tsx +++ b/app/src/components/forms/dynamic-form.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Play, Loader2 } from "lucide-react"; +import { Play, Loader2, Info } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Form } from "@/components/ui/form"; @@ -19,22 +19,40 @@ function emptyValueFor(variable: ClientScript["variables"][number]): unknown { return ""; } -function defaultValuesFor(script: ClientScript): Record { +/** `initialValues` comes from a previous run's (already-redacted) variables when re-running - + * secret fields are deliberately excluded there (their stored value is just "***", not the real + * one), so those always fall through to the normal empty/default state and have to be re-entered. */ +function defaultValuesFor( + script: ClientScript, + initialValues?: Record, +): Record { const values: Record = {}; for (const variable of script.variables) { - values[variable.name] = variable.default ?? emptyValueFor(variable); + const fromPreviousRun = + initialValues && !variable.secret + ? initialValues[variable.name] + : undefined; + values[variable.name] = + fromPreviousRun ?? variable.default ?? emptyValueFor(variable); } return values; } -export function DynamicForm({ script }: { script: ClientScript }) { +export function DynamicForm({ + script, + initialValues, +}: { + script: ClientScript; + initialValues?: Record; +}) { const router = useRouter(); const [submitError, setSubmitError] = useState(null); const schema = buildVariableSchemaFromList(script.variables); + const hasSecretVariable = script.variables.some((v) => v.secret); const form = useForm({ resolver: zodResolver(schema), - defaultValues: defaultValuesFor(script), + defaultValues: defaultValuesFor(script, initialValues), }); async function onSubmit(values: Record) { @@ -67,6 +85,16 @@ export function DynamicForm({ script }: { script: ClientScript }) { {submitError} )} + {initialValues && ( + + + + Pre-filled from a previous run. + {hasSecretVariable && + " Secret fields aren't carried over and need to be entered again."} + + + )} {script.variables.length === 0 && (

This script takes no parameters.