fix(ui): keep Signage state consistent across tabs, and confirm destructive actions
Activating a timeline from Playback left Timelines showing it as inactive, because each tab read its own stale copy of the channel groups; operators read that as activation not sticking. Tabs also mounted every panel afresh, so each switch flashed a spinner and collapsed the page before regrowing. Panels now share one stale-while-revalidate resource cache, and Playback follows the management realtime channel so Now showing tracks the wall. Deleting an image, a timeline, a slot, a source, or an audience happened on a single click; each is now a two-step in-place confirmation (DESIGN 21.3). The audience key was presented as a field to fill in. Audiences are now named in plain words, explained (images look the same for every audience), and the content key is derived from the name with an Advanced override. The roster labels signed-in browsers and lets an offline entry be removed after confirmation, so test tabs and retired screens no longer accumulate. The preview keeps nothing in the Dashboard origin's storage, and the narrow-screen notice no longer names the platform. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
67d89f1020
commit
7c17282616
@@ -1,13 +1,13 @@
|
||||
import type { ModuleUiHostContext } from "@ekklesia/module-ui-sdk";
|
||||
import {
|
||||
Badge,
|
||||
EmptyState,
|
||||
Panel,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Badge,
|
||||
EmptyState,
|
||||
Panel,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@ekklesia/ui";
|
||||
import type { ReactElement } from "react";
|
||||
import type { SignageApi } from "../api.js";
|
||||
@@ -31,101 +31,88 @@ const SLIDE_RENDERER_CAPABILITY = "signage.slide-renderer";
|
||||
* exactly that, which is the difference between "this is broken" and "this needs an update".
|
||||
*/
|
||||
export function ProvidersPanel({
|
||||
api,
|
||||
context,
|
||||
settings,
|
||||
api,
|
||||
context,
|
||||
settings,
|
||||
}: {
|
||||
readonly api: SignageApi;
|
||||
readonly context: ModuleUiHostContext;
|
||||
readonly settings: SettingsResponse;
|
||||
readonly api: SignageApi;
|
||||
readonly context: ModuleUiHostContext;
|
||||
readonly settings: SettingsResponse;
|
||||
}): ReactElement {
|
||||
const providers = useResource(
|
||||
() => api.get<{ providers: readonly ProviderHealth[] }>("providers"),
|
||||
[],
|
||||
);
|
||||
const published = new Set(
|
||||
context.contributions
|
||||
.list(SLIDE_RENDERER_CAPABILITY)
|
||||
.map((contribution) => contribution.id),
|
||||
);
|
||||
const providers = useResource(
|
||||
() => api.get<{ providers: readonly ProviderHealth[] }>("providers"),
|
||||
[],
|
||||
"providers",
|
||||
);
|
||||
const published = new Set(
|
||||
context.contributions.list(SLIDE_RENDERER_CAPABILITY).map((contribution) => contribution.id),
|
||||
);
|
||||
|
||||
return (
|
||||
<Panel
|
||||
actions={
|
||||
<Badge tone="neutral">{`Contract ${settings.rendererInputVersion}`}</Badge>
|
||||
}
|
||||
description="Running content sources available to timelines."
|
||||
title="Content providers"
|
||||
>
|
||||
{renderLoadable(
|
||||
providers.state,
|
||||
(value) =>
|
||||
value.providers.length === 0 ? (
|
||||
<EmptyState
|
||||
description="Signage works on its own with native images. Install a content module to add more."
|
||||
title="No content providers registered"
|
||||
/>
|
||||
) : (
|
||||
<Table label="Provider health">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell header>Provider</TableCell>
|
||||
<TableCell header>Contract</TableCell>
|
||||
<TableCell header>Renderer</TableCell>
|
||||
<TableCell header>Health</TableCell>
|
||||
<TableCell header>Last success</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{value.providers.map((provider) => {
|
||||
const rendererPublished = published.has(
|
||||
provider.renderer.contributionId,
|
||||
);
|
||||
const isNative =
|
||||
provider.renderer.contributionId === "signage.native-image";
|
||||
return (
|
||||
<TableRow key={provider.providerId}>
|
||||
<TableCell>
|
||||
<span className="signage-provider__id">
|
||||
{provider.providerId}
|
||||
</span>
|
||||
<span className="signage-provider__version">{`v${provider.providerVersion}`}</span>
|
||||
</TableCell>
|
||||
<TableCell>{provider.contractVersion}</TableCell>
|
||||
<TableCell>
|
||||
{isNative ? (
|
||||
<Badge tone="info">Built in</Badge>
|
||||
) : rendererPublished ? (
|
||||
<Badge tone="success">
|
||||
{provider.renderer.contributionId}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge tone="warning">Renderer not published</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{provider.consecutiveFailures === 0 ? (
|
||||
<Badge tone="success">Healthy</Badge>
|
||||
) : (
|
||||
<Badge tone="danger">
|
||||
{`${provider.consecutiveFailures} failures · ${provider.lastFailureCode ?? "unknown"}`}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatInstant(
|
||||
provider.lastSuccessAt,
|
||||
settings.timezone,
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
{ label: "Loading providers…", onRetry: providers.reload },
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
return (
|
||||
<Panel
|
||||
actions={<Badge tone="neutral">{`Contract ${settings.rendererInputVersion}`}</Badge>}
|
||||
description="Running content sources available to timelines."
|
||||
title="Content providers"
|
||||
>
|
||||
{renderLoadable(
|
||||
providers.state,
|
||||
(value) =>
|
||||
value.providers.length === 0 ? (
|
||||
<EmptyState
|
||||
description="Signage works on its own with native images. Install a content module to add more."
|
||||
title="No content providers registered"
|
||||
/>
|
||||
) : (
|
||||
<Table label="Provider health">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell header>Provider</TableCell>
|
||||
<TableCell header>Contract</TableCell>
|
||||
<TableCell header>Renderer</TableCell>
|
||||
<TableCell header>Health</TableCell>
|
||||
<TableCell header>Last success</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{value.providers.map((provider) => {
|
||||
const rendererPublished = published.has(provider.renderer.contributionId);
|
||||
const isNative = provider.renderer.contributionId === "signage.native-image";
|
||||
return (
|
||||
<TableRow key={provider.providerId}>
|
||||
<TableCell>
|
||||
<span className="signage-provider__id">{provider.providerId}</span>
|
||||
<span className="signage-provider__version">{`v${provider.providerVersion}`}</span>
|
||||
</TableCell>
|
||||
<TableCell>{provider.contractVersion}</TableCell>
|
||||
<TableCell>
|
||||
{isNative ? (
|
||||
<Badge tone="info">Built in</Badge>
|
||||
) : rendererPublished ? (
|
||||
<Badge tone="success">{provider.renderer.contributionId}</Badge>
|
||||
) : (
|
||||
<Badge tone="warning">Renderer not published</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{provider.consecutiveFailures === 0 ? (
|
||||
<Badge tone="success">Healthy</Badge>
|
||||
) : (
|
||||
<Badge tone="danger">
|
||||
{`${provider.consecutiveFailures} failures · ${provider.lastFailureCode ?? "unknown"}`}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatInstant(provider.lastSuccessAt, settings.timezone)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
{ label: "Loading providers…", onRetry: providers.reload },
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user