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
3 changed files with 57 additions and 5 deletions

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
) : (
<>