feat: render dashboard from model

This commit is contained in:
vince 2026-06-18 19:22:55 +02:00
parent 13e3ff867b
commit fede33e8a7
11 changed files with 372 additions and 139 deletions

View file

@ -16,6 +16,7 @@
<a
class="footer-cell"
data-severity={item.severity || "neutral"}
data-model-id={item.id}
href={item.link.href}
target={target}
rel={rel}
@ -24,7 +25,7 @@
{@render cellContent()}
</a>
{:else}
<div class="footer-cell" data-severity={item.severity || "neutral"}>
<div class="footer-cell" data-severity={item.severity || "neutral"} data-model-id={item.id}>
{@render cellContent()}
</div>
{/if}

View file

@ -5,7 +5,7 @@
let { module }: { module: UiModuleBlock } = $props();
</script>
<aside class="module-card" data-severity={module.severity || "neutral"}>
<aside class="module-card" data-severity={module.severity || "neutral"} data-model-id={module.id}>
<div>
{#if module.title}
<h2>{module.title}</h2>

View file

@ -24,6 +24,7 @@
<a
class="service-row"
data-severity={service.severity}
data-model-id={service.id}
href={service.link.href}
target={target}
rel={rel}
@ -32,7 +33,7 @@
{@render rowContent()}
</a>
{:else}
<article class="service-row" data-severity={service.severity}>
<article class="service-row" data-severity={service.severity} data-model-id={service.id}>
{@render rowContent()}
</article>
{/if}

View file

@ -9,7 +9,7 @@
let value = $derived(formatMetricValue(card.value));
</script>
<article class="telemetry-card" data-severity={card.severity}>
<article class="telemetry-card" data-severity={card.severity} data-model-id={card.id}>
<header>
{#if card.icon}
<IconGlyph name={card.icon} size="sm" />

View file

@ -23,6 +23,7 @@ export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte";
export { default as TelemetryStrip } from "./components/TelemetryStrip.svelte";
export { default as WeatherModule } from "./components/WeatherModule.svelte";
export { dashboardPreviewFixtures } from "./fixtures";
export { dashboardDocumentToUiDashboard } from "./model-renderer";
export type {
UiDashboardPreview,
UiLink,

View file

@ -0,0 +1,90 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import {
type DashboardDocument,
} from "$lib/model";
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { dashboardDocumentToUiDashboard } from "./model-renderer";
describe("dashboard model renderer", () => {
test("projects the Dimension Lab model into UI component props", () => {
const dashboard = dashboardDocumentToUiDashboard(dimensionLabDashboardFixture);
expect(dashboard.title).toBe(dimensionLabDashboardFixture.metadata.title);
expect(dashboard.subtitle).toBe(dimensionLabDashboardFixture.metadata.subtitle);
expect(dashboard.telemetry.map((card) => card.id)).toEqual(
dimensionLabDashboardFixture.layout.telemetry,
);
expect(dashboard.telemetry[0]).toMatchObject({
label: "Infra RAM",
icon: "mdi:memory",
severity: "ok",
});
expect(dashboard.serviceGroups.map((group) => group.id)).toEqual(
dimensionLabDashboardFixture.layout.serviceGroups,
);
expect(dashboard.modules.map((module) => module.id)).toEqual([
"weather-amsterdam",
"runtime-health-summary",
]);
expect(dashboard.statusItems.map((item) => item.id)).toEqual([
"footer-status:system-status",
"footer-status:last-sync",
"footer-status:uptime",
"footer-status:load-avg",
]);
});
test("projects a second fixture through the same mapper", () => {
const dashboard = dashboardDocumentToUiDashboard(genericDashboardFixture);
expect(dashboard.title).toBe("Operations Console");
expect(dashboard.telemetry.map((card) => card.id)).toEqual([
"service-uptime",
"queue-depth",
]);
expect(dashboard.serviceGroups[0]?.services.map((service) => service.id)).toEqual([
"identity",
"scheduler",
]);
expect(dashboard.modules[0]).toMatchObject({
id: "ambient",
title: "Environment",
icon: "mdi:radar",
});
});
test("returns intentional empty arrays for missing optional sections", () => {
const emptyModel: DashboardDocument = {
...genericDashboardFixture,
layout: {
density: "compact",
telemetry: [],
serviceGroups: [],
statusStrips: [],
modules: [],
},
telemetry: [],
serviceGroups: [],
statusStrips: [],
modules: [],
};
const dashboard = dashboardDocumentToUiDashboard(emptyModel);
expect(dashboard.telemetry).toEqual([]);
expect(dashboard.serviceGroups).toEqual([]);
expect(dashboard.statusItems).toEqual([]);
expect(dashboard.modules).toEqual([]);
});
test("does not hardcode environment-specific content in mapper source", () => {
const source = readFileSync(join(process.cwd(), "src/lib/ui/model-renderer.ts"), "utf8").toLowerCase();
expect(source).not.toContain("dimension");
expect(source).not.toContain("vaultwarden");
expect(source).not.toContain("forgejo");
});
});

View file

@ -0,0 +1,109 @@
import type {
DashboardDocument,
DashboardModule,
ServiceEntry,
ServiceGroup,
StatusItem,
StatusStrip,
TelemetryCard,
} from "$lib/model";
import type {
UiDashboardPreview,
UiModuleBlock,
UiServiceGroup,
UiServiceRow,
UiStatusItem,
UiTelemetryCard,
} from "./types";
export function dashboardDocumentToUiDashboard(
document: DashboardDocument,
): UiDashboardPreview {
return {
eyebrow: document.schemaVersion,
title: document.metadata.title,
subtitle: document.metadata.subtitle,
telemetry: orderedItems(document.layout.telemetry, document.telemetry).map(
telemetryToUi,
),
serviceGroups: orderedItems(
document.layout.serviceGroups,
document.serviceGroups,
).map(serviceGroupToUi),
modules: orderedItems(
document.layout.modules || [],
document.modules || [],
).map(moduleToUi),
statusItems: orderedItems(
document.layout.statusStrips,
document.statusStrips,
).flatMap(statusStripToUiItems),
};
}
function telemetryToUi(card: TelemetryCard): UiTelemetryCard {
return {
id: card.id,
label: card.label,
description: card.description,
icon: card.icon,
value: card.value,
detail: card.detail,
severity: card.severity,
sparkline: card.sparkline,
};
}
function serviceGroupToUi(group: ServiceGroup): UiServiceGroup {
return {
id: group.id,
title: group.title,
services: group.services.map(serviceToUi),
};
}
function serviceToUi(service: ServiceEntry): UiServiceRow {
return {
id: service.id,
label: service.label,
description: service.description,
icon: service.icon,
severity: service.severity,
detail: service.detail,
link: service.link,
};
}
function moduleToUi(module: DashboardModule): UiModuleBlock {
return {
id: module.id,
title: module.title,
label: module.label,
value: module.value,
detail: module.detail,
icon: module.icon,
severity: module.severity,
};
}
function statusStripToUiItems(strip: StatusStrip): UiStatusItem[] {
return strip.items.map((item) => statusItemToUi(strip.id, item));
}
function statusItemToUi(stripId: string, item: StatusItem): UiStatusItem {
return {
id: `${stripId}:${item.id}`,
label: item.label,
value: item.value,
severity: item.severity,
link: item.link,
};
}
function orderedItems<T extends { id: string }>(ids: string[], items: T[]): T[] {
const byId = new Map(items.map((item) => [item.id, item]));
return ids.flatMap((id) => {
const item = byId.get(id);
return item ? [item] : [];
});
}