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:
parent
a9dd276935
commit
8847e6390b
4 changed files with 57 additions and 5 deletions
|
|
@ -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
|
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
|
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.
|
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.
|
**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
|
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
|
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
|
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
|
**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
|
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
|
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
|
which hives haven't applied the current deny list yet; until they do, their proxies may still let
|
||||||
|
|
|
||||||
|
|
@ -157,3 +157,16 @@
|
||||||
.ui-broadcast-fail .ui-broadcast-outcome-name {
|
.ui-broadcast-fail .ui-broadcast-outcome-name {
|
||||||
color: var(--fg);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||||
import { EmptyState } from "../../ui/empty-state/EmptyState.js";
|
import { EmptyState } from "../../ui/empty-state/EmptyState.js";
|
||||||
import { Hint } from "../../ui/hint/Hint.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 { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||||
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js";
|
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js";
|
||||||
import {
|
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() {
|
export function BroadcastComposer() {
|
||||||
const [intervalMs, setIntervalMs] =
|
const [intervalMs, setIntervalMs] =
|
||||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||||
|
|
@ -183,7 +208,9 @@ export function BroadcastComposer() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{agents !== null && shown.length === 0 ? (
|
{agents === null ? (
|
||||||
|
<RosterSkeleton />
|
||||||
|
) : shown.length === 0 ? (
|
||||||
<EmptyState centered>
|
<EmptyState centered>
|
||||||
{filter === "running" && all.length > 0
|
{filter === "running" && all.length > 0
|
||||||
? "no agent is running right now. Show all to pick a stopped one."
|
? "no agent is running right now. Show all to pick a stopped one."
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||||
import { Hint } from "../../ui/hint/Hint.js";
|
import { Hint } from "../../ui/hint/Hint.js";
|
||||||
import { SectionHeading } from "../../ui/section-heading/SectionHeading.js";
|
import { SectionHeading } from "../../ui/section-heading/SectionHeading.js";
|
||||||
|
import { Skeleton, SkeletonRegion } from "../../ui/skeleton/Skeleton.js";
|
||||||
import "../../agent/detail/AgentLimits.css";
|
import "../../agent/detail/AgentLimits.css";
|
||||||
import "./SwarmPage.css";
|
import "./SwarmPage.css";
|
||||||
|
|
||||||
|
|
@ -67,9 +68,18 @@ function DomainSection({
|
||||||
{errorPanel}
|
{errorPanel}
|
||||||
{entries === null ? (
|
{entries === null ? (
|
||||||
errorPanel === null ? (
|
errorPanel === null ? (
|
||||||
<Hint as="span" role="status">
|
<SkeletonRegion
|
||||||
loading the {kind === "deny" ? "denied" : "allowed"} domains…
|
label={`the ${kind === "deny" ? "denied" : "allowed"} domains`}
|
||||||
</Hint>
|
>
|
||||||
|
<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
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue