Photograph the runs screen and a dashboard read against runs
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) : "—"}
|
||||||
|
|||||||
Reference in New Issue
Block a user