swarm-ui: show an agent's details in a popover, not a modal dialog
The details open under the agent badge without dimming or blocking the page; a click outside or Escape closes it. New ui/popover primitive (anchored, portal, no backdrop) with a sample on the components page. A confirm or link dialog opened from inside it stays modal, and the popover ignores clicks and Escape that belong to that dialog. AgentDetailDialog becomes AgentDetailPopover.
This commit is contained in:
parent
74e588415b
commit
f957e19d0d
7 changed files with 221 additions and 42 deletions
|
|
@ -1,29 +1,34 @@
|
|||
// <AgentDetailDialog> — the agent detail panel from `/agents`, in a dialog, for
|
||||
// a page that is already showing the agent's terminal and should not be left
|
||||
// to read or change its details. It carries the same body (`AgentDetail`) and
|
||||
// the same actions (`useAgentActions`) as the panel, minus the terminal
|
||||
// preview the page itself already is.
|
||||
// <AgentDetailPopover> — the agent detail panel from `/agents`, in a popover
|
||||
// under its anchor, for a page that is already showing the agent's terminal
|
||||
// and should not be left, or dimmed, to read or change its details. It carries
|
||||
// the same body (`AgentDetail`) and the same actions (`useAgentActions`) as the
|
||||
// panel, minus the terminal preview the page itself already is.
|
||||
//
|
||||
// The row comes from `GET /api/agents/status` when the dialog opens, found by
|
||||
// The row comes from `GET /api/agents/status` when the popover opens, found by
|
||||
// name: the panel on `/agents` has the whole roster in hand, this page does
|
||||
// not. A wanted-state change patches the row in place from the PUT's answer.
|
||||
// The confirm and link dialogs those actions open stay modal: they hold a
|
||||
// decision, and the popover ignores clicks inside them.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import type { RefObject } from "preact";
|
||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import { Popover } from "../../ui/popover/Popover.js";
|
||||
import { useAgentActions } from "../roster/AgentActions.js";
|
||||
import { AgentDetail } from "./AgentDetail.js";
|
||||
import type { AgentRow } from "../AgentTypes.js";
|
||||
|
||||
export function AgentDetailDialog({
|
||||
export function AgentDetailPopover({
|
||||
agent,
|
||||
open,
|
||||
onClose,
|
||||
anchorRef,
|
||||
}: {
|
||||
agent: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
anchorRef: RefObject<HTMLElement>;
|
||||
}) {
|
||||
// `undefined` until the roster has answered; `null` when it has and the
|
||||
// agent is not in it.
|
||||
|
|
@ -36,7 +41,7 @@ export function AgentDetailDialog({
|
|||
}
|
||||
});
|
||||
|
||||
// Fetched on each open, so what the dialog shows is current rather than
|
||||
// Fetched on each open, so what the popover shows is current rather than
|
||||
// what the page last saw.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
|
@ -61,24 +66,27 @@ export function AgentDetailDialog({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onClose={onClose} label={`${agent} details`} plain>
|
||||
{open ? (
|
||||
<Panel title={agent} icon="🔎" onClose={onClose}>
|
||||
{error ? (
|
||||
<ApiErrorPanel
|
||||
context="failed to load the agent's details"
|
||||
problem={error}
|
||||
/>
|
||||
) : row ? (
|
||||
<AgentDetail row={row} actions={actions} />
|
||||
) : row === null ? (
|
||||
<p>{agent} is not in the swarm's agent roster</p>
|
||||
) : (
|
||||
<p>loading…</p>
|
||||
)}
|
||||
</Panel>
|
||||
) : null}
|
||||
</Dialog>
|
||||
<Popover
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
anchorRef={anchorRef}
|
||||
label={`${agent} details`}
|
||||
>
|
||||
<Panel title={agent} icon="🔎" onClose={onClose}>
|
||||
{error ? (
|
||||
<ApiErrorPanel
|
||||
context="failed to load the agent's details"
|
||||
problem={error}
|
||||
/>
|
||||
) : row ? (
|
||||
<AgentDetail row={row} actions={actions} />
|
||||
) : row === null ? (
|
||||
<p>{agent} is not in the swarm's agent roster</p>
|
||||
) : (
|
||||
<p>loading…</p>
|
||||
)}
|
||||
</Panel>
|
||||
</Popover>
|
||||
{actions.dialogs}
|
||||
</>
|
||||
);
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
// its wanted state (up / offline / paused / destroyed) and link an account to
|
||||
// it, with the confirm and form dialogs those need. Shared by the agent
|
||||
// roster (`AgentsPage`) and the detail dialog on a terminal page
|
||||
// (`AgentDetailDialog`), so a state change reads the same from either; each
|
||||
// (`AgentDetailPopover`), so a state change reads the same from either; each
|
||||
// caller renders the returned `dialogs` once.
|
||||
//
|
||||
// "offline" and "paused" are the *same* confirm shape — same button
|
||||
|
|
|
|||
Loading…
Reference in a new issue