swarm-ui/agents: detail panel shows full agent state, fix dropdown-behind-dialog bug
Three things from mara's PR review + argus's: - "the info from main list should be included in the agent view" - detail panel now repeats status/message/wanted alongside the panel-only fields (hive, config PR, matrix link), not just the leftovers. - "destroy is already available via wanted state" - dropped the standalone "destroy agent" button; the detail panel's wanted field is a real WantedMenu (default showDestroy) instead, same control as the card/table, just with the fourth option back. Also resolves argus's stale-doc-comment finding (the comment described a second WantedMenu call site that didn't exist yet - now it does). - Rebased onto main to pick up the just-merged dropdown-portal-clip fix - this branch was cut before that merged, so it had silently regressed back to the pre-fix Dropdown the whole time. That rebase surfaced a real bug of its own, likely "the third screenshot shows a layout bug": WantedMenu always passed `portal` to Dropdown, and a portaled (position: fixed, body-appended) dropdown 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. Only the sliver of the dropdown extending past the dialog's own edge was visible. WantedMenu's `portal` is now its own prop, opt-in, true only at the table's call site (the one with an actual clipping ancestor to escape) - card and detail-panel call sites render it as a plain child instead, which is both correct inside the dialog and one fewer moving part where it isn't needed. Verified with real CDP clicks: detail panel shows all fields, and its wanted dropdown now renders in the right place with all four options visible instead of mostly hidden behind the dialog.
This commit is contained in:
parent
1e6156d116
commit
8786f24111
1 changed files with 99 additions and 43 deletions
|
|
@ -174,6 +174,7 @@ function WantedMenu({
|
|||
onSelectPaused,
|
||||
onDestroy,
|
||||
showDestroy = true,
|
||||
portal = false,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
pending: boolean;
|
||||
|
|
@ -182,15 +183,29 @@ function WantedMenu({
|
|||
onSelectPaused: (row: AgentRow) => void;
|
||||
onDestroy?: (row: AgentRow) => void;
|
||||
/**
|
||||
* The card view (see `AgentCard` below) keeps this menu to the
|
||||
* three everyday states and gives "destroy" — the rare, one-way
|
||||
* one — its own button in the detail panel instead, so it can't be
|
||||
* reached by an extra click off a state that's already open (mara,
|
||||
* scoping the card/detail split: destroy belongs with the "rarely
|
||||
* used actions" bucket, not next to up/paused/offline). The
|
||||
* `WantedMenu` inside that detail panel still gets the full set.
|
||||
* The card view (see `AgentCard` below) 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);
|
||||
|
|
@ -234,7 +249,7 @@ function WantedMenu({
|
|||
/>
|
||||
<Dropdown
|
||||
open={open}
|
||||
portal
|
||||
portal={portal}
|
||||
options={options}
|
||||
activeValue={row.wanted ?? undefined}
|
||||
label={`declare ${row.name}`}
|
||||
|
|
@ -255,13 +270,14 @@ function WantedMenu({
|
|||
// One card per roster agent (mara: "main view: name, status, message,
|
||||
// wanted" / "message as second line" / "more like card per agent") —
|
||||
// the `viewMode === "cards"` alternative to the original `Table`, not
|
||||
// its replacement (see `AgentsPage`'s own comment). Everything the
|
||||
// table's other columns carried (hive, matrix link-account, config-PR
|
||||
// link, destroy) moves to a detail panel on card click instead — a
|
||||
// plain `Dialog`, not the shared `hive-side-panel` slide-in drawer:
|
||||
// that's a shadow-DOM custom element, and swarm-ui's esbuild config
|
||||
// can't consume those yet (same gap `Dialog.tsx`'s own comment already
|
||||
// flags for `hive-dialog`). Revisit once that gap closes.
|
||||
// its replacement (see `AgentsPage`'s own comment). Clicking a card
|
||||
// opens a detail panel repeating this same info plus what doesn't fit
|
||||
// here (hive, config-PR link, matrix link-account) — mara: "the info
|
||||
// from main list should be included in the agent view" too, not just
|
||||
// the leftovers. A plain `Dialog`, not the shared `hive-side-panel`
|
||||
// slide-in drawer: that's a shadow-DOM custom element, and swarm-ui's
|
||||
// esbuild config can't consume those yet (same gap `Dialog.tsx`'s own
|
||||
// comment already flags for `hive-dialog`). Revisit once that closes.
|
||||
//
|
||||
// A `role="button"` div, not a real `<button>`: the card also hosts the
|
||||
// real `<button>`s inside `WantedMenu`, and nested buttons are invalid
|
||||
|
|
@ -404,9 +420,8 @@ export function AgentsPage() {
|
|||
// confirmation of a `declareState` call, it's an unrelated action with
|
||||
// its own form (`LinkMatrixAccountForm`).
|
||||
const [matrixTarget, setMatrixTarget] = useState<AgentRow | null>(null);
|
||||
// The row showing the detail panel (hive, matrix link-account,
|
||||
// config-PR link, destroy — everything `AgentCard`'s main view
|
||||
// doesn't) — same null-means-closed shape as the two above.
|
||||
// The row showing the detail panel (see the `Dialog` below for what
|
||||
// it holds) — same null-means-closed shape as the two above.
|
||||
const [detailTarget, setDetailTarget] = useState<AgentRow | null>(null);
|
||||
|
||||
async function refresh() {
|
||||
|
|
@ -509,6 +524,16 @@ export function AgentsPage() {
|
|||
await declareState(row, "destroyed");
|
||||
}
|
||||
|
||||
// The three non-destroy `WantedMenu` selections, factored out once
|
||||
// rather than repeated per call site — the card, the table's "wanted"
|
||||
// column, and the detail panel's own `WantedMenu` (below) all wire the
|
||||
// same three.
|
||||
const selectUp = (row: AgentRow) => void declareState(row, "up");
|
||||
const selectOffline = (row: AgentRow) =>
|
||||
setConfirmTarget({ row, state: "offline" });
|
||||
const selectPaused = (row: AgentRow) =>
|
||||
setConfirmTarget({ row, state: "paused" });
|
||||
|
||||
// `viewMode === "table"`'s columns — the full field set (including
|
||||
// per-column sort/filter, which the card view doesn't have) mara asked
|
||||
// to keep. Table's own `WantedMenu` keeps the default `showDestroy`
|
||||
|
|
@ -581,13 +606,10 @@ export function AgentsPage() {
|
|||
<WantedMenu
|
||||
row={a}
|
||||
pending={pendingAgents.has(a.name)}
|
||||
onSelectUp={(row) => void declareState(row, "up")}
|
||||
onSelectOffline={(row) =>
|
||||
setConfirmTarget({ row, state: "offline" })
|
||||
}
|
||||
onSelectPaused={(row) =>
|
||||
setConfirmTarget({ row, state: "paused" })
|
||||
}
|
||||
portal
|
||||
onSelectUp={selectUp}
|
||||
onSelectOffline={selectOffline}
|
||||
onSelectPaused={selectPaused}
|
||||
onDestroy={setDestroyTarget}
|
||||
/>
|
||||
{err ? (
|
||||
|
|
@ -700,13 +722,9 @@ export function AgentsPage() {
|
|||
row={a}
|
||||
pending={pendingAgents.has(a.name)}
|
||||
error={actionErrors.get(a.name)}
|
||||
onSelectUp={(row) => void declareState(row, "up")}
|
||||
onSelectOffline={(row) =>
|
||||
setConfirmTarget({ row, state: "offline" })
|
||||
}
|
||||
onSelectPaused={(row) =>
|
||||
setConfirmTarget({ row, state: "paused" })
|
||||
}
|
||||
onSelectUp={selectUp}
|
||||
onSelectOffline={selectOffline}
|
||||
onSelectPaused={selectPaused}
|
||||
onOpenDetail={setDetailTarget}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -725,12 +743,59 @@ export function AgentsPage() {
|
|||
onClose={() => setDetailTarget(null)}
|
||||
label={detailTarget ? `${detailTarget.name} details` : "agent details"}
|
||||
>
|
||||
{/* Everything `AgentCard`'s main view doesn't already show — see
|
||||
that component's own comment for the field split. */}
|
||||
{/* Mara: "the info from main list should be included in the
|
||||
agent view" — so this repeats status/message/wanted rather
|
||||
than showing only what `AgentCard`'s main view doesn't, plus
|
||||
hive/config-PR/matrix-link, which really are panel-only. */}
|
||||
{detailTarget ? (
|
||||
<div class="ui-agent-detail">
|
||||
<h2 class="ui-agent-detail-name">{detailTarget.name}</h2>
|
||||
<dl class="ui-agent-detail-fields">
|
||||
<dt>status</dt>
|
||||
<dd>
|
||||
{(() => {
|
||||
const { tone, label } = FRESHNESS[detailTarget.freshness];
|
||||
return (
|
||||
<Badge
|
||||
tone={tone}
|
||||
value={
|
||||
<>
|
||||
{label}
|
||||
{detailTarget.last_seen_unix !== null ? (
|
||||
<>
|
||||
{" "}
|
||||
(
|
||||
<RelativeTime
|
||||
epochMs={detailTarget.last_seen_unix * 1000}
|
||||
/>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</dd>
|
||||
<dt>message</dt>
|
||||
<dd>{detailTarget.snapshot?.status_text ?? "—"}</dd>
|
||||
<dt>wanted</dt>
|
||||
<dd>
|
||||
{/* Full menu (destroy included) — mara: "destroy is
|
||||
already available via wanted state", no separate
|
||||
button needed. */}
|
||||
<WantedMenu
|
||||
row={detailTarget}
|
||||
pending={pendingAgents.has(detailTarget.name)}
|
||||
onSelectUp={selectUp}
|
||||
onSelectOffline={selectOffline}
|
||||
onSelectPaused={selectPaused}
|
||||
onDestroy={(row) => {
|
||||
setDestroyTarget(row);
|
||||
setDetailTarget(null);
|
||||
}}
|
||||
/>
|
||||
</dd>
|
||||
<dt>hive</dt>
|
||||
<dd>{detailTarget.hive ?? "—"}</dd>
|
||||
<dt>config PR</dt>
|
||||
|
|
@ -777,15 +842,6 @@ export function AgentsPage() {
|
|||
: "no hive on record for this agent — nothing to link against"
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
disabled={detailTarget.wanted === "destroyed"}
|
||||
onClick={() => {
|
||||
setDestroyTarget(detailTarget);
|
||||
setDetailTarget(null);
|
||||
}}
|
||||
>
|
||||
destroy agent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Reference in a new issue