"use client";
import { useState } from "react";
import { ChevronRightIcon } from "@/components/icons";
import type { StepStatus } from "@/lib/runs";
import { RunStatusIcon } from "./run-status";
export type StepView = {
index: number;
name: string;
status: StepStatus;
exitCode: number | null;
duration: string | null;
/** The output's last lines, already cleaned of terminal escapes. */
lines: string[];
/** How many lines before `lines` were left out. */
omitted: number;
};
/**
* A job's steps, each a row that opens onto its log. Running and failed steps open by themselves (and
* follow along as the page refreshes) until the reader opens or closes them by hand.
*/
export function JobSteps({ steps, rawHref }: { steps: StepView[]; rawHref: string }) {
const [toggled, setToggled] = useState<Map<number, boolean>>(new Map());
return (
<ol className="flex flex-col">
{steps.map((step) => {
const auto = step.status === "in_progress" || step.status === "failure";
const open = toggled.get(step.index) ?? auto;
const hasLog = step.lines.length > 0 || step.omitted > 0;
return (
<li key={step.index}>
<button
type="button"
aria-expanded={open}
onClick={() => setToggled((t) => new Map(t).set(step.index, !open))}
className="flex h-10 w-full items-center gap-2.5 rounded-row px-3 text-left transition-[background,box-shadow] duration-150 outline-none hover:recipe-row-list-hover focus-visible:focus-ring"
>
<ChevronRightIcon width={14} height={14} className={`shrink-0 text-ink3 transition-transform duration-150 ${open ? "rotate-90" : ""}`} />
<RunStatusIcon status={step.status} size={15} />
<span className={`min-w-0 flex-1 truncate type-ui ${step.status === "skipped" || step.status === "pending" ? "text-ink3" : "text-ink"}`}>
{step.name}
</span>
{step.duration && <span className="shrink-0 type-readout text-ink3">{step.duration}</span>}
</button>
{open && (
<div className="mx-3 mt-0.5 mb-2 overflow-hidden rounded-row well">
{hasLog ? (
<pre className="max-h-[560px] overflow-auto py-2 font-mono text-[12px] leading-5 text-ink">
{step.omitted > 0 && (
<div className="px-3 pb-1 text-ink3">
{step.omitted.toLocaleString("en")} earlier lines not shown ·{" "}
<a href={rawHref} className="underline underline-offset-2 hover:text-ink">
view the full log
</a>
</div>
)}
{step.lines.map((line, i) => (
<div key={i} className="flex hover:bg-[color-mix(in_srgb,var(--mu-ink)_4%,transparent)]">
<span className="w-12 shrink-0 pr-3 text-right text-ink3 select-none">{step.omitted + i + 1}</span>
<span className="min-w-0 pr-3 break-all whitespace-pre-wrap">{line || " "}</span>
</div>
))}
</pre>
) : (
<p className="px-3 py-2.5 type-meta text-ink3">
{step.status === "pending" ? "Not started yet." : step.status === "skipped" ? "This step was skipped." : "No output."}
</p>
)}
</div>
)}
</li>
);
})}
</ol>
);
}