// — the "wanted" state control: one badge showing the // current declaration, opening a `Dropdown` with the four explicit // states. An agent with no declaration has none to change: its badge is // disabled and points at creating it at swarm level, because the state // route refuses an agent the swarm never declared on that hive. // Presentational only, same split as `StatusChips`'s // `StatusMenu`: the caller owns what each selection actually does. // // "paused" has no separate resume option here — selecting "up" from a // paused row is the resume, same PUT either way (`AgentsPage`'s // `declareState`, `hive-c0re`'s `decide_pause` backend counterpart, // treats a declared `Up` with the marker still set as "clear it"). A // dedicated "resume" entry would just be a second spelling of the // option already above it. import { useRef, useState } from "preact/hooks"; import { Badge, type BadgeTone } from "@hive/shared/badge.js"; import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js"; import type { AgentRow } from "../AgentTypes.js"; export function WantedMenu({ row, pending, onSelectUp, onSelectOffline, onSelectPaused, onDestroy, showDestroy = true, portal = false, }: { row: AgentRow; pending: boolean; onSelectUp: (row: AgentRow) => void; onSelectOffline: (row: AgentRow) => void; onSelectPaused: (row: AgentRow) => void; onDestroy?: (row: AgentRow) => void; /** * The card view (see `AgentCard`) passes `false` here — its * quick-access menu keeps to the three everyday states, so "destroy" * can't be reached by an extra click off a state that's already open. * `AgentsPage`'s detail-panel `WantedMenu` keeps the default (`true`) * — mara: "destroy is already available via wanted state", i.e. the * full menu there, not a separate button. */ showDestroy?: boolean; /** * Forwarded to `Dropdown`'s own `portal` (see its file-top comment) — * only the table's "wanted" column needs it, to escape * `.ui-table-scroll`'s clip on the table's last row (the bug `Dropdown`'s * own `portal` prop exists to fix in the first place). * Neither the card list nor the detail panel has a clipping ancestor, * and inside the detail panel specifically `portal` is actively wrong: * a `position: fixed` element appended to `document.body` renders * *behind* an open native `` (the dialog is promoted to the * browser's top layer, which composites above ordinary body content * regardless of z-index) — found while screenshotting this exact * dropdown open inside the detail panel, only the portion extending * past the dialog's own edge was visible. */ portal?: boolean; }) { const [open, setOpen] = useState(false); const anchorRef = useRef(null); const destroyed = row.wanted === "destroyed"; const undeclared = !row.wanted; const canDeclare = !!row.hive && !destroyed && !undeclared; const tone: BadgeTone = row.wanted === "up" ? "positive" : row.wanted === "paused" ? "warning" : destroyed ? "negative" : "neutral"; const options: DropdownOption[] = [ { value: "up", label: "up" }, { value: "paused", label: "paused" }, { value: "offline", label: "offline" }, ...(showDestroy ? [{ value: "destroy", label: "destroy", danger: true }] : []), ]; return (
setOpen((o) => !o) : undefined} expanded={open} disabled={pending || !canDeclare} title={ destroyed ? `${row.name} is destroyed — redeploy via "+ agent" to bring it back` : !row.hive ? "no hive on record for this agent — nothing to declare against" : undeclared ? `${row.name} is not declared at swarm level — create it there` : `declare a new state for ${row.name}` } /> { setOpen(false); if (value === "up") onSelectUp(row); else if (value === "offline") onSelectOffline(row); else if (value === "paused") onSelectPaused(row); else onDestroy?.(row); }} onClose={() => setOpen(false)} anchorRef={anchorRef} />
); }