183 lines
4.7 KiB
TypeScript
183 lines
4.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { DashboardRuntimeState } from "$lib/server/dashboard";
|
|
import { dashboardDocumentToUiDashboard } from "$lib/ui-adapter/model-renderer";
|
|
import {
|
|
DashboardFrame,
|
|
SystemState,
|
|
ThemeToggle,
|
|
persistUiTheme,
|
|
resolveInitialUiTheme,
|
|
type UiTheme,
|
|
type UiSeverity,
|
|
} from "@dimensionlab/ui";
|
|
|
|
const loadingDashboardState: DashboardRuntimeState = {
|
|
state: "loading",
|
|
title: "Loading Dashboard",
|
|
subtitle: "Fetching active model",
|
|
message: "Waiting for the active dashboard document.",
|
|
};
|
|
|
|
export function AppStateView({
|
|
dashboard,
|
|
onThemeChange,
|
|
theme,
|
|
}: {
|
|
dashboard: DashboardRuntimeState;
|
|
onThemeChange?: (theme: UiTheme) => void;
|
|
theme?: UiTheme;
|
|
}) {
|
|
const themeToggle =
|
|
theme && onThemeChange ? (
|
|
<ThemeToggle theme={theme} onThemeChange={onThemeChange} />
|
|
) : null;
|
|
|
|
if (dashboard.state === "ready") {
|
|
return (
|
|
<DashboardFrame
|
|
dashboard={dashboardDocumentToUiDashboard(dashboard.document)}
|
|
actions={themeToggle}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const detail = `${dashboard.subtitle}: ${dashboard.message}`;
|
|
const errors = dashboard.state === "invalid" ? dashboard.errors : [];
|
|
|
|
return (
|
|
<main className="state-shell" data-dashboard-state={dashboard.state}>
|
|
{themeToggle ? (
|
|
<div className="state-shell__actions">{themeToggle}</div>
|
|
) : null}
|
|
<SystemState
|
|
title={dashboard.title}
|
|
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>
|
|
);
|
|
}
|
|
|
|
export function resolveDocumentMetadata(dashboard: DashboardRuntimeState): {
|
|
description: string;
|
|
title: string;
|
|
} {
|
|
if (dashboard.state === "ready") {
|
|
const uiDashboard = dashboardDocumentToUiDashboard(dashboard.document);
|
|
return {
|
|
title: uiDashboard.title,
|
|
description:
|
|
uiDashboard.subtitle ||
|
|
dashboard.document.metadata.description ||
|
|
uiDashboard.title,
|
|
};
|
|
}
|
|
|
|
return {
|
|
title: dashboard.title,
|
|
description: dashboard.subtitle || dashboard.message,
|
|
};
|
|
}
|
|
|
|
export default function App() {
|
|
const [dashboard, setDashboard] =
|
|
useState<DashboardRuntimeState>(loadingDashboardState);
|
|
const [theme, setTheme] = useState<UiTheme>(() => {
|
|
if (typeof window === "undefined") return "dark";
|
|
|
|
return resolveInitialUiTheme(getThemeStorage());
|
|
});
|
|
|
|
useEffect(() => {
|
|
const metadata = resolveDocumentMetadata(dashboard);
|
|
document.title = metadata.title;
|
|
|
|
let description = document.querySelector<HTMLMetaElement>(
|
|
'meta[name="description"]',
|
|
);
|
|
if (!description) {
|
|
description = document.createElement("meta");
|
|
description.name = "description";
|
|
document.head.append(description);
|
|
}
|
|
description.content = metadata.description;
|
|
}, [dashboard]);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.dataset.uiTheme = theme;
|
|
persistUiTheme(theme, getThemeStorage());
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
let refreshTimer: number | undefined;
|
|
|
|
async function loadDashboard() {
|
|
const response = await fetch("/api/dashboard");
|
|
const nextDashboard = (await response.json()) as DashboardRuntimeState;
|
|
|
|
if (cancelled) return;
|
|
setDashboard(nextDashboard);
|
|
|
|
if (refreshTimer) {
|
|
window.clearInterval(refreshTimer);
|
|
refreshTimer = undefined;
|
|
}
|
|
|
|
const refreshIntervalSeconds =
|
|
nextDashboard.state === "ready"
|
|
? nextDashboard.document.metadata.refreshIntervalSeconds
|
|
: undefined;
|
|
|
|
if (refreshIntervalSeconds) {
|
|
refreshTimer = window.setInterval(
|
|
() => void loadDashboard(),
|
|
refreshIntervalSeconds * 1000,
|
|
);
|
|
}
|
|
}
|
|
|
|
void loadDashboard();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (refreshTimer) window.clearInterval(refreshTimer);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<AppStateView
|
|
dashboard={dashboard}
|
|
theme={theme}
|
|
onThemeChange={setTheme}
|
|
/>
|
|
);
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
function getThemeStorage(): Storage | undefined {
|
|
try {
|
|
return window.localStorage;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|