swarm-ui: hives are a folded card on the agents page, and hide developer details also hides the components tab
The hives nav tab and route go, with no redirect; the roster is a card above the agents panel that starts folded and is read on the first open. The settings toggle is renamed from hide debug output to hide developer details, as it now also drops the components tab (the page still answers at /components); the stored preference keeps its key.
This commit is contained in:
parent
0cbfa806d3
commit
70917b7dd0
12 changed files with 110 additions and 49 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// Settings-menu row: "hide debug output". Rendered as a `<SettingsMenu>`
|
||||
// Settings-menu row: "hide developer details". Rendered as a `<SettingsMenu>`
|
||||
// child (`./SettingsMenu.js`) by each page that has a terminal, the
|
||||
// per-agent page and the swarm UI, rather than as a prop on the menu: the
|
||||
// caller owns its row. Both read the same preference (`../prefs.js`), which
|
||||
|
|
@ -11,7 +11,7 @@ export function HideDebugSetting() {
|
|||
const [hideDebug, setHideDebugState] = useState(() => getHideDebugPref());
|
||||
return (
|
||||
<label class="settings-menu-row">
|
||||
<span>hide debug output</span>
|
||||
<span>hide developer details</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hideDebug}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ function DiffBody({ text }: { text: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
// A `debug` row, which follows the "hide debug output" preference as it
|
||||
// A `debug` row, which follows the "hide developer details" preference as it
|
||||
// changes. Only debug rows subscribe, so a long terminal does not hold a
|
||||
// listener per row.
|
||||
function DebugRow({ row }: { row: TermRow }) {
|
||||
|
|
@ -67,7 +67,7 @@ function DebugRow({ row }: { row: TermRow }) {
|
|||
() => subscribeHideDebugPref(() => setHidden(getHideDebugPref())),
|
||||
[],
|
||||
);
|
||||
// "hide debug output" is a skip, not a dimmer — a hidden debug row
|
||||
// "hide developer details" is a skip, not a dimmer — a hidden debug row
|
||||
// still occupies no DOM node at all, same as it never streamed, rather
|
||||
// than a CSS `display: none` that would keep it in the layout/DOM for
|
||||
// no benefit.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js";
|
|||
import { NotificationsPage } from "./pages/notifications/NotificationsPage.js";
|
||||
import { ComponentsPage } from "./pages/ComponentsPage.js";
|
||||
import { JobsPage } from "./pages/JobsPage.js";
|
||||
import { HivesPage } from "./pages/HivesPage.js";
|
||||
import { IssueReportPage } from "./pages/IssueReportPage.js";
|
||||
import { ReviewPage } from "./pages/review/ReviewPage.js";
|
||||
import { SchedulesPage } from "./pages/schedules/SchedulesPage.js";
|
||||
|
|
@ -34,7 +33,6 @@ export function App() {
|
|||
<Shell>
|
||||
<Switch>
|
||||
<Route path="/" component={StartPage} />
|
||||
<Route path="/hives" component={HivesPage} />
|
||||
<Route path="/alerts" component={AlertsPage} />
|
||||
<Route path="/agents" component={AgentsPage} />
|
||||
<Route path="/agents/:name/terminal" component={AgentTerminalPage} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// <StartPage> — `/`, the swarm's landing page: the swarm's name over the
|
||||
// digital-rain backdrop, plus the setup assistant while the swarm has no
|
||||
// `ruth`. The header's swarm name links here. The hive roster that used to be
|
||||
// `/` is now `/hives`.
|
||||
// `ruth`. The header's swarm name links here. The hive roster is a card on the agents page.
|
||||
import { MatrixRain } from "../shell/MatrixRain.js";
|
||||
import { useSwarmName } from "../shell/useSwarmName.js";
|
||||
import { SetupAssistant } from "./SetupAssistant.js";
|
||||
|
|
|
|||
|
|
@ -36,3 +36,14 @@
|
|||
.ui-agents-view-toggle button:not(.active):hover {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* The folded hives card over the agents and details panels. */
|
||||
.ui-agents-hives {
|
||||
margin-block-end: 1em;
|
||||
}
|
||||
/* The roster's refresh picker, at the top right of the open card. */
|
||||
.ui-hives-refresh {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { Button } from "../../ui/button/Button.js";
|
|||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||
import { FilterableView } from "../../ui/filterable-view/FilterableView.js";
|
||||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import { HivesCard } from "./HivesCard.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
|
|
@ -255,6 +256,9 @@ export function AgentsPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div class="ui-agents-hives">
|
||||
<HivesCard />
|
||||
</div>
|
||||
<SplitView
|
||||
primary={
|
||||
<Panel
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// <HivesPage> — the hive roster, `/hives`' content. Fetches
|
||||
// <HivesCard> — the hive roster, a card on the agents page that starts folded.
|
||||
// Fetches
|
||||
// swarm-controller's `GET /api/hives/status`, the swarm-wide status
|
||||
// aggregate: one row per roster hive, freshness derived at read time
|
||||
// from the status bucket. Supersedes the earlier `GET /api/hives` +
|
||||
|
|
@ -15,15 +16,15 @@
|
|||
import { useState } from "preact/hooks";
|
||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
||||
import { Disclosure } from "../../ui/disclosure/Disclosure.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
type RefreshIntervalMs,
|
||||
} from "../ui/refresh-interval/RefreshInterval.js";
|
||||
import { fetchJson, usePolled } from "../ui/polled/usePolled.js";
|
||||
import { Table, type TableColumn } from "../ui/table/Table.js";
|
||||
import { domainCell } from "./hive-domain.js";
|
||||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
||||
import { domainCell } from "../hive-domain.js";
|
||||
|
||||
type Freshness = "fresh" | "stale" | "never_reported" | "unknown";
|
||||
|
||||
|
|
@ -103,14 +104,16 @@ const COLUMNS: TableColumn<HiveStatus>[] = [
|
|||
},
|
||||
];
|
||||
|
||||
// 30s default: this page has no inputs to interrupt, and the whole
|
||||
// 30s default: this card 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() {
|
||||
// The roster is read on the first open and followed after, so an agents page
|
||||
// where the card is never opened does not poll it.
|
||||
function HiveRoster() {
|
||||
const [intervalMs, setIntervalMs] =
|
||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||
const { data: hives, error } = usePolled(intervalMs, (signal) =>
|
||||
|
|
@ -118,17 +121,14 @@ export function HivesPage() {
|
|||
);
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="hives"
|
||||
icon="🐝"
|
||||
actions={
|
||||
<>
|
||||
<div class="ui-hives-refresh">
|
||||
<RefreshIntervalPicker
|
||||
id="hives-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
}
|
||||
>
|
||||
</div>
|
||||
{error ? (
|
||||
<ApiErrorPanel
|
||||
context="failed to load the hive roster"
|
||||
|
|
@ -145,6 +145,25 @@ export function HivesPage() {
|
|||
storageKey="swarm-ui:hives:table-filters"
|
||||
/>
|
||||
) : null}
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HivesCard() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [built, setBuilt] = useState(false);
|
||||
return (
|
||||
<Disclosure
|
||||
title="hives"
|
||||
icon="🐝"
|
||||
hint="the swarm's hives and when each last reported"
|
||||
open={open}
|
||||
onToggle={(next) => {
|
||||
setOpen(next);
|
||||
if (next) setBuilt(true);
|
||||
}}
|
||||
>
|
||||
{built ? <HiveRoster /> : null}
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import type { ComponentChildren } from "preact";
|
|||
import { Link, useLocation } from "wouter-preact";
|
||||
import { LinksMenu } from "./LinksMenu.js";
|
||||
import { UserMenu } from "./UserMenu.js";
|
||||
import { useHideDebug } from "./useHideDebug.js";
|
||||
import { SettingsMenu } from "@hive/shared/settings-menu.js";
|
||||
import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
|
||||
import { useApplyThemeOverride } from "@hive/shared/theme-apply.js";
|
||||
|
|
@ -49,7 +50,6 @@ const THEME_KEY = "swarm-ui:theme-override";
|
|||
const MOTION_KEY = "swarm-ui:motion-override";
|
||||
|
||||
const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
||||
{ href: "/hives", label: "hives", accent: "var(--purple)" },
|
||||
{ href: "/agents", label: "agents", accent: "var(--green)" },
|
||||
{ href: "/alerts", label: "alerts", accent: "var(--red)" },
|
||||
{ href: "/jobs", label: "jobs", accent: "var(--pink)" },
|
||||
|
|
@ -209,6 +209,13 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
// rather than reusing `NavLink`.
|
||||
const { tabs, close: closeTab } = useDynamicTabs();
|
||||
|
||||
// The components tab is a developer page: it goes while "hide developer
|
||||
// details" is on. The page itself still answers at `/components`.
|
||||
const hideDeveloper = useHideDebug();
|
||||
const navItems = hideDeveloper
|
||||
? NAV_ITEMS.filter((item) => item.href !== "/components")
|
||||
: NAV_ITEMS;
|
||||
|
||||
// Applied once here, not inside `SettingsMenu` — every route mounts
|
||||
// through this one `<Shell>`, so the override takes effect regardless
|
||||
// of which page is showing or whether the menu's ever been opened,
|
||||
|
|
@ -226,7 +233,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
// scroll-offset bookkeeping needed.
|
||||
function measureIndex(index: number): IndicatorRect | null {
|
||||
const navEl = navRef.current;
|
||||
const item = NAV_ITEMS[index];
|
||||
const item = navItems[index];
|
||||
const textEl = item ? textRefs.current[item.href] : null;
|
||||
if (!navEl || !item || !textEl) return null;
|
||||
const navRect = navEl.getBoundingClientRect();
|
||||
|
|
@ -245,8 +252,14 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
// [c's colour]"). Re-runs on navigation; a separate resize listener
|
||||
// below re-measures the already-settled position without re-hopping
|
||||
// (a reflow isn't a navigation).
|
||||
// The list changed (the components tab came or went), so the settled index
|
||||
// no longer names the same item: land directly on the active one again.
|
||||
useEffect(() => {
|
||||
const targetIndex = NAV_ITEMS.findIndex((item) => item.href === location);
|
||||
settledIndexRef.current = -1;
|
||||
}, [hideDeveloper]);
|
||||
|
||||
useEffect(() => {
|
||||
const targetIndex = navItems.findIndex((item) => item.href === location);
|
||||
if (targetIndex === -1) {
|
||||
settledIndexRef.current = -1;
|
||||
setIndicator(null);
|
||||
|
|
@ -278,7 +291,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
cancelled = true;
|
||||
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [location]);
|
||||
}, [location, hideDeveloper]);
|
||||
|
||||
// Fires the render *after* the indicator's first non-null commit —
|
||||
// deliberately one tick behind, not folded into the effect above,
|
||||
|
|
@ -323,7 +336,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
// label its page registered as a dynamic tab. An unmatched path gets
|
||||
// the bare brand.
|
||||
const pageLabel =
|
||||
NAV_ITEMS.find((item) => item.href === location)?.label ??
|
||||
navItems.find((item) => item.href === location)?.label ??
|
||||
tabs.find((tab) => tab.href === location)?.label;
|
||||
useEffect(() => {
|
||||
document.title = pageLabel ? `${pageLabel} · ${brand}` : brand;
|
||||
|
|
@ -355,7 +368,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
</span>
|
||||
</Link>
|
||||
<nav class="shell-nav" ref={navRef}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
|
|
|
|||
17
frontend/packages/swarm-ui/src/shell/useHideDebug.ts
Normal file
17
frontend/packages/swarm-ui/src/shell/useHideDebug.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// The "hide developer details" preference as a value that follows its toggle:
|
||||
// this tab's, through `setHideDebugPref`, and another tab's, through the
|
||||
// `storage` event. The header uses it to hide the components tab.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import {
|
||||
getHideDebugPref,
|
||||
subscribeHideDebugPref,
|
||||
} from "@hive/shared/prefs.js";
|
||||
|
||||
export function useHideDebug(): boolean {
|
||||
const [hide, setHide] = useState(getHideDebugPref);
|
||||
useEffect(
|
||||
() => subscribeHideDebugPref(() => setHide(getHideDebugPref())),
|
||||
[],
|
||||
);
|
||||
return hide;
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ test("the start page is full-bleed", () => {
|
|||
});
|
||||
|
||||
test("the hive directory moved off / and is not full-bleed", () => {
|
||||
assert.equal(isFullBleedRoute("/hives"), false);
|
||||
assert.equal(isFullBleedRoute("/agents"), false);
|
||||
});
|
||||
|
||||
test("an agent's terminal and a subagent's are full-bleed", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue