import { useQuery } from "@tanstack/react-query"
import { Link } from "@tanstack/react-router"
import { ChevronDown, ChevronRight, Download, RotateCcw } from "lucide-react"
import { useState } from "react"
import type { ArtifactRow, RunNodeRow } from "@/client"
import { ValuePreview } from "@/components/Flow/ValuePreview"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import useCustomToast from "@/hooks/useCustomToast"
import { ago, cn, dur, si } from "@/lib/utils"
import { MetricPicker, RunMetricChart, useMetricNames } from "./MetricChart"
import { OpenInDashboard } from "./OpenInDashboard"
import {
CARD,
curveLost,
downloadArtifact,
FLOW_GONE,
isLive,
paramText,
runQueryOptions,
shortCommit,
shortId,
tookDefault,
useCancelRun,
useFlowInputs,
useRetryRun,
} from "./queries"
import { NodeStatusBadge, RunStatusBadge, statusReason } from "./RunStatus"
const LABEL = "text-muted-foreground text-xs"
export function RunDetail({ id }: { id: string }) {
const { data: run, isPending } = useQuery(runQueryOptions(id))
const cancel = useCancelRun()
const retry = useRetryRun()
const names = useMetricNames(id)
const { declared: current, gone } = useFlowInputs(run?.flow)
const [metric, setMetric] = useState("")
if (isPending || !run) return
const shown = metric && names.includes(metric) ? metric : (names[0] ?? "")
const nodes = run.nodes ?? []
const artifacts = run.artifacts ?? []
const reason = statusReason(run)
// A restored node's curve comes from the run it was restored from, so it
// goes when that run is deleted. Said once, where it applies.
const lost = nodes.filter((node) => node.curve_lost)
// What the flow declared when this run was submitted. A run from before
// that was kept reads against the flow's current declarations.
const declared = run.declared
? new Map(Object.entries(run.declared))
: current
// Declared order first, so the panel reads the way the flow does; anything
// the run carries that the flow no longer declares still shows, since it is
// what the run was actually given.
const fed = [
...[...declared.keys()].map((name) => ({
name,
value: name in run.params ? run.params[name] : declared.get(name),
fromFlow: tookDefault(run, name),
})),
...Object.keys(run.params)
.filter((name) => !declared.has(name))
.map((name) => ({
name,
value: run.params[name],
fromFlow: tookDefault(run, name),
})),
]
return (
{run.flow}
{shortId(run.id)}
{run.group_id && (
in a sweep
)}
{run.draft && (
ran the working copy
)}
{gone && (
flow deleted
)}
{isLive(run.status) ? (
) : (
)}
{reason &&
{reason}
}
{gone &&
{FLOW_GONE}
}
{ago(String(run.created_at ?? ""))}{run.duration_ms ? dur(run.duration_ms) : "—"}{run.actor || "—"}{run.cause}{run.seed ?? "—"}
{/* The user's own repository for a code-declared flow; the store's
commit is a generated shim and says less. */}
{shortCommit(run.origin_commit || run.commit || "") || "—"}
{run.labels.join(", ") || "—"}
{shortId(run.params_digest || "")}
{/* What the run actually ran with, which is not the same list as what it
was passed: a flow's inputs *are* its parameters, and one left alone
took the flow's own value. Both are drawn, and which is which is
marked — reading a number off a chart is worth nothing if the other
half of the setting is invisible. */}
Inputs
{fed.length === 0 ? (
{gone && !run.declared
? "The run carried no parameters of its own, and its flow is gone, so what it took from the flow cannot be read back."
: "This flow declares no inputs, so there was nothing to choose."}
)
}
/**
* One named value: read directly when it is a scalar, unfolded when it is not.
*
* The same disclosure a node's ports use on the canvas — a run's report is a
* record like any other, and serialising it onto one truncated line answers
* nothing.
*/
export function Entry({
name,
value,
note,
}: {
name: string
value: unknown
/** Where the value came from, when it was not this run. */
note?: string
}) {
const structured = value !== null && typeof value === "object"
return (
)
}
/** What each node did, with its logs and its traceback behind a disclosure. */
function NodesTable({ nodes }: { nodes: RunNodeRow[] }) {
const [open, setOpen] = useState(null)
if (nodes.length === 0) return null
return (