Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: builds are a card under the job graph on the jobs tab

The builds tab is gone from the nav; its card sits below the jobs card on
/jobs. The swarm-builder commit status now links to /jobs?run=<id>, and the old
/builds address redirects there with its query so links already on PRs still
open the build. Docs and the link test follow.
This commit is contained in:
iris 2026-10-26 20:02:57 +01:00
commit a2f000bab4
7 changed files with 64 additions and 21 deletions

View file

@ -1,12 +1,12 @@
// Root shell component — routing only. Each route's content is its own
// page component under `./pages/`; this file just maps paths to them.
import { Route, Router, Switch } from "wouter-preact";
import { Redirect, Route, Router, Switch, useSearch } from "wouter-preact";
import { jobsHref } from "./pages/builds/builds-types.js";
import { Shell } from "./shell/Shell.js";
import { useGuardedLocation } from "./shell/useGuardedLocation.js";
import { AgentsPage } from "./pages/agents/AgentsPage.js";
import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js";
import { AlertsPage } from "./pages/AlertsPage.js";
import { BuildsPage } from "./pages/builds/BuildsPage.js";
import { MessagesPage } from "./pages/messages/MessagesPage.js";
import { NEW_FLOW_PATH } from "./pages/notifications/FlowDraft.js";
import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js";
@ -19,6 +19,13 @@ import { SchedulesPage } from "./pages/schedules/SchedulesPage.js";
import { StartPage } from "./pages/StartPage.js";
import { Panel } from "./ui/panel/Panel.js";
// The builds page moved onto the jobs tab. Links made before that, such as the
// Details link on a swarm-builder commit status, land here and go on to the
// same build's card, query kept.
function BuildsMoved() {
return <Redirect to={jobsHref(useSearch())} replace />;
}
function NotFound() {
return (
<Panel title="404" icon="🧭">
@ -42,7 +49,7 @@ export function App() {
/>
<Route path="/jobs" component={JobsPage} />
<Route path="/schedules" component={SchedulesPage} />
<Route path="/builds" component={BuildsPage} />
<Route path="/builds" component={BuildsMoved} />
<Route path="/messages" component={MessagesPage} />
<Route path="/notifications" component={NotificationsPage} />
<Route path={NEW_FLOW_PATH}>

View file

@ -7,3 +7,10 @@
per bundle call). */
@import "@hive/shared/jobq-graph.css";
@import "@hive/shared/jobq-rollup.css";
/* The jobs tab: the job graph's card, then the builds card, one gap apart. */
.ui-jobs-page {
display: flex;
flex-direction: column;
gap: 1em;
}

View file

@ -22,6 +22,7 @@ import { useState } from "preact/hooks";
import { JobqGraph } from "@hive/shared/jobq-graph.js";
import { JobqRollup } from "@hive/shared/jobq-rollup.js";
import { Panel } from "../ui/panel/Panel.js";
import { BuildsCard } from "./builds/BuildsPage.js";
import {
RefreshIntervalPicker,
useRefreshInterval,
@ -34,7 +35,7 @@ import "./JobsPage.css";
// just solve staleness rather than require an opt-in every visit.
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
export function JobsPage() {
function JobsCard() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const [refreshToken, setRefreshToken] = useState(0);
@ -66,3 +67,14 @@ export function JobsPage() {
</Panel>
);
}
// The jobs tab holds two cards: the controller's job graph, and the swarm
// builder's recent builds under it.
export function JobsPage() {
return (
<div class="ui-jobs-page">
<JobsCard />
<BuildsCard />
</div>
);
}

View file

@ -1,8 +1,10 @@
// <BuildsPage> — the swarm builder's recent builds, newest first: which agent
// <BuildsCard> — the swarm builder's recent builds, newest first: which agent
// config PR heads it built, how each ended, and what `nix` printed when one
// failed. One card per build; selecting a card opens it. Reads
// `GET /api/builder/runs`. A `swarm-builder` commit status links here as
// `/builds?run=<id>`, which opens that build's card and scrolls to it.
// `GET /api/builder/runs`. A `swarm-builder` commit status links to the jobs
// page as `/jobs?run=<id>`, which opens that build's card and scrolls to it.
// The card sits under the job graph on that page; the old `/builds` address
// redirects there with its query.
import { useEffect, useRef, useState } from "preact/hooks";
import { useSearch } from "wouter-preact";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
@ -91,7 +93,7 @@ function BuildCard({
);
}
export function BuildsPage() {
export function BuildsCard() {
const wanted = wantedRun(useSearch());
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);

View file

@ -37,6 +37,11 @@ export const OUTCOME_TONE: Record<BuildOutcome, BadgeTone> = {
error: "warning",
};
/** Where an old `/builds` link goes: the jobs page, keeping its query. */
export function jobsHref(search: string): string {
return search === "" ? "/jobs" : `/jobs?${search}`;
}
/** The run a `?run=<id>` query names, or `null` when it names none. */
export function wantedRun(search: string): string | null {
const id = new URLSearchParams(search).get("run");

View file

@ -58,7 +58,6 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
{ href: "/notifications", label: "notifications", accent: "var(--yellow)" },
{ href: "/issues", label: "issues", accent: "var(--yellow)" },
{ href: "/review", label: "review", accent: "var(--amber)" },
{ href: "/builds", label: "builds", accent: "var(--green)" },
{ href: "/components", label: "components", accent: "var(--blue)" },
];

View file

@ -3,7 +3,11 @@
// stripping, so the `.ts` module is imported as it is.
import assert from "node:assert/strict";
import { test } from "node:test";
import { bodyText, wantedRun } from "../src/pages/builds/builds-types.ts";
import {
bodyText,
jobsHref,
wantedRun,
} from "../src/pages/builds/builds-types.ts";
const base = {
id: "atlas-abc-1",
@ -49,3 +53,8 @@ test("a run with no text for its outcome shows no body", () => {
assert.equal(bodyText({ ...base, outcome: "failed" }), null);
assert.equal(bodyText({ ...base, outcome: "built", error_tail: "x" }), null);
});
test("an old builds link goes to the jobs page and keeps its query", () => {
assert.equal(jobsHref("run=atlas-abc-1"), "/jobs?run=atlas-abc-1");
assert.equal(jobsHref(""), "/jobs");
});