feat(app): render dashboard with react
This commit is contained in:
parent
73abcc1b69
commit
92a8990e7a
3 changed files with 133 additions and 4 deletions
43
src/App.tsx
43
src/App.tsx
|
|
@ -1,5 +1,11 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { DashboardRuntimeState } from "$lib/server/dashboard";
|
import type { DashboardRuntimeState } from "$lib/server/dashboard";
|
||||||
|
import {
|
||||||
|
DashboardFrame,
|
||||||
|
SystemState,
|
||||||
|
dashboardDocumentToUiDashboard,
|
||||||
|
type UiSeverity,
|
||||||
|
} from "$lib/ui";
|
||||||
|
|
||||||
const loadingDashboardState: DashboardRuntimeState = {
|
const loadingDashboardState: DashboardRuntimeState = {
|
||||||
state: "loading",
|
state: "loading",
|
||||||
|
|
@ -14,14 +20,31 @@ export function AppStateView({
|
||||||
dashboard: DashboardRuntimeState;
|
dashboard: DashboardRuntimeState;
|
||||||
}) {
|
}) {
|
||||||
if (dashboard.state === "ready") {
|
if (dashboard.state === "ready") {
|
||||||
return <main>{dashboard.document.metadata.title}</main>;
|
return (
|
||||||
|
<DashboardFrame
|
||||||
|
dashboard={dashboardDocumentToUiDashboard(dashboard.document)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detail = `${dashboard.subtitle}: ${dashboard.message}`;
|
||||||
|
const errors = dashboard.state === "invalid" ? dashboard.errors : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="state-shell" data-dashboard-state={dashboard.state}>
|
<main className="state-shell" data-dashboard-state={dashboard.state}>
|
||||||
<h1>{dashboard.title}</h1>
|
<SystemState
|
||||||
<p>{dashboard.subtitle}</p>
|
title={dashboard.title}
|
||||||
<p>{dashboard.message}</p>
|
detail={detail}
|
||||||
|
severity={stateSeverity(dashboard.state)}
|
||||||
|
icon={stateIcon(dashboard.state)}
|
||||||
|
/>
|
||||||
|
{errors.length ? (
|
||||||
|
<ul aria-label="Validation errors">
|
||||||
|
{errors.map((error) => (
|
||||||
|
<li key={error}>{error}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -69,3 +92,15 @@ export default function App() {
|
||||||
|
|
||||||
return <AppStateView dashboard={dashboard} />;
|
return <AppStateView dashboard={dashboard} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
|
||||||
21
src/app.css
21
src/app.css
|
|
@ -133,6 +133,27 @@ a {
|
||||||
color: inherit;
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
|
|
||||||
73
src/page.test.tsx
Normal file
73
src/page.test.tsx
Normal file
|
|
@ -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(
|
||||||
|
<AppStateView
|
||||||
|
dashboard={{
|
||||||
|
state: "ready",
|
||||||
|
document: genericDashboardFixture,
|
||||||
|
schemaVersion: "dashboard.v1",
|
||||||
|
currentRevisionId: "revision-1234567890",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<AppStateView
|
||||||
|
dashboard={{
|
||||||
|
state: "invalid",
|
||||||
|
title: "Invalid Dashboard",
|
||||||
|
subtitle: "Validation failed",
|
||||||
|
message: "Dashboard document is invalid.",
|
||||||
|
errors: ["/metadata/title is required"],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<AppStateView
|
||||||
|
dashboard={{
|
||||||
|
state: "empty",
|
||||||
|
title: "No Dashboard Model",
|
||||||
|
subtitle: "No active document",
|
||||||
|
message: "No validated dashboard document is active yet.",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const loading = renderToString(
|
||||||
|
<AppStateView
|
||||||
|
dashboard={{
|
||||||
|
state: "loading",
|
||||||
|
title: "Loading Dashboard",
|
||||||
|
subtitle: "Fetching active model",
|
||||||
|
message: "Waiting for the active dashboard document.",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(empty).toContain("No Dashboard Model");
|
||||||
|
expect(empty).toContain("No active document");
|
||||||
|
expect(loading).toContain("Loading Dashboard");
|
||||||
|
expect(loading).toContain("Fetching active model");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue