fix: complete model renderer states and ids

This commit is contained in:
vince 2026-06-18 19:29:30 +02:00
parent fede33e8a7
commit c2fb20b066
11 changed files with 84 additions and 7 deletions

View file

@ -10,6 +10,7 @@ import { UnsupportedDashboardModelVersionError } from "$lib/server/db/model-migr
export type DashboardRuntimeState = export type DashboardRuntimeState =
| DashboardRuntimeEmpty | DashboardRuntimeEmpty
| DashboardRuntimeInvalid | DashboardRuntimeInvalid
| DashboardRuntimeLoading
| DashboardRuntimeReady; | DashboardRuntimeReady;
export interface DashboardRuntimeReady { export interface DashboardRuntimeReady {
@ -26,6 +27,13 @@ export interface DashboardRuntimeEmpty {
message: string; message: string;
} }
export interface DashboardRuntimeLoading {
state: "loading";
title: string;
subtitle: string;
message: string;
}
export interface DashboardRuntimeInvalid { export interface DashboardRuntimeInvalid {
state: "invalid"; state: "invalid";
title: string; title: string;

View file

@ -44,7 +44,7 @@
{/each} {/each}
</section> </section>
<StatusStrip items={dashboard.statusItems} /> <StatusStrip id={dashboard.statusStripId} items={dashboard.statusItems} />
</main> </main>
<style> <style>

View file

@ -7,11 +7,13 @@
let { group }: { group: UiServiceGroup } = $props(); let { group }: { group: UiServiceGroup } = $props();
</script> </script>
<div class="service-panel" data-model-id={group.id}>
<Panel title={group.title}> <Panel title={group.title}>
{#if group.summary?.length} {#if group.summary?.length}
<StatusStrip items={group.summary} compact /> <StatusStrip id={`${group.id}:summary`} items={group.summary} compact />
{/if} {/if}
{#each group.services as service (service.id)} {#each group.services as service (service.id)}
<ServiceRow {service} /> <ServiceRow {service} />
{/each} {/each}
</Panel> </Panel>
</div>

View file

@ -3,15 +3,17 @@
import FooterCell from "./FooterCell.svelte"; import FooterCell from "./FooterCell.svelte";
let { let {
id,
items, items,
compact = false, compact = false,
}: { }: {
id?: string;
items: UiStatusItem[]; items: UiStatusItem[];
compact?: boolean; compact?: boolean;
} = $props(); } = $props();
</script> </script>
<section class="status-strip" data-compact={compact}> <section class="status-strip" data-compact={compact} data-model-id={id}>
{#each items as item (item.id)} {#each items as item (item.id)}
<FooterCell {item} /> <FooterCell {item} />
{/each} {/each}

View file

@ -70,6 +70,17 @@ describe("dashboard UI components", () => {
expect(footer.body).toContain("href=\"https://example.test/status\""); expect(footer.body).toContain("href=\"https://example.test/status\"");
}); });
test("renders stable model IDs on group and status containers", () => {
const { body } = render(DashboardFrame, {
props: {
dashboard: dashboardPreviewFixtures.primary,
},
});
expect(body).toContain("data-model-id=\"queue-workers\"");
expect(body).toContain("data-model-id=\"dashboard-status\"");
});
test("does not render progress bars for non-percent metrics without explicit progress", () => { test("does not render progress bars for non-percent metrics without explicit progress", () => {
const withoutProgress = render(TelemetryCard, { const withoutProgress = render(TelemetryCard, {
props: { props: {

View file

@ -58,6 +58,7 @@ export const dashboardPreviewFixtures: Record<string, UiDashboardPreview> = {
{ id: "uptime", label: "Uptime", value: "14d 08h", severity: "neutral" }, { id: "uptime", label: "Uptime", value: "14d 08h", severity: "neutral" },
{ id: "refresh", label: "Refresh", value: "15s", severity: "neutral" }, { id: "refresh", label: "Refresh", value: "15s", severity: "neutral" },
], ],
statusStripId: "dashboard-status",
}, },
secondary: { secondary: {
eyebrow: "Support Surface", eyebrow: "Support Surface",
@ -96,6 +97,7 @@ export const dashboardPreviewFixtures: Record<string, UiDashboardPreview> = {
{ id: "handoff", label: "Handoff", value: "Pending", severity: "warning" }, { id: "handoff", label: "Handoff", value: "Pending", severity: "warning" },
{ id: "routing", label: "Routing", value: "Manual", severity: "neutral" }, { id: "routing", label: "Routing", value: "Manual", severity: "neutral" },
], ],
statusStripId: "support-status",
}, },
}; };

View file

@ -35,6 +35,7 @@ describe("dashboard model renderer", () => {
"footer-status:uptime", "footer-status:uptime",
"footer-status:load-avg", "footer-status:load-avg",
]); ]);
expect(dashboard.statusStripId).toBe("footer-status");
}); });
test("projects a second fixture through the same mapper", () => { test("projects a second fixture through the same mapper", () => {

View file

@ -38,6 +38,7 @@ export function dashboardDocumentToUiDashboard(
document.layout.statusStrips, document.layout.statusStrips,
document.statusStrips, document.statusStrips,
).flatMap(statusStripToUiItems), ).flatMap(statusStripToUiItems),
statusStripId: document.layout.statusStrips[0],
}; };
} }

View file

@ -83,4 +83,5 @@ export interface UiDashboardPreview {
serviceGroups: UiServiceGroup[]; serviceGroups: UiServiceGroup[];
modules: UiModuleBlock[]; modules: UiModuleBlock[];
statusItems: UiStatusItem[]; statusItems: UiStatusItem[];
statusStripId?: string;
} }

View file

@ -43,8 +43,8 @@
<SystemState <SystemState
title={stateTitle} title={stateTitle}
detail={`${stateSubtitle}: ${stateMessage}`} detail={`${stateSubtitle}: ${stateMessage}`}
severity={data.dashboard.state === "invalid" ? "danger" : "stale"} severity={stateSeverity(data.dashboard.state)}
icon={data.dashboard.state === "invalid" ? "mdi:file-alert-outline" : "mdi:tray"} icon={stateIcon(data.dashboard.state)}
/> />
{#if stateErrors.length} {#if stateErrors.length}
<ul aria-label="Validation errors"> <ul aria-label="Validation errors">
@ -78,3 +78,20 @@
padding: var(--ui-space-3); padding: var(--ui-space-3);
} }
</style> </style>
<script lang="ts" module>
import type { DashboardRuntimeState } from "$lib/server/dashboard";
import type { UiSeverity } from "$lib/ui";
function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
if (state === "invalid") return "danger";
if (state === "loading") return "loading";
return "stale";
}
function stateIcon(state: DashboardRuntimeState["state"]): string {
if (state === "invalid") return "mdi:file-alert-outline";
if (state === "loading") return "mdi:progress-clock";
return "mdi:tray";
}
</script>

View file

@ -45,4 +45,36 @@ describe("home page model renderer", () => {
expect(body).toContain("Validation failed"); expect(body).toContain("Validation failed");
expect(body).toContain("Dashboard document is invalid."); expect(body).toContain("Dashboard document is invalid.");
}); });
test("renders empty and loading model states without crashing", () => {
const empty = render(Page, {
props: {
data: {
dashboard: {
state: "empty",
title: "No Dashboard Model",
subtitle: "No active document",
message: "No validated dashboard document is active yet.",
},
},
},
});
const loading = render(Page, {
props: {
data: {
dashboard: {
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
},
},
},
});
expect(empty.body).toContain("No Dashboard Model");
expect(empty.body).toContain("No active document");
expect(loading.body).toContain("Loading Dashboard");
expect(loading.body).toContain("Fetching active model");
});
}); });