feat(web): hydrate dashboard tiles independently #42

Merged
vince merged 3 commits from codex/independent-tile-hydration into main 2026-06-20 12:38:54 +02:00
5 changed files with 124 additions and 22 deletions
Showing only changes of commit 2a1d94195e - Show all commits

View file

@ -1,7 +1,8 @@
import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest";
import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { AppStateView } from "./App";
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import { AppStateView, dashboardHydrationTiles } from "./App";
describe("React app dashboard state view", () => {
test("renders loading dashboard state", () => {
@ -54,7 +55,7 @@ describe("React app dashboard state view", () => {
}}
hydratingItemIds={new Set([
"telemetry:service-uptime",
"service:identity",
"service:core-services:identity",
"module:ambient",
"status:runtime:status",
])}
@ -73,4 +74,12 @@ describe("React app dashboard state view", () => {
expect(html).toContain('data-severity="loading" data-model-id="ambient"');
expect(html).toContain('data-severity="loading" data-model-id="runtime:status"');
});
test("hydrates every status cell from the Dimension Lab shell", () => {
expect(dashboardHydrationTiles(dimensionLabDashboardFixture)).toContainEqual({
kind: "status",
stripId: "footer-status",
id: "auto-refresh",
});
});
});

View file

@ -21,7 +21,7 @@ import {
type DashboardTileReference =
| { kind: "telemetry"; id: string }
| { kind: "service"; id: string }
| { kind: "service"; groupId: string; id: string }
| { kind: "module"; id: string }
| { kind: "status"; stripId: string; id: string };
@ -295,7 +295,7 @@ function markHydratingItems(
serviceGroups: dashboard.serviceGroups.map((group) => ({
...group,
services: group.services.map((service) =>
hydratingItemIds.has(`service:${service.id}`)
hydratingItemIds.has(`service:${group.id}:${service.id}`)
? {
...service,
severity: "loading",
@ -325,14 +325,20 @@ function markHydratingItems(
};
}
function dashboardHydrationTiles(document: DashboardDocument): DashboardTileReference[] {
export function dashboardHydrationTiles(
document: DashboardDocument,
): DashboardTileReference[] {
const telemetry = document.telemetry
.filter((card) => card.datasource?.type === "external")
.map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id }));
const services = document.serviceGroups.flatMap((group) =>
group.services
.filter((service) => service.datasource?.type === "external")
.map((service): DashboardTileReference => ({ kind: "service", id: service.id })),
.map((service): DashboardTileReference => ({
kind: "service",
groupId: group.id,
id: service.id,
})),
);
const modules = (document.modules || [])
.filter((module) =>
@ -342,7 +348,6 @@ function dashboardHydrationTiles(document: DashboardDocument): DashboardTileRefe
.map((module): DashboardTileReference => ({ kind: "module", id: module.id }));
const status = document.statusStrips.flatMap((strip) =>
strip.items
.filter((item) => item.id !== "auto-refresh")
.map((item): DashboardTileReference => ({
kind: "status",
stripId: strip.id,
@ -354,15 +359,17 @@ function dashboardHydrationTiles(document: DashboardDocument): DashboardTileRefe
}
function dashboardTileKey(tile: DashboardTileReference): string {
return tile.kind === "status"
? `${tile.kind}:${tile.stripId}:${tile.id}`
: `${tile.kind}:${tile.id}`;
if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`;
if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`;
return `${tile.kind}:${tile.id}`;
}
function dashboardTileUrl(tile: DashboardTileReference): string {
const parts = tile.kind === "status"
? ["api", "dashboard", "tile", tile.kind, tile.stripId, tile.id]
: ["api", "dashboard", "tile", tile.kind, tile.id];
: tile.kind === "service"
? ["api", "dashboard", "tile", tile.kind, tile.groupId, tile.id]
: ["api", "dashboard", "tile", tile.kind, tile.id];
return `/${parts.map(encodeURIComponent).join("/")}`;
}
@ -380,13 +387,16 @@ function applyDashboardTile(
}
if (response.tile.kind === "service") {
const tile = response.tile;
return {
...document,
serviceGroups: document.serviceGroups.map((group) => ({
...group,
services: group.services.map((service) =>
service.id === response.tile.id ? response.item as ServiceEntry : service,
),
services: group.id === tile.groupId
? group.services.map((service) =>
service.id === tile.id ? response.item as ServiceEntry : service,
)
: group.services,
})),
};
}

View file

@ -12,7 +12,7 @@ import type {
export type DashboardTileReference =
| { kind: "telemetry"; id: string }
| { kind: "service"; id: string }
| { kind: "service"; groupId: string; id: string }
| { kind: "module"; id: string }
| { kind: "status"; stripId: string; id: string };
@ -32,6 +32,11 @@ export type DashboardTileResolution =
state: "not_found";
tile: DashboardTileReference;
message: string;
}
| {
state: "disabled";
tile: DashboardTileReference;
message: string;
};
export interface DatasourceResolutionOptions {
@ -90,8 +95,8 @@ export async function resolveDashboardTile(
if (tile.kind === "service") {
const service = document.serviceGroups
.flatMap((group) => group.services)
.find((item) => item.id === tile.id);
.find((group) => group.id === tile.groupId)
?.services.find((item) => item.id === tile.id);
if (!service) return missingTile(tile);
return {
@ -832,7 +837,7 @@ function missingTile(tile: DashboardTileReference): DashboardTileResolution {
}
function tileKey(tile: DashboardTileReference): string {
return tile.kind === "status"
? `${tile.kind}:${tile.stripId}:${tile.id}`
: `${tile.kind}:${tile.id}`;
if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`;
if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`;
return `${tile.kind}:${tile.id}`;
}

View file

@ -113,6 +113,65 @@ describe("dashboard API route", () => {
expect.objectContaining({ cache: "no-store" }),
);
});
test("hydrates a service tile with its service group identity", async () => {
const fetch = vi.fn(async () =>
jsonResponse({
status: "UP",
ping: 42,
}),
);
const response = await loadDashboardTileResponse(
{ kind: "service", groupId: "essentials", id: "vaultwarden" },
{
fetch,
refreshSeedDocument: true,
seedIfEmpty: true,
},
);
expect(response.state).toBe("ready");
if (response.state !== "ready") throw new Error("expected ready tile");
expect(response.tile).toEqual({
kind: "service",
groupId: "essentials",
id: "vaultwarden",
});
expect(response.item).toMatchObject({
id: "vaultwarden",
severity: "ok",
detail: "42 ms",
});
});
test("does not hydrate tile routes when live datasources are disabled", async () => {
const previous = process.env.DISABLE_LIVE_DATASOURCES;
process.env.DISABLE_LIVE_DATASOURCES = "1";
const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
new Error("live datasource fetch should not run when disabled"),
);
try {
const response = await loadDashboardTileResponse(
{ kind: "telemetry", id: "infra-ram" },
{
refreshSeedDocument: true,
seedIfEmpty: true,
},
);
expect(response.state).toBe("disabled");
expect(fetch).not.toHaveBeenCalled();
} finally {
fetch.mockRestore();
if (previous === undefined) {
delete process.env.DISABLE_LIVE_DATASOURCES;
} else {
process.env.DISABLE_LIVE_DATASOURCES = previous;
}
}
});
});
function jsonResponse(payload: unknown): Response {

View file

@ -63,6 +63,17 @@ export async function loadDashboardTileResponse(
tile: DashboardTileReference,
options: LoadDashboardResponseOptions = {},
): Promise<DashboardTileResolution> {
if (
options.disableLiveDatasources ||
process.env.DISABLE_LIVE_DATASOURCES === "1"
) {
return {
state: "disabled",
tile,
message: "Live datasource hydration is disabled.",
};
}
const dashboard = loadDashboardRuntime(undefined, {
refreshSeedDocument: options.refreshSeedDocument ?? true,
seedIfEmpty: options.seedIfEmpty ?? true,
@ -95,7 +106,7 @@ export async function handleDashboardTileRoute(pathname: string): Promise<Respon
const response = await loadDashboardTileResponse(tile);
return Response.json(response, {
status: response.state === "ready" ? 200 : 404,
status: response.state === "not_found" ? 404 : 200,
});
}
@ -107,10 +118,18 @@ function parseDashboardTilePath(pathname: string): DashboardTileReference | null
}
const id = decodeURIComponent(firstId);
if (kind === "telemetry" || kind === "service" || kind === "module") {
if (kind === "telemetry" || kind === "module") {
return { kind, id };
}
if (kind === "service" && secondId) {
return {
kind,
groupId: id,
id: decodeURIComponent(secondId),
};
}
if (kind === "status" && secondId) {
return {
kind,