A placing state is accepted only for an agent declared or queued on the hive; anything else gets 404 telling the caller to create it at swarm level. swarm-ui's wanted badge no longer offers a first declaration for an undeclared row.
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
// <WantedMenu> — 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 `<dialog>` (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<HTMLDivElement>(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 (
|
|
<div class="ui-dropdown-anchor" ref={anchorRef}>
|
|
<Badge
|
|
tone={tone}
|
|
// `destroyed` and an undeclared row block opening the menu at
|
|
// all — a UX-only guard, the server enforces both (see
|
|
// `declareState`'s caller).
|
|
onClick={canDeclare ? () => 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}`
|
|
}
|
|
/>
|
|
<Dropdown
|
|
open={open}
|
|
portal={portal}
|
|
options={options}
|
|
activeValue={row.wanted ?? undefined}
|
|
label={`declare ${row.name}`}
|
|
onSelect={(value) => {
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|