fix: complete model renderer states and ids
This commit is contained in:
parent
fede33e8a7
commit
c2fb20b066
11 changed files with 84 additions and 7 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<StatusStrip items={dashboard.statusItems} />
|
<StatusStrip id={dashboard.statusStripId} items={dashboard.statusItems} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@
|
||||||
let { group }: { group: UiServiceGroup } = $props();
|
let { group }: { group: UiServiceGroup } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel title={group.title}>
|
<div class="service-panel" data-model-id={group.id}>
|
||||||
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,4 +83,5 @@ export interface UiDashboardPreview {
|
||||||
serviceGroups: UiServiceGroup[];
|
serviceGroups: UiServiceGroup[];
|
||||||
modules: UiModuleBlock[];
|
modules: UiModuleBlock[];
|
||||||
statusItems: UiStatusItem[];
|
statusItems: UiStatusItem[];
|
||||||
|
statusStripId?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue