Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: the broadcast roster and the egress lists show placeholders while they load

The broadcast grid showed nothing while the agents loaded, and the egress
card a 'loading the … domains' line. They now show grey placeholders in the
shape of what is coming: six agent chips with the real chip's height (160 px,
measured), and three domain rows with their remove buttons.
This commit is contained in:
iris 2026-11-06 19:49:47 +01:00 • committed by mara
commit 8847e6390b
4 changed files with 57 additions and 5 deletions

View file

@ -611,6 +611,7 @@ to more than one agent at once, and the messages show up in the list below as th
go. Folding it again keeps what you typed and ticked. Pick the
agents from a grid of their icons. **running** shows the agents that report
themselves running and **all** shows every agent; the browser keeps the choice.
While the agents load, the grid shows grey placeholder chips of the same size.
**select all** ticks every agent the filter shows, and **clear** unticks them.
The composer skips an agent you ticked and then hid with the filter. Type the
message and press **send to N agents**. The text goes exactly as typed, as an
@ -739,7 +740,8 @@ swarm. The tick is off by default, so an entry is for this agent alone.
The **egress** card on `/swarm` has two lists of the swarm-wide entries, one per line, with
**remove** beside each and an **add** that opens the same dialog without the tick (the
entry is always for every agent). **allowed** is the list above. **denied** is the swarm's
entry is always for every agent). While a list loads, it shows grey placeholder
rows. **allowed** is the list above. **denied** is the swarm's
deny list: every egress mode refuses a denied name, even when an allowed entry, a hive's
own `allowedDomains` or the unrestricted internet flag would let it through. The card says
which hives haven't applied the current deny list yet; until they do, their proxies may still let

View file

@ -157,3 +157,16 @@
.ui-broadcast-fail .ui-broadcast-outcome-name {
color: var(--fg);
}
/* The loading twin's chip: not a control, so no pointer. */
.ui-broadcast-agent-loading {
cursor: default;
}
/* Its name and state lines take the height of a line of text, so the chip is
the height of a real one. */
.ui-broadcast-agent-loading .ui-broadcast-name,
.ui-broadcast-agent-loading .ui-broadcast-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 1.6em;
}

View file

@ -23,6 +23,7 @@ import {
} from "../../ui/refresh-interval/RefreshInterval.js";
import { EmptyState } from "../../ui/empty-state/EmptyState.js";
import { Hint } from "../../ui/hint/Hint.js";
import { Skeleton, SkeletonRegion } from "../../ui/skeleton/Skeleton.js";
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js";
import {
@ -59,6 +60,30 @@ function storedFilter(): AgentFilter {
}
}
// The twin of the agent grid while the roster loads: the same grid and the
// same chip (border, icon, name, state) with a placeholder in each slot.
function RosterSkeleton({ count = 6 }: { count?: number }) {
return (
<SkeletonRegion label="the agents">
<ul class="ui-broadcast-grid" aria-hidden="true">
{Array.from({ length: count }, (_, i) => (
<li key={i}>
<div class="ui-broadcast-agent ui-broadcast-agent-loading">
<Skeleton shape="block" width="5em" height="5em" />
<span class="ui-broadcast-name">
<Skeleton width="4em" />
</span>
<span class="ui-broadcast-state">
<Skeleton width="3.5em" />
</span>
</div>
</li>
))}
</ul>
</SkeletonRegion>
);
}
export function BroadcastComposer() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
@ -183,7 +208,9 @@ export function BroadcastComposer() {
/>
</div>
{agents !== null && shown.length === 0 ? (
{agents === null ? (
<RosterSkeleton />
) : shown.length === 0 ? (
<EmptyState centered>
{filter === "running" && all.length > 0
? "no agent is running right now. Show all to pick a stopped one."

View file

@ -26,6 +26,7 @@ import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
import { Dialog } from "../../ui/dialog/Dialog.js";
import { Hint } from "../../ui/hint/Hint.js";
import { SectionHeading } from "../../ui/section-heading/SectionHeading.js";
import { Skeleton, SkeletonRegion } from "../../ui/skeleton/Skeleton.js";
import "../../agent/detail/AgentLimits.css";
import "./SwarmPage.css";
@ -67,9 +68,18 @@ function DomainSection({
{errorPanel}
{entries === null ? (
errorPanel === null ? (
<Hint as="span" role="status">
loading the {kind === "deny" ? "denied" : "allowed"} domains…
</Hint>
<SkeletonRegion
label={`the ${kind === "deny" ? "denied" : "allowed"} domains`}
>
<ul class="ui-swarm-egress-list" aria-hidden="true">
{[0, 1, 2].map((i) => (
<li key={i}>
<Skeleton width="10em" />
<Skeleton width="5em" height="2.75em" />
</li>
))}
</ul>
</SkeletonRegion>
) : null
) : (
<>