From 92a8990e7a0204193378398f0849269995711ed4 Mon Sep 17 00:00:00 2001 From: vince Date: Fri, 19 Jun 2026 23:42:25 +0200 Subject: [PATCH] feat(app): render dashboard with react --- src/App.tsx | 43 +++++++++++++++++++++++++--- src/app.css | 21 ++++++++++++++ src/page.test.tsx | 73 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 src/page.test.tsx diff --git a/src/App.tsx b/src/App.tsx index 2ea4f3b..1dc7285 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,11 @@ import { useEffect, useState } from "react"; import type { DashboardRuntimeState } from "$lib/server/dashboard"; +import { + DashboardFrame, + SystemState, + dashboardDocumentToUiDashboard, + type UiSeverity, +} from "$lib/ui"; const loadingDashboardState: DashboardRuntimeState = { state: "loading", @@ -14,14 +20,31 @@ export function AppStateView({ dashboard: DashboardRuntimeState; }) { if (dashboard.state === "ready") { - return
{dashboard.document.metadata.title}
; + return ( + + ); } + const detail = `${dashboard.subtitle}: ${dashboard.message}`; + const errors = dashboard.state === "invalid" ? dashboard.errors : []; + return (
-

{dashboard.title}

-

{dashboard.subtitle}

-

{dashboard.message}

+ + {errors.length ? ( +
    + {errors.map((error) => ( +
  • {error}
  • + ))} +
+ ) : null}
); } @@ -69,3 +92,15 @@ export default function App() { return ; } + +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"; +} diff --git a/src/app.css b/src/app.css index 9baeb28..f851aa0 100644 --- a/src/app.css +++ b/src/app.css @@ -133,6 +133,27 @@ a { color: inherit; } +.state-shell { + display: grid; + min-height: 100vh; + align-content: center; + gap: var(--ui-space-3); + padding: var(--ui-space-4); +} + +.state-shell ul { + display: grid; + max-width: 56rem; + gap: var(--ui-space-2); + margin: 0; + border: var(--ui-border); + background: rgba(6, 8, 7, 0.82); + color: var(--ui-color-muted); + font-size: 0.76rem; + list-style-position: inside; + padding: var(--ui-space-3); +} + @media (prefers-reduced-motion: reduce) { *, *::before, diff --git a/src/page.test.tsx b/src/page.test.tsx new file mode 100644 index 0000000..45e3d3a --- /dev/null +++ b/src/page.test.tsx @@ -0,0 +1,73 @@ +import { renderToString } from "react-dom/server"; +import { describe, expect, test } from "vitest"; +import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { AppStateView } from "./App"; + +describe("home page model renderer", () => { + test("renders the active dashboard model from the runtime state", () => { + const body = renderToString( + , + ); + + expect(body).toContain("Operations Console"); + expect(body).toContain("Service Uptime"); + expect(body).toContain("Identity"); + expect(body).toContain("data-model-id=\"service-uptime\""); + expect(body).not.toContain("primary"); + expect(body).not.toContain("secondary"); + }); + + test("renders invalid model state without crashing", () => { + const body = renderToString( + , + ); + + expect(body).toContain("Invalid Dashboard"); + expect(body).toContain("Validation failed"); + expect(body).toContain("Dashboard document is invalid."); + expect(body).toContain("/metadata/title is required"); + }); + + test("renders empty and loading model states without crashing", () => { + const empty = renderToString( + , + ); + const loading = renderToString( + , + ); + + expect(empty).toContain("No Dashboard Model"); + expect(empty).toContain("No active document"); + expect(loading).toContain("Loading Dashboard"); + expect(loading).toContain("Fetching active model"); + }); +});