swarm-ui: use ConfirmDialog for the stop confirmation too
mara: "use the new component where we already have confirm dialogs." The stop/offline direction of the wanted toggle used a native window.confirm -- the reasoning at the time was that a hand-rolled Dialog felt heavy for a reversible action, with the real Dialog confirm reserved for the irreversible destroy direction. Now that ConfirmDialog is a one-line-per-caller shared component, that argument no longer buys consistency anything, so both directions confirm the same way.
This commit is contained in:
parent
82a4324b17
commit
2874badd4c
1 changed files with 48 additions and 22 deletions
|
|
@ -25,9 +25,9 @@
|
||||||
// itself (`CreateAgentForm`) mounts inside a `Dialog` here rather than
|
// itself (`CreateAgentForm`) mounts inside a `Dialog` here rather than
|
||||||
// its own route.
|
// its own route.
|
||||||
//
|
//
|
||||||
// The "wanted" column is the start/stop control — see its own `render`
|
// The "wanted" column is the start/stop control (`toggleWanted`) plus a
|
||||||
// and `toggleWanted` below for how. Destroy is a separate control next
|
// separate destroy control (`destroyAgent`) — both confirm via the
|
||||||
// to it — see `destroyTarget`/`destroyAgent`.
|
// shared `ui/confirm-dialog`.
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||||
|
|
@ -119,8 +119,13 @@ export function AgentsPage() {
|
||||||
>(new Map());
|
>(new Map());
|
||||||
// The row pending destroy confirmation, `null` when the dialog is
|
// The row pending destroy confirmation, `null` when the dialog is
|
||||||
// closed — not a boolean, so the dialog can name the agent without a
|
// closed — not a boolean, so the dialog can name the agent without a
|
||||||
// second piece of state to keep in sync with it.
|
// second piece of state to keep in sync with it. `stopTarget` mirrors
|
||||||
|
// it for the "declare offline" direction of the same toggle — two
|
||||||
|
// separate pieces of state (not one "pending confirm" union) since
|
||||||
|
// both `ConfirmDialog`s can't be open at once anyway and keeping them
|
||||||
|
// apart means each one's JSX below reads standalone.
|
||||||
const [destroyTarget, setDestroyTarget] = useState<AgentRow | null>(null);
|
const [destroyTarget, setDestroyTarget] = useState<AgentRow | null>(null);
|
||||||
|
const [stopTarget, setStopTarget] = useState<AgentRow | null>(null);
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const res = await fetch("/api/agents/status");
|
const res = await fetch("/api/agents/status");
|
||||||
|
|
@ -139,11 +144,11 @@ export function AgentsPage() {
|
||||||
refresh().catch((e: unknown) => setError({ detail: String(e) }));
|
refresh().catch((e: unknown) => setError({ detail: String(e) }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Shared by `toggleWanted` and `destroyAgent` — both are "PUT a new
|
// Shared by `confirmStop` and `destroyAgent` — both are "PUT a new
|
||||||
// `wanted` declaration, track per-agent pending/error state, patch the
|
// `wanted` declaration, track per-agent pending/error state, patch the
|
||||||
// response back into `rows`" with nothing else distinguishing them.
|
// response back into `rows`" with nothing else distinguishing them.
|
||||||
// Confirmation (native vs. `Dialog`) and target-state selection stay
|
// Which `ConfirmDialog` fires it and the target state stay in each
|
||||||
// in each caller, since those are the parts that actually differ.
|
// caller, since those are the parts that actually differ.
|
||||||
async function declareState(row: AgentRow, target: string) {
|
async function declareState(row: AgentRow, target: string) {
|
||||||
if (!row.hive) return;
|
if (!row.hive) return;
|
||||||
setPendingAgents((prev) => new Set(prev).add(row.name));
|
setPendingAgents((prev) => new Set(prev).add(row.name));
|
||||||
|
|
@ -201,31 +206,38 @@ export function AgentsPage() {
|
||||||
// target is read off the agent's own last-reported `running` instead
|
// target is read off the agent's own last-reported `running` instead
|
||||||
// — the button's first click always means "make the declaration match
|
// — the button's first click always means "make the declaration match
|
||||||
// reality, then flip it", which is the only reading that makes sense
|
// reality, then flip it", which is the only reading that makes sense
|
||||||
// without a declaration to toggle.
|
// without a declaration to toggle. The "up" direction needs no
|
||||||
|
// confirmation and declares straight away; "offline" opens the
|
||||||
|
// `stopTarget` `ConfirmDialog` instead of declaring directly — see
|
||||||
|
// `confirmStop`.
|
||||||
async function toggleWanted(row: AgentRow) {
|
async function toggleWanted(row: AgentRow) {
|
||||||
if (!row.hive) return;
|
if (!row.hive) return;
|
||||||
const impliedCurrent =
|
const impliedCurrent =
|
||||||
row.wanted ?? (row.snapshot?.running ? "up" : "offline");
|
row.wanted ?? (row.snapshot?.running ? "up" : "offline");
|
||||||
const target = impliedCurrent === "up" ? "offline" : "up";
|
const target = impliedCurrent === "up" ? "offline" : "up";
|
||||||
if (
|
if (target === "offline") {
|
||||||
target === "offline" &&
|
setStopTarget(row);
|
||||||
// Native confirm, not a `Dialog` — stop is reversible (a later
|
|
||||||
// start un-does it), so the lighter-weight native prompt is
|
|
||||||
// proportionate here. Destroy is the irreversible direction and
|
|
||||||
// gets the real `Dialog` confirm instead — see `destroyAgent`.
|
|
||||||
!window.confirm(
|
|
||||||
`Declare ${row.name} offline? The hive brings its container down on its next reconcile sweep.`,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await declareState(row, target);
|
await declareState(row, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fires from the confirm `Dialog`, never directly off a row click —
|
// Fires from the `stopTarget` confirm `Dialog`. Used to be a native
|
||||||
// see `destroyTarget`/the "destroy" badge in the `wanted` column.
|
// `window.confirm` (stop is reversible, a later start un-does it, so a
|
||||||
// Unlike `toggleWanted`, there's no "current state" to read: destroy
|
// lighter-weight prompt than destroy's felt proportionate) — mara,
|
||||||
// is a one-way declaration, not a flip.
|
// reviewing the destroy confirm: use the shared component everywhere
|
||||||
|
// we already confirm before acting, not just for destroy. Once
|
||||||
|
// `ConfirmDialog` existed as a one-line-per-caller component, the
|
||||||
|
// "native is lighter" argument no longer bought consistency anything.
|
||||||
|
async function confirmStop(row: AgentRow) {
|
||||||
|
setStopTarget(null);
|
||||||
|
await declareState(row, "offline");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fires from the `destroyTarget` confirm `Dialog`, never directly off
|
||||||
|
// a row click — see `destroyTarget`/the "destroy" badge in the
|
||||||
|
// `wanted` column. Unlike `toggleWanted`, there's no "current state"
|
||||||
|
// to read: destroy is a one-way declaration, not a flip.
|
||||||
async function destroyAgent(row: AgentRow) {
|
async function destroyAgent(row: AgentRow) {
|
||||||
setDestroyTarget(null);
|
setDestroyTarget(null);
|
||||||
await declareState(row, "destroyed");
|
await declareState(row, "destroyed");
|
||||||
|
|
@ -415,6 +427,20 @@ export function AgentsPage() {
|
||||||
>
|
>
|
||||||
<CreateAgentForm />
|
<CreateAgentForm />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={stopTarget !== null}
|
||||||
|
label="stop agent"
|
||||||
|
onCancel={() => setStopTarget(null)}
|
||||||
|
onConfirm={() => stopTarget && void confirmStop(stopTarget)}
|
||||||
|
confirmLabel="stop"
|
||||||
|
>
|
||||||
|
{stopTarget ? (
|
||||||
|
<p>
|
||||||
|
Declare <strong>{stopTarget.name}</strong> offline? The hive brings
|
||||||
|
its container down on its next reconcile sweep.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</ConfirmDialog>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={destroyTarget !== null}
|
open={destroyTarget !== null}
|
||||||
label="destroy agent"
|
label="destroy agent"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue