Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/pages/JobsPage.tsx
iris bf7345405f swarm-ui: the refresh picker of a polling card sits in the card's header row
On the jobs, builds and hives cards the picker was alone on the first line of
the body. DisclosureAction puts a body's control in the header of the
disclosure it is in, beside the chevron and outside the toggle, so it costs no
row and pressing it does not fold the card.
2026-11-07 09:22:34 +01:00

132 lines
4.8 KiB
TypeScript

// <JobsPage> — the swarm-level job graph. Reuses the shared JobqGraph
// component (originally built for the per-hive dashboard's rebuild
// queue) pointed at swarm-controller's own GET /api/jobq/graph instead
// of hive-c0re's — same wire shape (Vec<hive_jobq_wire::GraphNode>),
// different endpoint, no component fork. swarm-controller's graph has
// no real node kinds wired in yet (see swarm-controller/src/jobq.rs's
// SwarmNodeKind/SwarmResourceKind), so this renders an empty tree today
// — the page exists so the wiring is in place before the first real
// swarm-level job (e.g. CreateAgent) lands.
//
// JobqRollup sits above the graph, same "N running / M queued" banner
// the dashboard's 4G3NTS tab shows — no `queueHref`, since a "view
// queue →" link back to this same page would be noise.
//
// Auto-refresh reuses `HivesCard`'s exact pattern (`RefreshIntervalPicker`
// in the `Panel`'s `actions` slot, `useRefreshInterval` driving the
// re-fetch), but ticks a `refreshToken` bump instead of doing its own
// fetch — both `JobqGraph`/`JobqRollup` already accept that prop
// (bumping it is their documented "refetch now" lever) since the
// dashboard's own rebuild-queue view needed the same thing first.
import type { ComponentChildren } from "preact";
import { useState } from "preact/hooks";
import { JobqGraph } from "@hive/shared/jobq-graph.js";
import { JobqRollup } from "@hive/shared/jobq-rollup.js";
import { DisclosureAction } from "../ui/disclosure/DisclosureAction.js";
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { Skeleton, SkeletonRegion } from "../ui/skeleton/Skeleton.js";
import { BuildsCard } from "./builds/BuildsPage.js";
import { ReviewPage } from "./review/ReviewPage.js";
import {
RefreshIntervalPicker,
useRefreshInterval,
type RefreshIntervalMs,
} from "../ui/refresh-interval/RefreshInterval.js";
import "./JobsPage.css";
// Same 30s default + same reasoning as `HivesCard`: no inputs on this
// page for a refresh to clobber, so the out-of-the-box behaviour should
// just solve staleness rather than require an opt-in every visit.
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
// The graph's twin while it loads: the graph's own node and row classes, so the
// rows and the indent of the children are the real ones, with a state glyph and a
// label as placeholders. Two roots, the first with two children.
function SkeletonNode({
label,
children,
}: {
label: string;
children?: ComponentChildren;
}) {
return (
<div class="jg-node">
<div class="jg-row">
<Skeleton shape="circle" width="1em" height="1em" />
<Skeleton width={label} />
</div>
{children}
</div>
);
}
function JobsGraphSkeleton() {
return (
<SkeletonRegion label="the jobs">
<SkeletonNode label="14em">
<SkeletonNode label="10em" />
<SkeletonNode label="12em" />
</SkeletonNode>
<SkeletonNode label="9em" />
</SkeletonRegion>
);
}
function JobsBody() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const [refreshToken, setRefreshToken] = useState(0);
// `useRefreshInterval` ticks once immediately on mount (by design —
// see its own doc comment), which lands on top of `JobqGraph`'s /
// `JobqRollup`'s own unconditional mount-time fetch and costs one
// harmless duplicate request on page load. Not worth a special case
// in the shared hook for: the alternative (skip the first tick) would
// need every other `useRefreshInterval` caller to reason about
// whether it's the sole fetcher or not, for a one-page, one-time,
// zero-user-visible-effect cost here.
useRefreshInterval(intervalMs, () => setRefreshToken((t) => t + 1));
return (
<>
<DisclosureAction>
<RefreshIntervalPicker
id="jobs-refresh"
value={intervalMs}
onChange={setIntervalMs}
/>
</DisclosureAction>
<JobqRollup endpoint="/api/jobq/rollup" refreshToken={refreshToken} />
<JobqGraph
endpoint="/api/jobq/graph"
refreshToken={refreshToken}
loading={<JobsGraphSkeleton />}
/>
</>
);
}
// The jobs tab holds three folded cards: the controller's job graph, the swarm
// builder's recent builds, and the review classifier's page. None reads
// anything until it is opened.
export function JobsPage() {
return (
<div class="ui-jobs-page">
<LazyDisclosure
title="jobs"
icon="🧩"
hint="the controller's job graph: agent creation, credential mints"
>
<JobsBody />
</LazyDisclosure>
<BuildsCard />
<LazyDisclosure
title="review"
icon="🔍"
hint="the review classifier: its recent runs, a try-out, its settings"
>
<ReviewPage />
</LazyDisclosure>
</div>
);
}