diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 0bdca539..8f850528 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -9,7 +9,6 @@ import { useState } from 'preact/hooks'; import type { ComponentChildren } from 'preact'; import { Panel } from '../ui/panel/Panel.js'; import { RelativeTime } from '../ui/relative-time/RelativeTime.js'; -import { RefreshIntervalPicker, type RefreshIntervalMs } from '../ui/refresh-interval/RefreshInterval.js'; import { StatusChip, type ChipTone } from '../ui/status-chip/StatusChip.js'; import { Table, type TableColumn } from '../ui/table/Table.js'; import { TextField } from '../ui/text-field/TextField.js'; @@ -81,11 +80,6 @@ function SelectFieldSample() { ); } -function RefreshIntervalPickerSample() { - const [value, setValue] = useState(30_000); - return ; -} - export function ComponentsPage() { return ( @@ -101,14 +95,6 @@ export function ComponentsPage() { panel body content, no title - - {}} />} - > - panel body content - -
@@ -141,12 +127,6 @@ export function ComponentsPage() {
-
- - - -
-
diff --git a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx index 7149e62f..f2f5550c 100644 --- a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx @@ -7,21 +7,11 @@ // that placeholder's comment laid out. Its own component/file rather // than living in `App.tsx`, matching `JobsPage`'s shape: `App.tsx` is // routing, a page owns its own fetch + render. -// -// Polls on a `RefreshIntervalPicker` cadence rather than fetching once -// at mount — no inputs on this page, so a re-fetch clobbering an -// in-progress edit (the hook's stated caller obligation) isn't a live -// concern here. -import { useState } from 'preact/hooks'; +import { useEffect, useState } from 'preact/hooks'; import { ApiErrorPanel } from '@hive/shared/api-error-panel.js'; import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js'; import { Panel } from '../ui/panel/Panel.js'; import { RelativeTime } from '../ui/relative-time/RelativeTime.js'; -import { - RefreshIntervalPicker, - useRefreshInterval, - type RefreshIntervalMs, -} from '../ui/refresh-interval/RefreshInterval.js'; import { StatusChip, type ChipTone } from '../ui/status-chip/StatusChip.js'; import { Table, type TableColumn } from '../ui/table/Table.js'; @@ -85,19 +75,11 @@ const COLUMNS: TableColumn[] = [ }, ]; -// 30s default: this page has no inputs to interrupt, and the whole -// point of a refresh-interval control is "no manual reload needed" — an -// operator who wants it off still can, but the out-of-the-box behaviour -// should actually solve the staleness problem rather than require an -// opt-in every visit. -const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; - export function HivesPage() { const [hives, setHives] = useState(null); const [error, setError] = useState(null); - const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); - useRefreshInterval(intervalMs, () => { + useEffect(() => { (async () => { const r = await fetch('/api/hives/status'); if (!r.ok) { @@ -105,18 +87,11 @@ export function HivesPage() { return; } setHives((await r.json()) as HiveStatus[]); - // A refresh that succeeds clears a previous failure — otherwise a - // transient error would sit on screen forever after the data - // itself has recovered. - setError(null); })().catch((e: unknown) => setError({ detail: String(e) })); - }); + }, []); return ( - } - > + {error ? : null} {!error && hives === null ?

loading…

: null} {hives ? h.name} /> : null} diff --git a/frontend/packages/swarm-ui/src/ui/panel/Panel.css b/frontend/packages/swarm-ui/src/ui/panel/Panel.css index 0132d514..23a40980 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.css +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.css @@ -3,26 +3,12 @@ border-radius: 0.5em; background: var(--bg-elev); } -.ui-panel-header { - display: flex; - align-items: center; - gap: 1em; - padding: 0.75em 1em; - border-bottom: 1px solid var(--border); -} .ui-panel-title { margin: 0; + padding: 0.75em 1em; font-size: 1em; font-weight: 600; -} -/* `margin-left: auto` (not `justify-content: space-between` on the - header) so actions still land at the right edge even on the rare - panel that has actions but no title. */ -.ui-panel-actions { - display: flex; - align-items: center; - gap: 0.5em; - margin-left: auto; + border-bottom: 1px solid var(--border); } .ui-panel-body { padding: 1em; diff --git a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx index eea69b56..de8f88db 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx @@ -2,35 +2,13 @@ // bordered surface with an optional title, no other opinions. Not a // card-with-actions/footer/whatever kit — those get added the first // time a real page actually needs one, not speculatively ahead of it. -// -// `actions` is that one addition: a slot in the title row, right- -// aligned, for a control that belongs next to the panel's own heading -// rather than its own row inside the body (design guide: "a control -// belongs next to the thing it affects, not tucked into a catch-all -// menu" — and per mara's review on the refresh-interval PR, not a -// dedicated row stealing vertical space from the panel's actual -// content either). `HivesPage`'s refresh-interval picker is the -// motivating caller. import type { ComponentChildren } from 'preact'; import './Panel.css'; -export function Panel({ - title, - actions, - children, -}: { - title?: string; - actions?: ComponentChildren; - children: ComponentChildren; -}) { +export function Panel({ title, children }: { title?: string; children: ComponentChildren }) { return (
- {title || actions ? ( -
- {title ?

{title}

: null} - {actions ?
{actions}
: null} -
- ) : null} + {title ?

{title}

: null}
{children}
); diff --git a/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.css b/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.css deleted file mode 100644 index 0e1a134d..00000000 --- a/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.css +++ /dev/null @@ -1,42 +0,0 @@ -/* Compact "clock icon · value · chevron" control — see RefreshInterval.tsx - for why this isn't `SelectField`'s label+bordered-control chrome. Quiet - until interacted with (no border/fill at rest), matching LinksMenu's - header-button treatment so it reads as chrome, not a form. */ -.ui-refresh-picker { - position: relative; - display: inline-flex; - align-items: center; - gap: 0.35em; - min-height: 2.75em; - padding: 0 0.6em; - border: 1px solid transparent; - border-radius: 0.4em; - color: var(--muted); - font-size: 0.9em; - cursor: pointer; -} -.ui-refresh-picker:hover, -.ui-refresh-picker:focus-within { - border-color: var(--border); - background: var(--bg); - color: var(--fg); -} -/* The select drives the interaction (native listbox, keyboard, a11y) but - contributes no chrome of its own — its own box is invisible, sized to - just its selected option's text, and its default arrow is stripped - since the chevron span stands in for it. */ -.ui-refresh-picker-select { - appearance: none; - border: none; - background: none; - color: inherit; - font: inherit; - padding: 0; - margin: 0; - cursor: pointer; -} -.ui-refresh-picker-icon, -.ui-refresh-picker-chevron { - font-size: 0.85em; - line-height: 1; -} diff --git a/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.tsx b/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.tsx deleted file mode 100644 index 5a0979bd..00000000 --- a/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.tsx +++ /dev/null @@ -1,131 +0,0 @@ -// + `useRefreshInterval` — the shared "grafana- -// like" polling pattern from the design-language discussion: the -// operator picks a cadence, the page re-fetches on it, and the timer -// pauses while the tab is backgrounded rather than burning requests for -// no visible benefit. Deliberately small (per mara: "component should -// not be too big") — a preset cadence list and a pause-on-hidden timer, -// nothing more. The UI (picker) and the behaviour (hook) live in one -// file since neither is useful without the other and splitting them -// would be two files for one concern. -// -// The picker itself is a compact "clock icon · value · chevron" inline -// control, not a labelled `SelectField` — per mara's review, a full -// label+bordered-control form field reads as way too heavy for a -// passive-until-touched setting that lives in a panel's title row -// (`Panel`'s `actions` slot), not a form. Visually quiet (no border/ -// fill until hovered/focused) the same way `LinksMenu`'s header button -// is, so it reads as chrome rather than another input to fill in. A -// native `` for the accessible - // name a sighted-only icon+chevron can't supply). - - ); -} - -// Calls `onTick` immediately and then every `intervalMs`, paused while -// the document is hidden and resynced (an immediate call, not a stale -// leftover wait) on becoming visible again — same pattern -// `RelativeTime` already uses for the same reason. `intervalMs = null` -// means "off": no calls at all, not even the initial one — a caller -// that wants data on mount regardless of the picker's starting value -// should default its own `intervalMs` state to a real cadence, not -// `null`, and add a separate mount effect only if it genuinely wants -// "off" to still mean "load once." -export function useRefreshInterval(intervalMs: RefreshIntervalMs, onTick: () => void) { - // Always-current via a ref rather than a `useEffect` dependency: - // callers pass an inline closure that's a new value every render, and - // depending on it directly would re-arm the timer (losing whatever's - // left of the current interval) on every render instead of only when - // the cadence itself changes. The ref keeps the *timer* stable while - // still calling the *latest* callback, not a stale one captured at - // mount. - const onTickRef = useRef(onTick); - onTickRef.current = onTick; - - useEffect(() => { - if (intervalMs === null) return; - let id: ReturnType | undefined; - - const start = () => { - onTickRef.current(); - id = setInterval(() => onTickRef.current(), intervalMs); - }; - const stop = () => { - if (id !== undefined) clearInterval(id); - id = undefined; - }; - - if (document.visibilityState === 'visible') start(); - const onVisibility = () => { - if (document.visibilityState === 'visible') start(); - else stop(); - }; - document.addEventListener('visibilitychange', onVisibility); - return () => { - stop(); - document.removeEventListener('visibilitychange', onVisibility); - }; - }, [intervalMs]); -}