Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: an egress card on the swarm page for the swarm-wide allowed domains, sharing the agent card's add form and list hook

This commit is contained in:
iris 2026-11-04 21:14:43 +01:00
commit ad76e7e937
3 changed files with 254 additions and 84 deletions

View file

@ -0,0 +1,114 @@
// <EgressCard> — the names every agent in the swarm may reach through its
// hive's egress proxy: the swarm-wide allowed domains, with an add box and a
// remove beside each. A card that starts folded and reads nothing until it is
// opened.
//
// The entries are the ones of `GET /api/egress/allowed-domains` that hold for
// every agent (an agent's own entries stay on its card). They add to the names
// each hive's own configuration allows. An entry the controller keeps from an
// environment or account (`derived`) is not listed here. Adding and removing
// are the agent card's own: `AddForm` without an agent, `useAllowedDomains`.
import { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { Badge } from "@hive/shared/badge.js";
import { AddForm } from "../../agent/detail/AllowedDomains.js";
import { useAllowedDomains } from "../../agent/detail/useAllowedDomains.js";
import { Button } from "../../ui/button/Button.js";
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
import { Dialog } from "../../ui/dialog/Dialog.js";
import "../../agent/detail/AgentLimits.css";
import "./SwarmPage.css";
function EgressBody() {
const { entries, error, busy, remove, refetch } = useAllowedDomains();
const [adding, setAdding] = useState(false);
// A list already loaded stays under a failed refetch, so a change that
// worked is not hidden by a read that did not.
const errorPanel = error ? (
<>
<ApiErrorPanel
context="the allowed domains were not read or changed"
problem={error}
/>
<Button onClick={() => refetch()}>retry</Button>
</>
) : null;
if (entries === null) {
return (
errorPanel ?? (
<span class="ui-agent-limits-muted" role="status">
loading the allowed domains…
</span>
)
);
}
const swarmWide = entries.filter(
(e) => e.agent === undefined && e.derived !== true,
);
return (
<>
{errorPanel}
<p class="ui-swarm-egress-help">
Names every agent in the swarm may reach through its hive's proxy, on
top of what each hive's own configuration allows. An agent that has
unrestricted internet reaches any name anyway. The hive reads a change
within about 30 seconds.
</p>
{swarmWide.length === 0 ? (
<p class="ui-agent-limits-muted">none added</p>
) : (
<ul class="ui-swarm-egress-list">
{swarmWide.map((e) => (
<li key={e.domain}>
<Badge value={e.domain} />{" "}
<Badge
variant="quiet"
value="remove"
title={`stop allowing ${e.domain} for every agent`}
disabled={busy}
onClick={() => void remove(e)}
/>
</li>
))}
</ul>
)}
<Badge
variant="quiet"
value="add"
title="allow a domain for every agent"
onClick={() => setAdding(true)}
/>
<Dialog
open={adding}
onClose={() => setAdding(false)}
label="add an allowed domain"
plain
>
{/* Remounted per open, so a cancelled add leaves nothing behind. */}
{adding ? (
<AddForm
onClose={() => setAdding(false)}
onAdded={() => {
setAdding(false);
refetch();
}}
/>
) : null}
</Dialog>
</>
);
}
export function EgressCard() {
return (
<LazyDisclosure
title="egress"
icon="🌐"
hint="the domains every agent may reach through its hive's proxy"
>
<EgressBody />
</LazyDisclosure>
);
}

View file

@ -17,3 +17,15 @@
border-radius: 0.4em;
background: color-mix(in srgb, var(--yellow) 12%, transparent);
}
/* The egress card: the swarm-wide allowed domains, one per line. */
.ui-swarm-egress-help {
margin: 0 0 0.75em;
color: var(--muted);
}
.ui-swarm-egress-list {
list-style: none;
margin: 0 0 0.5em;
padding: 0;
display: grid;
gap: 0.3em;
}

View file

@ -1,12 +1,13 @@
// <SwarmPage> — what belongs to the swarm as a whole rather than to one agent:
// the hive roster and the notification router. Each is a card that starts
// folded and reads nothing until it is opened.
// the hive roster, the swarm-wide egress allow list and the notification router.
// Each is a card that starts folded and reads nothing until it is opened.
//
// The notification router is marked beta: its card says so beside the title and
// carries a warning banner inside.
import { Badge } from "@hive/shared/badge.js";
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
import { NotificationRouter } from "../notifications/NotificationsPage.js";
import { EgressCard } from "./EgressCard.js";
import { HivesCard } from "./HivesCard.js";
import "./SwarmPage.css";
@ -16,6 +17,9 @@ export function SwarmPage() {
<div class="ui-swarm-card">
<HivesCard />
</div>
<div class="ui-swarm-card">
<EgressCard />
</div>
<div class="ui-swarm-card">
<LazyDisclosure
title="notification router"