swarm-ui: shared code leaves page folders; SectionHeading and Preformatted replace per-page copies
durationLabel moves to util.ts, termPaths to agent/terminal/term-paths.ts, StateBadges and tokenTotal to agent/state, and TryOutResult to pages/review-common, so the builds, runs and agent pages no longer import across page folders. SectionHeading is the body-size section heading the create form and the egress card each styled; Preformatted is the scrolling text block of the builds tail and the AGENTS.md view, which now share one chrome and are both keyboard-scrollable. The components page shows both.
This commit is contained in:
parent
4bd81d5b0f
commit
5d5df29687
14 changed files with 295 additions and 200 deletions
|
|
@ -45,6 +45,8 @@ import { FormStack } from "../ui/form-stack/FormStack.js";
|
|||
import { Hint, type HintTone } from "../ui/hint/Hint.js";
|
||||
import { FormResult } from "../ui/submit/FormResult.js";
|
||||
import type { SubmitState } from "../ui/submit/submit.js";
|
||||
import { Preformatted } from "../ui/preformatted/Preformatted.js";
|
||||
import { SectionHeading } from "../ui/section-heading/SectionHeading.js";
|
||||
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
||||
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
|
||||
import { Popover } from "../ui/popover/Popover.js";
|
||||
|
|
@ -667,6 +669,24 @@ export function ComponentsPage() {
|
|||
))}
|
||||
</Section>
|
||||
|
||||
<Section title="SectionHeading and Preformatted">
|
||||
<Sample label="SectionHeading">
|
||||
<SectionHeading>resource caps</SectionHeading>
|
||||
</Sample>
|
||||
<Sample label="Preformatted: scrolls sideways, and wrapped">
|
||||
<Preformatted maxHeight="6em">
|
||||
{
|
||||
"error: builder for '/nix/store/…drv' failed with exit code 1; last 10 log lines follow below, long lines keep their length and scroll sideways"
|
||||
}
|
||||
</Preformatted>
|
||||
<Preformatted wrap maxHeight="6em">
|
||||
{
|
||||
"A file's text, wrapped to the width of the block so it can be read without scrolling sideways."
|
||||
}
|
||||
</Preformatted>
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="ExpandableCard">
|
||||
<Sample label="closed, open, and the loading twin">
|
||||
<ExpandableCard
|
||||
|
|
|
|||
|
|
@ -75,8 +75,7 @@
|
|||
color: var(--muted);
|
||||
}
|
||||
.create-agent-heading {
|
||||
margin: 0.5em 0 0;
|
||||
font-size: 1em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.create-agent-footer {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
|
||||
import { FormField } from "../ui/form-field/FormField.js";
|
||||
import { Hint } from "../ui/hint/Hint.js";
|
||||
import { SectionHeading } from "../ui/section-heading/SectionHeading.js";
|
||||
import { useSubmit } from "../ui/submit/useSubmit.js";
|
||||
import { MultiselectFilterOptions } from "../ui/multiselect-filter/MultiselectFilter.js";
|
||||
import {
|
||||
|
|
@ -456,7 +457,9 @@ export function CreateAgentForm({
|
|||
Optional: leave a box empty to set nothing, and the hive's or
|
||||
the agent's own value applies.
|
||||
</p>
|
||||
<h3 class="create-agent-heading">resource caps</h3>
|
||||
<SectionHeading class="create-agent-heading">
|
||||
resource caps
|
||||
</SectionHeading>
|
||||
<LimitsFields
|
||||
idPrefix="create-limits"
|
||||
cpu={cpu}
|
||||
|
|
@ -464,7 +467,9 @@ export function CreateAgentForm({
|
|||
onCpu={setCpu}
|
||||
onMemory={setMemory}
|
||||
/>
|
||||
<h3 class="create-agent-heading">inference</h3>
|
||||
<SectionHeading class="create-agent-heading">
|
||||
inference
|
||||
</SectionHeading>
|
||||
<InferenceFields
|
||||
idPrefix="create-inference"
|
||||
draft={inference}
|
||||
|
|
@ -496,7 +501,9 @@ export function CreateAgentForm({
|
|||
today's literal behaviour: mara's call on this copy was to
|
||||
read as finished, not as a running commentary on partial
|
||||
implementation. */}
|
||||
<h3 class="create-agent-heading">what this creates</h3>
|
||||
<SectionHeading class="create-agent-heading">
|
||||
what this creates
|
||||
</SectionHeading>
|
||||
<p>
|
||||
Creating queues everything a new agent needs: a swarm-level
|
||||
identity, a config repo on the forge with the operator added as
|
||||
|
|
|
|||
|
|
@ -15,10 +15,8 @@ import { Badge } from "@hive/shared/badge.js";
|
|||
import { CloseIcon, ExpandIcon } from "@hive/shared/icons.js";
|
||||
import { AgentIcon } from "../../agent/AgentIcon.js";
|
||||
import { SubagentsMenu } from "../../agent/terminal/SubagentsMenu.js";
|
||||
import {
|
||||
AgentTermPreview,
|
||||
termPaths,
|
||||
} from "../../agent/terminal/AgentTermPreview.js";
|
||||
import { AgentTermPreview } from "../../agent/terminal/AgentTermPreview.js";
|
||||
import { termPaths } from "../../agent/terminal/term-paths.js";
|
||||
import {
|
||||
agentsIn,
|
||||
groupHref,
|
||||
|
|
|
|||
|
|
@ -15,10 +15,8 @@ import { Badge } from "@hive/shared/badge.js";
|
|||
import { AgentDetailPopover } from "../../agent/detail/AgentDetailPopover.js";
|
||||
import { AgentIcon } from "../../agent/AgentIcon.js";
|
||||
import { SubagentsMenu } from "../../agent/terminal/SubagentsMenu.js";
|
||||
import {
|
||||
AgentTermPreview,
|
||||
termPaths,
|
||||
} from "../../agent/terminal/AgentTermPreview.js";
|
||||
import { AgentTermPreview } from "../../agent/terminal/AgentTermPreview.js";
|
||||
import { termPaths } from "../../agent/terminal/term-paths.js";
|
||||
import { useAgentFavicon } from "../../agent/terminal/useAgentFavicon.js";
|
||||
import {
|
||||
groupHref,
|
||||
|
|
|
|||
|
|
@ -10,12 +10,3 @@
|
|||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* What nix printed: kept as printed, scrolls sideways rather than wrapping. */
|
||||
.ui-builds-tail {
|
||||
margin: 0;
|
||||
padding: 0.6em 0.75em;
|
||||
max-height: 24em;
|
||||
overflow: auto;
|
||||
border-radius: 0.4em;
|
||||
background: color-mix(in srgb, var(--crust) 60%, transparent);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
ExpandableCardListSkeleton,
|
||||
} from "../../ui/expandable-card/ExpandableCard.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { Preformatted } from "../../ui/preformatted/Preformatted.js";
|
||||
import { Toolbar } from "../../ui/toolbar/Toolbar.js";
|
||||
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||
import {
|
||||
|
|
@ -23,7 +24,7 @@ import {
|
|||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { durationLabel } from "../review-runs/RunsTypes.js";
|
||||
import { durationLabel } from "../../util.js";
|
||||
import {
|
||||
OUTCOME_LABEL,
|
||||
OUTCOME_TONE,
|
||||
|
|
@ -80,7 +81,7 @@ function BuildCard({
|
|||
<>
|
||||
<Hint>{body.label}</Hint>
|
||||
{body.preformatted ? (
|
||||
<pre class="ui-builds-tail">{body.text}</pre>
|
||||
<Preformatted>{body.text}</Preformatted>
|
||||
) : (
|
||||
<p class="ui-builds-text">{body.text}</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
/* A finished run's table, the overall verdict under it and the head commit. */
|
||||
.ui-tryout-overall,
|
||||
.ui-tryout-sha {
|
||||
margin: 0.75em 0 0;
|
||||
}
|
||||
.ui-tryout-sha {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// <TryOutResult> — a finished try-out: one row per classification, the
|
||||
// overall verdict under them, and the head commit the run read. One column per
|
||||
// field of a row, where the posted review folds them into four
|
||||
// (`docs/4-implementation/swarm/review-classifier.md`); no token
|
||||
// counts, cache counts or cost, here or anywhere in the UI.
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
||||
import type {
|
||||
TryOutResult as Result,
|
||||
TryOutRow,
|
||||
} from "../review-tryout/TryOutTypes.js";
|
||||
import "./TryOutResult.css";
|
||||
|
||||
const VERDICT_LABEL: Record<TryOutRow["verdict"], string> = {
|
||||
match: "match",
|
||||
"no-match": "no match",
|
||||
"not-applicable": "not applicable",
|
||||
};
|
||||
|
||||
const COLUMNS: TableColumn<TryOutRow>[] = [
|
||||
{ key: "name", header: "classification", render: (r) => r.name },
|
||||
{ key: "polarity", header: "polarity", render: (r) => r.polarity },
|
||||
{
|
||||
key: "verdict",
|
||||
header: "verdict",
|
||||
render: (r) => (r.error ? "—" : VERDICT_LABEL[r.verdict]),
|
||||
},
|
||||
{
|
||||
key: "certainty",
|
||||
header: "certainty",
|
||||
render: (r) => (r.error ? "—" : r.certainty),
|
||||
},
|
||||
{ key: "threshold", header: "threshold", render: (r) => r.threshold },
|
||||
{
|
||||
key: "counted",
|
||||
header: "counted",
|
||||
// A counted row shows whether it passed its polarity; one below its
|
||||
// threshold does not count and has no pass or fail.
|
||||
render: (r) =>
|
||||
r.error ? "—" : r.counted ? (r.passed ? "yes ✅" : "yes ❌") : "no",
|
||||
},
|
||||
{
|
||||
key: "reason",
|
||||
header: "reason",
|
||||
cellClass: "ui-table-prose",
|
||||
// A row that could not run says why where the reason would be.
|
||||
render: (r) =>
|
||||
r.error ? (
|
||||
<Hint tone="error" as="span" class="ui-tryout-row-error">
|
||||
{r.error}
|
||||
</Hint>
|
||||
) : (
|
||||
r.reason
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/** What the table needs of a run: the try-out's answer, or a listed run's. The
|
||||
* head commit is absent for a run that never read one. */
|
||||
export type TryOutAnswer = Pick<Result, "rows" | "overall"> & {
|
||||
head_sha?: string;
|
||||
};
|
||||
|
||||
export function TryOutResult({
|
||||
result,
|
||||
filterKey = "swarm-ui:review-tryout:table-filters",
|
||||
}: {
|
||||
result: TryOutAnswer;
|
||||
/** Where the column filters are remembered; a page showing several tables
|
||||
* keeps them apart from the try-out's. */
|
||||
filterKey?: string;
|
||||
}) {
|
||||
const rows = result.rows ?? [];
|
||||
return (
|
||||
<div class="ui-tryout-result">
|
||||
<Table
|
||||
columns={COLUMNS}
|
||||
rows={rows}
|
||||
// By position: two classifications may share a name, and a shared key
|
||||
// would let one result render over the other.
|
||||
rowKey={(r) => String(rows.indexOf(r))}
|
||||
storageKey={filterKey}
|
||||
emptyMessage="the run returned no classifications"
|
||||
/>
|
||||
{result.overall ? (
|
||||
<p class="ui-tryout-overall">
|
||||
<strong>Overall: </strong>
|
||||
<Badge
|
||||
tone={result.overall === "approve" ? "positive" : "negative"}
|
||||
value={result.overall === "approve" ? "approve" : "request changes"}
|
||||
/>
|
||||
</p>
|
||||
) : null}
|
||||
{result.head_sha ? (
|
||||
<p class="ui-tryout-sha">
|
||||
Ran on head <code>{result.head_sha.slice(0, 12)}</code>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,13 +21,13 @@ import {
|
|||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { TryOutResult } from "../review-tryout/TryOutResult.js";
|
||||
import { TryOutResult } from "../review-common/TryOutResult.js";
|
||||
import { durationLabel } from "../../util.js";
|
||||
import {
|
||||
OUTCOME_LABEL,
|
||||
OUTCOME_TONE,
|
||||
TRIGGER_LABEL,
|
||||
classificationCount,
|
||||
durationLabel,
|
||||
type ClassifierRun,
|
||||
} from "./RunsTypes.js";
|
||||
import "./ReviewRuns.css";
|
||||
|
|
|
|||
|
|
@ -60,16 +60,6 @@ export const TRIGGER_LABEL: Record<RunTrigger, string> = {
|
|||
review: "requested review",
|
||||
};
|
||||
|
||||
/** How long a run took, as `45s`, `3m 12s` or `1h 4m`. A clock that ran
|
||||
* backwards reads as `0s`. */
|
||||
export function durationLabel(startedUnix: number, finishedUnix: number) {
|
||||
const total = Math.max(0, Math.round(finishedUnix - startedUnix));
|
||||
if (total < 60) return `${total}s`;
|
||||
const minutes = Math.floor(total / 60);
|
||||
if (minutes < 60) return `${minutes}m ${total % 60}s`;
|
||||
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
}
|
||||
|
||||
/** How many classifications ran, in words. A run that never got as far as
|
||||
* running any has no rows. */
|
||||
export function classificationCount(run: ClassifierRun): string {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { ExternalLink } from "../../ui/external-link/ExternalLink.js";
|
|||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
import { ClassificationEditor } from "./ClassificationEditor.js";
|
||||
import { TryOutResult } from "./TryOutResult.js";
|
||||
import { TryOutResult } from "../review-common/TryOutResult.js";
|
||||
import {
|
||||
addProblem,
|
||||
draftFromRule,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Button } from "../../ui/button/Button.js";
|
|||
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { SectionHeading } from "../../ui/section-heading/SectionHeading.js";
|
||||
import "../../agent/detail/AgentLimits.css";
|
||||
import "./SwarmPage.css";
|
||||
|
||||
|
|
@ -61,7 +62,7 @@ function DomainSection({
|
|||
const note = kind === "deny" ? behindNote(behind) : null;
|
||||
return (
|
||||
<section class="ui-swarm-egress-section">
|
||||
<h3 class="ui-swarm-egress-heading">{heading}</h3>
|
||||
<SectionHeading class="ui-swarm-egress-heading">{heading}</SectionHeading>
|
||||
<Hint class="ui-swarm-egress-help">{help}</Hint>
|
||||
{errorPanel}
|
||||
{entries === null ? (
|
||||
|
|
|
|||
|
|
@ -36,8 +36,7 @@
|
|||
border-top: 1px dashed var(--border);
|
||||
}
|
||||
.ui-swarm-egress-heading {
|
||||
margin: 0 0 0.35em;
|
||||
font-size: 1em;
|
||||
margin-bottom: 0.35em;
|
||||
}
|
||||
.ui-swarm-egress-behind {
|
||||
margin: 0 0 0.75em;
|
||||
|
|
|
|||
Loading…
Reference in a new issue