CardAction's default row-span-2 reserved header column 2 across both the title row and the metadata dl row below it, shrinking the dl's available width (cramping the Run ID column) to fit around the button. Scope the button to just the title row and let the dl span the full header width on its own row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
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 { buttonVariants } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardAction,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
|
|
interface RunDetailPageProps {
|
|
params: Promise<{ runId: string }>;
|
|
}
|
|
|
|
function formatVariableValue(value: unknown): string {
|
|
if (value === undefined || value === null || value === "") return "—";
|
|
if (Array.isArray(value)) return value.length ? value.join(", ") : "—";
|
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
return String(value);
|
|
}
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: RunDetailPageProps): Promise<Metadata> {
|
|
const { runId } = await params;
|
|
const run = getDb()
|
|
.select({ scriptName: runs.scriptName })
|
|
.from(runs)
|
|
.where(eq(runs.id, runId))
|
|
.get();
|
|
return { title: run ? `${run.scriptName} run` : "Run not found" };
|
|
}
|
|
|
|
export default async function RunDetailPage({ params }: RunDetailPageProps) {
|
|
const { runId } = await params;
|
|
const db = getDb();
|
|
const run = db.select().from(runs).where(eq(runs.id, runId)).get();
|
|
if (!run) notFound();
|
|
|
|
const { text: initialLog, size: initialLogBytes } = readLogTail(
|
|
run.logFilePath,
|
|
);
|
|
|
|
const script = getScript(run.scriptId);
|
|
const variableEntries = script
|
|
? script.variables.map((variable) => ({
|
|
key: variable.name,
|
|
label: variable.label ?? variable.name,
|
|
value: run.variables[variable.name],
|
|
}))
|
|
: Object.entries(run.variables).map(([key, value]) => ({
|
|
key,
|
|
label: key,
|
|
value,
|
|
}));
|
|
|
|
return (
|
|
<div className="mx-auto flex max-w-5xl flex-col gap-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{run.scriptName}</CardTitle>
|
|
{script && (
|
|
<CardAction className="row-span-1">
|
|
<Link
|
|
href={`/scripts/${run.scriptId}?fromRun=${run.id}`}
|
|
className={buttonVariants({ variant: "outline", size: "sm" })}
|
|
>
|
|
<RotateCw className="size-3.5" />
|
|
Re-run
|
|
</Link>
|
|
</CardAction>
|
|
)}
|
|
<dl className="text-muted-foreground col-span-2 grid grid-cols-2 gap-x-4 gap-y-2 text-xs sm:grid-cols-3">
|
|
<div>
|
|
<dt className="font-mono text-[0.7rem] font-medium tracking-widest uppercase">
|
|
Triggered by
|
|
</dt>
|
|
<dd className="font-mono">{run.triggeredBy}</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="font-mono text-[0.7rem] font-medium tracking-widest uppercase">
|
|
Started
|
|
</dt>
|
|
<dd className="font-mono">
|
|
{run.startedAt ? new Date(run.startedAt).toLocaleString() : "-"}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="font-mono text-[0.7rem] font-medium tracking-widest uppercase">
|
|
Run ID
|
|
</dt>
|
|
<dd className="truncate font-mono">{run.id}</dd>
|
|
</div>
|
|
</dl>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-4">
|
|
<div className="flex flex-col gap-1.5">
|
|
<span className="text-muted-foreground font-mono text-[0.7rem] font-medium tracking-widest uppercase">
|
|
Command
|
|
</span>
|
|
<pre className="bg-muted border-primary/30 overflow-x-auto rounded-md border-l-2 p-3 font-mono text-xs whitespace-pre">
|
|
{run.resolvedCommand}
|
|
</pre>
|
|
</div>
|
|
|
|
{variableEntries.length > 0 && (
|
|
<div className="flex flex-col gap-1.5">
|
|
<span className="text-muted-foreground font-mono text-[0.7rem] font-medium tracking-widest uppercase">
|
|
Variables
|
|
</span>
|
|
<dl className="grid gap-x-6 gap-y-2 rounded-md border p-3 text-xs sm:grid-cols-2 lg:grid-cols-3">
|
|
{variableEntries.map(({ key, label, value }) => (
|
|
<div key={key} className="flex flex-col gap-0.5">
|
|
<dt className="text-muted-foreground">{label}</dt>
|
|
<dd className="font-mono break-all">
|
|
{formatVariableValue(value)}
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
)}
|
|
|
|
<RunTerminal
|
|
runId={run.id}
|
|
initialStatus={run.status}
|
|
initialLog={initialLog}
|
|
initialLogBytes={initialLogBytes}
|
|
initialExitCode={run.exitCode}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|