Photograph the runs screen and a dashboard read against runs

This commit is contained in:
2026-08-25 12:13:58 +02:00
parent 77754936c6
commit 2840cc8e2b
6 changed files with 80 additions and 12 deletions
+3 -1
View File
@@ -82,6 +82,9 @@ class RunRow(BaseModel):
#: decorators. Empty for one drawn on the canvas, where `commit` is the #: decorators. Empty for one drawn on the canvas, where `commit` is the
#: whole answer to what produced the number. #: whole answer to what produced the number.
origin_commit: str = "" origin_commit: str = ""
#: The flow store's own commit. Short, unlike `result`, so the list
#: carries it: "what code produced this" is a question asked of a table.
commit: str = ""
seed: int | None seed: int | None
group_id: str | None group_id: str | None
labels: list[str] labels: list[str]
@@ -94,7 +97,6 @@ class RunRow(BaseModel):
class RunDetail(RunRow): class RunDetail(RunRow):
result: dict[str, Any] = Field(default_factory=dict) result: dict[str, Any] = Field(default_factory=dict)
commit: str = ""
flow_version: int = 1 flow_version: int = 1
nodes: list[RunNodeRow] = Field(default_factory=list) nodes: list[RunNodeRow] = Field(default_factory=list)
artifacts: list[ArtifactRow] = Field(default_factory=list) artifacts: list[ArtifactRow] = Field(default_factory=list)
+58 -1
View File
@@ -89,16 +89,73 @@ for (const theme of ["light", "dark"]) {
await captureFlows(page, dir) await captureFlows(page, dir)
await captureDashboards(page, dir) await captureDashboards(page, dir)
await captureRuns(page, dir)
await context.close() await context.close()
console.log( console.log(
` wrote ${dir}/{website-hero,app-login,app-dashboard,app-flows,app-flow-panel,app-panel}.png`, ` wrote ${dir}/{website-hero,app-login,app-dashboard,app-flows,app-flow-panel,app-panel,app-runs,app-run,app-run-context}.png`,
) )
} }
} }
await browser.close() await browser.close()
/**
* The experiment log: the table, one run in full, and a dashboard read against
* the runs someone picked. Skipped on an instance that has never run anything,
* where all three would photograph the same empty state.
*/
async function captureRuns(page, dir) {
await page.goto(`${APP_URL}/runs`, { waitUntil: "networkidle" })
await page.waitForTimeout(1000)
await page.screenshot({ path: `${dir}/app-runs.png` })
const rows = page.getByTestId("run-row")
if (!(await rows.count())) return
// Two runs compared, which is the whole point of the screen.
const boxes = page.getByTestId("run-select")
await boxes.nth(0).click()
if ((await boxes.count()) > 1) await boxes.nth(1).click()
await page.waitForTimeout(1500)
await page.screenshot({ path: `${dir}/app-runs-compare.png`, fullPage: true })
// A dashboard read against those two runs: the same page a live run is
// watched on, showing finished ones. This is the seam the feature exists for.
const picked = await page
.getByTestId("run-link")
.evaluateAll((links) => links.slice(0, 2).map((a) => a.getAttribute("href")))
const ids = picked
.map((href) => (href || "").split("/").pop())
.filter(Boolean)
// The API is its own host here; the SPA's origin does not proxy /api.
const apiUrl = APP_URL.replace("//app.", "//api.")
const results = await page.evaluate(async (base) => {
const answer = await fetch(`${base}/api/v1/dashboards/`, {
headers: { Authorization: `Bearer ${localStorage.getItem("access_token")}` },
})
if (!answer.ok) return null
const body = await answer.json()
return (body.data || [])
.map((one) => one.name)
.find((n) => n.endsWith("_results"))
}, apiUrl)
if (results && ids.length) {
await page.goto(`${APP_URL}/view/${results}?runs=${ids.join(",")}`, {
waitUntil: "networkidle",
})
await page.waitForTimeout(2000)
await page.screenshot({ path: `${dir}/app-run-context.png` })
}
await page.goto(`${APP_URL}/runs`, { waitUntil: "networkidle" })
await page.getByTestId("run-link").first().click()
await page.waitForURL(/\/runs\/.+/, { timeout: 15000 })
await page.waitForLoadState("networkidle")
await page.waitForTimeout(1500)
await page.screenshot({ path: `${dir}/app-run.png`, fullPage: true })
}
/** /**
* A dashboard as a wall panel sees it. Seeds one if the instance has none, so * A dashboard as a wall panel sees it. Seeds one if the instance has none, so
* the shot shows the grid rather than an empty-state message. * the shot shows the grid rather than an empty-state message.
+10 -5
View File
@@ -2491,6 +2491,11 @@ export const RunDetailSchema = {
title: 'Origin Commit', title: 'Origin Commit',
default: '' default: ''
}, },
commit: {
type: 'string',
title: 'Commit',
default: ''
},
seed: { seed: {
anyOf: [ anyOf: [
{ {
@@ -2542,11 +2547,6 @@ export const RunDetailSchema = {
type: 'object', type: 'object',
title: 'Result' title: 'Result'
}, },
commit: {
type: 'string',
title: 'Commit',
default: ''
},
flow_version: { flow_version: {
type: 'integer', type: 'integer',
title: 'Flow Version', title: 'Flow Version',
@@ -3619,6 +3619,11 @@ export const fluksio__api__routes__runs__RunRowSchema = {
title: 'Origin Commit', title: 'Origin Commit',
default: '' default: ''
}, },
commit: {
type: 'string',
title: 'Commit',
default: ''
},
seed: { seed: {
anyOf: [ anyOf: [
{ {
+2 -1
View File
@@ -426,6 +426,7 @@ export type fluksio__api__routes__runs__RunRow = {
}; };
params_digest: string; params_digest: string;
origin_commit?: string; origin_commit?: string;
commit?: string;
seed: (number | null); seed: (number | null);
group_id: (string | null); group_id: (string | null);
labels: Array<(string)>; labels: Array<(string)>;
@@ -898,6 +899,7 @@ export type RunDetail = {
}; };
params_digest: string; params_digest: string;
origin_commit?: string; origin_commit?: string;
commit?: string;
seed: (number | null); seed: (number | null);
group_id: (string | null); group_id: (string | null);
labels: Array<(string)>; labels: Array<(string)>;
@@ -909,7 +911,6 @@ export type RunDetail = {
result?: { result?: {
[key: string]: unknown; [key: string]: unknown;
}; };
commit?: string;
flow_version?: number; flow_version?: number;
nodes?: Array<RunNodeRow>; nodes?: Array<RunNodeRow>;
artifacts?: Array<ArtifactRow>; artifacts?: Array<ArtifactRow>;
+1 -1
View File
@@ -53,7 +53,7 @@ export function RunMetricChart({
const drawn = plots.reduce((total, plot) => total + plot.length, 0) const drawn = plots.reduce((total, plot) => total + plot.length, 0)
return ( return (
<div className="flex flex-col gap-2"> <div className="flex h-64 flex-col gap-2">
<UplotChart <UplotChart
labels={labels} labels={labels}
plots={plots} plots={plots}
+6 -3
View File
@@ -289,12 +289,13 @@ function RunsTable({
</TableRow> </TableRow>
)} )}
{runs.map((run) => ( {runs.map((run) => (
<TableRow key={run.id}> <TableRow key={run.id} data-testid="run-row">
<TableCell> <TableCell>
<Checkbox <Checkbox
checked={selected.includes(run.id)} checked={selected.includes(run.id)}
onCheckedChange={() => onToggle(run.id)} onCheckedChange={() => onToggle(run.id)}
aria-label={`Compare ${run.id}`} aria-label={`Compare ${run.id}`}
data-testid="run-select"
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -303,6 +304,7 @@ function RunsTable({
to="/runs/$id" to="/runs/$id"
params={{ id: run.id }} params={{ id: run.id }}
className="font-mono text-sm hover:underline" className="font-mono text-sm hover:underline"
data-testid="run-link"
> >
{shortId(run.id)} {shortId(run.id)}
</Link> </Link>
@@ -342,8 +344,9 @@ function RunsTable({
<TableCell className="font-mono text-muted-foreground text-xs"> <TableCell className="font-mono text-muted-foreground text-xs">
{/* The user's own repository when there is one: for a flow {/* The user's own repository when there is one: for a flow
declared in code, the store's commit names a generated declared in code, the store's commit names a generated
shim rather than anything anyone wrote. */} shim rather than anything anyone wrote. A canvas flow has
{shortCommit(run.origin_commit ?? "") || "—"} only the store's, which is then the whole answer. */}
{shortCommit(run.origin_commit || run.commit || "") || "—"}
</TableCell> </TableCell>
<TableCell className="text-right text-muted-foreground text-sm tabular-nums"> <TableCell className="text-right text-muted-foreground text-sm tabular-nums">
{run.duration_ms ? dur(run.duration_ms) : "—"} {run.duration_ms ? dur(run.duration_ms) : "—"}