swarm-ui: shared Hint, ActionRow, Toolbar, EmptyState, ExternalLink and ExpandableCard replace per-page copies
Hint, ActionRow, Toolbar, EmptyState and ExternalLink replace the muted text, button row, refresh bar, empty line and _blank anchor that each page restyled under its own class. ExpandableCard (with a loading twin) holds the builds and recent-runs cards, which had identical head and body CSS. The create form's raw checkbox becomes CheckboxField. The components page shows each new piece.
This commit is contained in:
parent
b74b5e77e4
commit
8a2e5203ad
42 changed files with 689 additions and 427 deletions
|
|
@ -7,6 +7,7 @@ import { useState } from "preact/hooks";
|
|||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||
import { Button } from "../ui/button/Button.js";
|
||||
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
|
||||
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
|
||||
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
||||
import {
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
import { fetchJson, usePolled } from "../ui/polled/usePolled.js";
|
||||
import { Table, type TableColumn } from "../ui/table/Table.js";
|
||||
import { TableSkeleton } from "../ui/table/TableSkeleton.js";
|
||||
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
||||
import { activeSinceMs, alertKey, listedAlerts, type Alert } from "./alerts.js";
|
||||
|
||||
const STATE_TONE: Record<string, BadgeTone> = {
|
||||
|
|
@ -90,10 +92,10 @@ function AlertsBody() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div class="ui-alerts-bar">
|
||||
<a href={GRAFANA_ALERTS_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Toolbar>
|
||||
<ExternalLink href={GRAFANA_ALERTS_URL}>
|
||||
Grafana's alert list
|
||||
</a>
|
||||
</ExternalLink>
|
||||
{showAll || hidden > 0 ? (
|
||||
<Button onClick={() => setShowAll(!showAll)}>
|
||||
{showAll ? "only firing" : `show ${hidden} more`}
|
||||
|
|
@ -105,7 +107,7 @@ function AlertsBody() {
|
|||
onChange={setIntervalMs}
|
||||
refreshing={refreshing}
|
||||
/>
|
||||
</div>
|
||||
</Toolbar>
|
||||
{error ? (
|
||||
<ApiErrorPanel
|
||||
context="failed to load Grafana's alerts"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,15 @@ import {
|
|||
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
|
||||
import { Button, type ButtonVariant } from "../ui/button/Button.js";
|
||||
import { Dialog } from "../ui/dialog/Dialog.js";
|
||||
import { ActionRow } from "../ui/action-row/ActionRow.js";
|
||||
import { EmptyState } from "../ui/empty-state/EmptyState.js";
|
||||
import {
|
||||
ExpandableCard,
|
||||
ExpandableCardSkeleton,
|
||||
} from "../ui/expandable-card/ExpandableCard.js";
|
||||
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
|
||||
import { Hint, type HintTone } from "../ui/hint/Hint.js";
|
||||
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
||||
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
|
||||
import { Popover } from "../ui/popover/Popover.js";
|
||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||
|
|
@ -41,6 +50,8 @@ import { GearIcon } from "@hive/shared/icons.js";
|
|||
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
|
||||
import "./ComponentsPage.css";
|
||||
|
||||
const HINT_TONES: HintTone[] = ["muted", "warn", "error", "ok"];
|
||||
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
|
|
@ -590,6 +601,62 @@ export function ComponentsPage() {
|
|||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Hint">
|
||||
<div class="components-chip-row">
|
||||
{HINT_TONES.map((tone) => (
|
||||
<Sample key={tone} label={tone}>
|
||||
<Hint tone={tone}>A line of supporting text.</Hint>
|
||||
</Sample>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="ActionRow, Toolbar, EmptyState, ExternalLink">
|
||||
<Sample label="ActionRow: the buttons that end a form">
|
||||
<ActionRow>
|
||||
<Button variant="primary">save</Button>
|
||||
<Button>cancel</Button>
|
||||
</ActionRow>
|
||||
</Sample>
|
||||
<Sample label="Toolbar: the bar above a list">
|
||||
<Toolbar>
|
||||
<Button>refresh</Button>
|
||||
</Toolbar>
|
||||
</Sample>
|
||||
<Sample label="EmptyState">
|
||||
<EmptyState>Nothing here yet.</EmptyState>
|
||||
</Sample>
|
||||
<Sample label="ExternalLink">
|
||||
<ExternalLink href="https://example.com">example.com</ExternalLink>
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="ExpandableCard">
|
||||
<Sample label="closed, open, and the loading twin">
|
||||
<ExpandableCard
|
||||
id="components-expandable-closed"
|
||||
open={false}
|
||||
onToggle={() => {}}
|
||||
title="build 1"
|
||||
badge={<Badge value="ok" />}
|
||||
meta="2 minutes ago"
|
||||
>
|
||||
body
|
||||
</ExpandableCard>
|
||||
<ExpandableCard
|
||||
id="components-expandable-open"
|
||||
open
|
||||
onToggle={() => {}}
|
||||
title="build 2"
|
||||
badge={<Badge value="failed" />}
|
||||
meta="an hour ago"
|
||||
>
|
||||
<Hint>The body appears while the card is open.</Hint>
|
||||
</ExpandableCard>
|
||||
<ExpandableCardSkeleton />
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="Dialog">
|
||||
<Sample label="modal, caller-owned content">
|
||||
<DialogSample />
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@
|
|||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
margin-left: 0.25em;
|
||||
color: var(--muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.create-agent-summary {
|
||||
|
|
@ -118,14 +117,6 @@
|
|||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.create-agent-result-ok {
|
||||
color: var(--green);
|
||||
}
|
||||
/* The controller's refusal of the AGENTS.md text, under its box. */
|
||||
.create-agent-field-error {
|
||||
margin: 0;
|
||||
color: var(--red);
|
||||
}
|
||||
/* The picked archetype's description, under its select. */
|
||||
.create-agent-archetype-description {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ import {
|
|||
InferenceFields,
|
||||
type InferenceDraft,
|
||||
} from "../agent/detail/AgentInference.js";
|
||||
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
|
||||
import { FormField } from "../ui/form-field/FormField.js";
|
||||
import { Hint } from "../ui/hint/Hint.js";
|
||||
import { MultiselectFilterOptions } from "../ui/multiselect-filter/MultiselectFilter.js";
|
||||
import {
|
||||
initialSelection,
|
||||
|
|
@ -325,12 +327,12 @@ export function CreateAgentForm({
|
|||
/>
|
||||
)}
|
||||
{done && (
|
||||
<div class="create-agent-result create-agent-result-ok">
|
||||
<Hint tone="ok" as="div" class="create-agent-result">
|
||||
<p>
|
||||
Queued as job node #{result.nodeId} —{" "}
|
||||
<Link href="/jobs">watch it in jobs</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</Hint>
|
||||
)}
|
||||
{!done && step === "identity" && (
|
||||
<div class="create-agent-fields">
|
||||
|
|
@ -409,7 +411,9 @@ export function CreateAgentForm({
|
|||
onInput={setAgentsMd}
|
||||
/>
|
||||
{refusal !== null && (
|
||||
<p class="create-agent-field-error">{refusal}</p>
|
||||
<Hint tone="error" class="create-agent-field-error">
|
||||
{refusal}
|
||||
</Hint>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -438,14 +442,12 @@ export function CreateAgentForm({
|
|||
)}
|
||||
</div>
|
||||
</FormField>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={internet}
|
||||
onChange={() => setInternet((v) => !v)}
|
||||
/>{" "}
|
||||
Unrestricted internet
|
||||
</label>
|
||||
<CheckboxField
|
||||
id="create-agent-internet"
|
||||
label="Unrestricted internet"
|
||||
checked={internet}
|
||||
onChange={setInternet}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!done && step === "resources" && (
|
||||
|
|
@ -531,9 +533,9 @@ export function CreateAgentForm({
|
|||
back
|
||||
</Button>
|
||||
{problem !== null && (step !== "identity" || name !== "") && (
|
||||
<span class="create-agent-hint" role="status">
|
||||
<Hint as="span" role="status" class="create-agent-hint">
|
||||
{problem}
|
||||
</span>
|
||||
</Hint>
|
||||
)}
|
||||
{step === "summary" ? (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import { useLocalSetting } from "@hive/shared/settings-storage.js";
|
||||
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
|
||||
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
|
||||
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
|
||||
import { SelectField } from "../ui/select-field/SelectField.js";
|
||||
|
|
@ -268,9 +269,7 @@ function IssueReportBody() {
|
|||
ariaSort: ariaSortFor("number"),
|
||||
render: (r) =>
|
||||
r.html_url ? (
|
||||
<a href={r.html_url} target="_blank" rel="noreferrer">
|
||||
#{r.number}
|
||||
</a>
|
||||
<ExternalLink href={r.html_url}>#{r.number}</ExternalLink>
|
||||
) : (
|
||||
`#${r.number}`
|
||||
),
|
||||
|
|
|
|||
|
|
@ -14,8 +14,3 @@
|
|||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
.ui-jobs-refresh {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
|
||||
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
||||
import { BuildsCard } from "./builds/BuildsPage.js";
|
||||
import { ReviewPage } from "./review/ReviewPage.js";
|
||||
import {
|
||||
|
|
@ -53,13 +54,13 @@ function JobsBody() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div class="ui-jobs-refresh">
|
||||
<Toolbar>
|
||||
<RefreshIntervalPicker
|
||||
id="jobs-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
</div>
|
||||
</Toolbar>
|
||||
<JobqRollup endpoint="/api/jobq/rollup" refreshToken={refreshToken} />
|
||||
<JobqGraph endpoint="/api/jobq/graph" refreshToken={refreshToken} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { TextAreaField } from "../ui/text-area-field/TextAreaField.js";
|
||||
import { Button } from "../ui/button/Button.js";
|
||||
import { Hint } from "../ui/hint/Hint.js";
|
||||
import "./LinkMatrixAccountForm.css";
|
||||
|
||||
type SubmitState =
|
||||
|
|
@ -115,9 +116,9 @@ export function LinkBackendEnvForm({
|
|||
</Button>
|
||||
</form>
|
||||
{result.status === "done" && (
|
||||
<p class="link-matrix-account-result-ok">
|
||||
<Hint tone="ok" class="link-matrix-account-result-ok">
|
||||
{result.message} <strong>{agent}</strong>
|
||||
</p>
|
||||
</Hint>
|
||||
)}
|
||||
{result.status === "error" && (
|
||||
<ApiErrorPanel
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { TextField } from "../ui/text-field/TextField.js";
|
||||
import { Button } from "../ui/button/Button.js";
|
||||
import { Hint } from "../ui/hint/Hint.js";
|
||||
import {
|
||||
checkLink,
|
||||
LABEL_PATTERN,
|
||||
|
|
@ -147,10 +148,10 @@ export function LinkForgeAccountForm({
|
|||
</Button>
|
||||
</form>
|
||||
{result.status === "done" && (
|
||||
<p class="link-matrix-account-result-ok">
|
||||
<Hint tone="ok" class="link-matrix-account-result-ok">
|
||||
linked <strong>{result.label}</strong> ({result.url}) to{" "}
|
||||
<strong>{agent}</strong>
|
||||
</p>
|
||||
</Hint>
|
||||
)}
|
||||
{result.status === "error" && (
|
||||
<ApiErrorPanel
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { TextField } from "../ui/text-field/TextField.js";
|
||||
import { Button } from "../ui/button/Button.js";
|
||||
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
|
||||
import { Hint } from "../ui/hint/Hint.js";
|
||||
import "./LinkMatrixAccountForm.css";
|
||||
|
||||
type SubmitState =
|
||||
|
|
@ -67,13 +69,9 @@ export function LinkGithubAccountForm({
|
|||
two minutes, and its <code>gh</code> and <code>git push</code> to
|
||||
github.com then authenticate with it. Use a dedicated bot account and a
|
||||
minimally scoped token, created at{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink href="https://github.com/settings/tokens">
|
||||
github.com/settings/tokens
|
||||
</a>
|
||||
</ExternalLink>
|
||||
; GitHub notifications also need the <code>notifications</code> scope.
|
||||
</p>
|
||||
<form class="link-matrix-account-form" onSubmit={submit}>
|
||||
|
|
@ -94,9 +92,9 @@ export function LinkGithubAccountForm({
|
|||
</Button>
|
||||
</form>
|
||||
{result.status === "done" && (
|
||||
<p class="link-matrix-account-result-ok">
|
||||
<Hint tone="ok" class="link-matrix-account-result-ok">
|
||||
stored a github token for <strong>{agent}</strong>
|
||||
</p>
|
||||
</Hint>
|
||||
)}
|
||||
{result.status === "error" && (
|
||||
<ApiErrorPanel
|
||||
|
|
|
|||
|
|
@ -13,6 +13,5 @@
|
|||
margin-top: 1em;
|
||||
}
|
||||
.link-matrix-account-result-ok {
|
||||
margin-top: 1em;
|
||||
color: var(--green);
|
||||
margin-block: 1em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { Panel } from "../ui/panel/Panel.js";
|
|||
import { TextField } from "../ui/text-field/TextField.js";
|
||||
import { SelectField } from "../ui/select-field/SelectField.js";
|
||||
import { Button } from "../ui/button/Button.js";
|
||||
import { Hint } from "../ui/hint/Hint.js";
|
||||
import "./LinkMatrixAccountForm.css";
|
||||
|
||||
type Mode = "token" | "password";
|
||||
|
|
@ -224,7 +225,7 @@ export function LinkMatrixAccountForm({
|
|||
</Button>
|
||||
</form>
|
||||
{result.status === "done" && (
|
||||
<p class="link-matrix-account-result-ok">
|
||||
<Hint tone="ok" class="link-matrix-account-result-ok">
|
||||
linked <strong>{result.account}</strong> to <strong>{agent}</strong>
|
||||
{result.userId ? (
|
||||
<>
|
||||
|
|
@ -232,7 +233,7 @@ export function LinkMatrixAccountForm({
|
|||
— logged in as <strong>{result.userId}</strong>
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
</Hint>
|
||||
)}
|
||||
{result.status === "error" && (
|
||||
<ApiErrorPanel
|
||||
|
|
|
|||
|
|
@ -54,15 +54,8 @@
|
|||
}
|
||||
.ui-broadcast-count {
|
||||
margin-left: auto;
|
||||
color: var(--muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.ui-broadcast-empty {
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
padding: 1.25em 0.75em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* The picker: each agent a labelled checkbox card, icon over name. */
|
||||
.ui-broadcast-grid {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import {
|
|||
RefreshIntervalPicker,
|
||||
type RefreshIntervalMs,
|
||||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||
import { EmptyState } from "../../ui/empty-state/EmptyState.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js";
|
||||
import {
|
||||
|
|
@ -170,9 +172,9 @@ export function BroadcastComposer() {
|
|||
>
|
||||
clear
|
||||
</button>
|
||||
<span class="ui-broadcast-count" role="status">
|
||||
<Hint as="span" role="status" class="ui-broadcast-count">
|
||||
{targets.length} selected
|
||||
</span>
|
||||
</Hint>
|
||||
<RefreshIntervalPicker
|
||||
id="broadcast-refresh"
|
||||
value={intervalMs}
|
||||
|
|
@ -182,11 +184,11 @@ export function BroadcastComposer() {
|
|||
</div>
|
||||
|
||||
{agents !== null && shown.length === 0 ? (
|
||||
<p class="ui-broadcast-empty">
|
||||
<EmptyState centered>
|
||||
{filter === "running" && all.length > 0
|
||||
? "no agent is running right now. Show all to pick a stopped one."
|
||||
: "no agents."}
|
||||
</p>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<ul class="ui-broadcast-grid" aria-label="agents">
|
||||
{shown.map((agent) => {
|
||||
|
|
|
|||
|
|
@ -1,42 +1,10 @@
|
|||
/* The build list: one card per build, the head row a button that opens it. */
|
||||
/* The build list: one card per build (`ui/expandable-card`). */
|
||||
.ui-builds-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.ui-builds-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5em 0.75em;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-builds-head:focus-visible {
|
||||
outline: 2px solid var(--purple);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.ui-builds-title {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.ui-builds-meta {
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
}
|
||||
.ui-builds-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4em;
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
.ui-builds-text {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
|
|
@ -51,11 +19,3 @@
|
|||
background: color-mix(in srgb, var(--crust) 60%, transparent);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.ui-builds-empty {
|
||||
color: var(--muted);
|
||||
}
|
||||
.ui-builds-refresh {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,13 @@ import { useEffect, useRef, useState } from "preact/hooks";
|
|||
import { useSearch } from "wouter-preact";
|
||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import { Card } from "../../ui/card/Card.js";
|
||||
import { EmptyState } from "../../ui/empty-state/EmptyState.js";
|
||||
import {
|
||||
ExpandableCard,
|
||||
ExpandableCardSkeleton,
|
||||
} from "../../ui/expandable-card/ExpandableCard.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { Toolbar } from "../../ui/toolbar/Toolbar.js";
|
||||
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
|
|
@ -45,50 +51,44 @@ function BuildCard({
|
|||
onToggle: () => void;
|
||||
}) {
|
||||
const body = bodyText(run);
|
||||
const bodyId = `${cardId(run.id)}-body`;
|
||||
return (
|
||||
<Card class="ui-builds-card">
|
||||
<div id={cardId(run.id)}>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-builds-head"
|
||||
aria-expanded={open}
|
||||
aria-controls={bodyId}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span class="ui-builds-title">
|
||||
<strong>{runTitle(run)}</strong>
|
||||
<code>{run.head_sha.slice(0, 7)}</code>
|
||||
</span>
|
||||
<Badge
|
||||
tone={OUTCOME_TONE[run.outcome]}
|
||||
value={OUTCOME_LABEL[run.outcome]}
|
||||
/>
|
||||
<span class="ui-builds-meta">
|
||||
<RelativeTime epochMs={run.started_unix * 1000} /> · took{" "}
|
||||
{durationLabel(run.started_unix, run.finished_unix)}
|
||||
</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<div class="ui-builds-body" id={bodyId}>
|
||||
{body ? (
|
||||
<>
|
||||
<p class="ui-builds-meta">{body.label}</p>
|
||||
{body.preformatted ? (
|
||||
<pre class="ui-builds-tail">{body.text}</pre>
|
||||
) : (
|
||||
<p class="ui-builds-text">{body.text}</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p class="ui-builds-meta">
|
||||
nothing more was recorded for this build.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
<ExpandableCard
|
||||
id={cardId(run.id)}
|
||||
class="ui-builds-card"
|
||||
open={open}
|
||||
onToggle={onToggle}
|
||||
title={
|
||||
<>
|
||||
<strong>{runTitle(run)}</strong>
|
||||
<code>{run.head_sha.slice(0, 7)}</code>
|
||||
</>
|
||||
}
|
||||
badge={
|
||||
<Badge
|
||||
tone={OUTCOME_TONE[run.outcome]}
|
||||
value={OUTCOME_LABEL[run.outcome]}
|
||||
/>
|
||||
}
|
||||
meta={
|
||||
<>
|
||||
<RelativeTime epochMs={run.started_unix * 1000} /> · took{" "}
|
||||
{durationLabel(run.started_unix, run.finished_unix)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{body ? (
|
||||
<>
|
||||
<Hint>{body.label}</Hint>
|
||||
{body.preformatted ? (
|
||||
<pre class="ui-builds-tail">{body.text}</pre>
|
||||
) : (
|
||||
<p class="ui-builds-text">{body.text}</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Hint>nothing more was recorded for this build.</Hint>
|
||||
)}
|
||||
</ExpandableCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -133,14 +133,14 @@ function BuildsBody() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div class="ui-builds-refresh">
|
||||
<Toolbar>
|
||||
<RefreshIntervalPicker
|
||||
id="builds-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
refreshing={refreshing}
|
||||
/>
|
||||
</div>
|
||||
</Toolbar>
|
||||
<p>
|
||||
What the swarm builder built, newest first: agent config PR heads, and
|
||||
the agents it builds from internal/meta before a deploy. How each ended,
|
||||
|
|
@ -153,15 +153,22 @@ function BuildsBody() {
|
|||
/>
|
||||
) : null}
|
||||
{missing ? (
|
||||
<p class="ui-builds-meta" role="status">
|
||||
<Hint role="status">
|
||||
the build in this link is no longer listed: the builder keeps only its
|
||||
newest builds.
|
||||
</p>
|
||||
</Hint>
|
||||
) : null}
|
||||
{runs !== null && runs.length === 0 && !error ? (
|
||||
<p class="ui-builds-empty">No builds yet.</p>
|
||||
<EmptyState>No builds yet.</EmptyState>
|
||||
) : null}
|
||||
<div class="ui-builds-list">
|
||||
{runs === null && !error ? (
|
||||
<>
|
||||
<ExpandableCardSkeleton />
|
||||
<ExpandableCardSkeleton />
|
||||
<ExpandableCardSkeleton />
|
||||
</>
|
||||
) : null}
|
||||
{(runs ?? []).map((run) => (
|
||||
<BuildCard
|
||||
key={run.id}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
}
|
||||
.ui-messages-count {
|
||||
margin-inline-start: auto;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ui-messages-wrap.terminal-wrap {
|
||||
max-height: 70vh;
|
||||
|
|
@ -30,5 +29,4 @@
|
|||
}
|
||||
.ui-messages-note {
|
||||
margin: 0.75em 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { Row } from "@hive/shared/term-row.js";
|
|||
import { BroadcastComposer } from "../broadcast/BroadcastComposer.js";
|
||||
import { SchedulesCard } from "../schedules/SchedulesPage.js";
|
||||
import { Disclosure } from "../../ui/disclosure/Disclosure.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import { SelectField } from "../../ui/select-field/SelectField.js";
|
||||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
|
|
@ -136,11 +137,11 @@ export function MessagesPage() {
|
|||
placeholder="sender or recipient"
|
||||
onInput={setQuery}
|
||||
/>
|
||||
<span class="ui-messages-count">
|
||||
<Hint as="span" class="ui-messages-count">
|
||||
{query.trim() === ""
|
||||
? `${messages.length} messages`
|
||||
: `${shown.length} of ${messages.length} messages`}
|
||||
</span>
|
||||
</Hint>
|
||||
</div>
|
||||
<div class="terminal-wrap ui-messages-wrap">
|
||||
<div class="live terminal" ref={logRef} onScroll={handleScroll}>
|
||||
|
|
@ -159,11 +160,11 @@ export function MessagesPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p class="ui-messages-note">
|
||||
<Hint class="ui-messages-note">
|
||||
Shows what the agents sent each other. A message an agent drained with
|
||||
an in-turn <code>recv</code> is missing. The history is at most 100
|
||||
recent messages per agent.
|
||||
</p>
|
||||
</Hint>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// <Deliveries> — the notification router's recent deliveries, newest first.
|
||||
// The router keeps the newest 500 rows; this shows what
|
||||
// `GET /api/notify/deliveries` returns.
|
||||
import { EmptyState } from "../../ui/empty-state/EmptyState.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
||||
import { issueRef, outcomeText, type Delivery } from "./RouterLive.js";
|
||||
|
|
@ -48,7 +49,7 @@ const COLUMNS: TableColumn<Row>[] = [
|
|||
];
|
||||
|
||||
export function Deliveries({ rows }: { rows: Delivery[] }) {
|
||||
if (rows.length === 0) return <p>no deliveries yet</p>;
|
||||
if (rows.length === 0) return <EmptyState>no deliveries yet</EmptyState>;
|
||||
const placed = rows.map((d, place) => ({ ...d, place }));
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
type Edit,
|
||||
type Limits,
|
||||
} from "./FlowDraft.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { BoxFace, hitsText } from "./FlowBox.js";
|
||||
import { layoutFlow, neighbour, nodeAt } from "./FlowLayout.js";
|
||||
import { describeNode, ordinal, type Flow } from "./FlowTypes.js";
|
||||
|
|
@ -282,7 +283,9 @@ export function FlowCanvas({
|
|||
return (
|
||||
<div class="notify-canvas-wrap">
|
||||
<div class="notify-palette" role="group" aria-label="add a node">
|
||||
<span class="notify-hint">add</span>
|
||||
<Hint as="span" class="notify-hint">
|
||||
add
|
||||
</Hint>
|
||||
{palette.map((p) => (
|
||||
<button
|
||||
key={p.kind}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue