feat(housekeeping): deliver system vertical
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
createCorrelationId,
|
||||
fail,
|
||||
type HousekeepingResult,
|
||||
} from "../../../foundation/contracts";
|
||||
import { HousekeepingPageShell } from "../../../foundation/page/housekeeping-page-shell";
|
||||
import { HousekeepingPageState } from "../../../foundation/page/housekeeping-page-state";
|
||||
import type { HousekeepingPageInput } from "../../../route-handlers";
|
||||
import {
|
||||
type SystemObservabilityQueryData,
|
||||
type SystemObservabilityQueryInput,
|
||||
systemObservabilityQuery,
|
||||
} from "../queries/observability";
|
||||
|
||||
interface SystemObservabilityPageProps {
|
||||
readonly result?: HousekeepingResult<SystemObservabilityQueryData>;
|
||||
}
|
||||
|
||||
function pageState(
|
||||
kind: "loading" | "empty" | "error",
|
||||
title: string,
|
||||
description: string,
|
||||
) {
|
||||
return (
|
||||
<div data-housekeeping-state={kind}>
|
||||
<HousekeepingPageState
|
||||
state={kind}
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function forbiddenState() {
|
||||
return (
|
||||
<section
|
||||
role="alert"
|
||||
data-housekeeping-state="forbidden"
|
||||
className="rounded-lg border border-[var(--admin-error)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2 className="font-medium text-[var(--admin-text)]">Access denied</h2>
|
||||
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
|
||||
Your account cannot view this observability source.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function observabilityContent(data: SystemObservabilityQueryData) {
|
||||
if (data.kind === "devops") {
|
||||
return (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
|
||||
<h2 className="font-medium text-[var(--admin-text)]">
|
||||
Service health
|
||||
</h2>
|
||||
{data.health ? (
|
||||
<dl className="mt-3 grid grid-cols-2 gap-2 text-sm text-[var(--admin-text-muted)]">
|
||||
<dt>Database</dt>
|
||||
<dd>{data.health.dbOk ? "Available" : "Unavailable"}</dd>
|
||||
<dt>Redis</dt>
|
||||
<dd>
|
||||
{data.health.redisOk === null
|
||||
? "Not configured"
|
||||
: data.health.redisOk
|
||||
? "Available"
|
||||
: "Unavailable"}
|
||||
</dd>
|
||||
<dt>Emulator</dt>
|
||||
<dd>{data.health.emulatorOk ? "Available" : "Unavailable"}</dd>
|
||||
<dt>Online users</dt>
|
||||
<dd>{data.health.onlineUsers}</dd>
|
||||
</dl>
|
||||
) : (
|
||||
<p className="mt-3 text-sm text-[var(--admin-text-muted)]">
|
||||
Health data is unavailable.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
{observationRows("Recent emulator errors", data.errors)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.kind === "devops-errors") {
|
||||
return observationRows("Emulator errors", data.errors);
|
||||
}
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{data.metrics.map((metric) => (
|
||||
<section
|
||||
key={metric.label}
|
||||
className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
{metric.label}
|
||||
</p>
|
||||
<p className="mt-1 text-xl font-semibold text-[var(--admin-text)]">
|
||||
{metric.value}
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
{observationRows(
|
||||
data.kind === "logs" ? "Recent entries" : "Details",
|
||||
data.rows,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function observationRows(
|
||||
title: string,
|
||||
rows: readonly { id: string; primary: string; secondary?: string }[],
|
||||
) {
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
|
||||
<h2 className="font-medium text-[var(--admin-text)]">{title}</h2>
|
||||
<ul className="mt-3 divide-y divide-[var(--admin-border)]">
|
||||
{rows.map((row) => (
|
||||
<li key={row.id} className="py-2 text-sm">
|
||||
<p className="font-medium text-[var(--admin-text)]">
|
||||
{row.primary}
|
||||
</p>
|
||||
{row.secondary ? (
|
||||
<p className="break-words text-[var(--admin-text-muted)]">
|
||||
{row.secondary}
|
||||
</p>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function isEmpty(data: SystemObservabilityQueryData): boolean {
|
||||
if (data.kind === "devops")
|
||||
return data.health === null && data.errors.length === 0;
|
||||
if (data.kind === "devops-errors") return data.errors.length === 0;
|
||||
return data.metrics.length === 0 && data.rows.length === 0;
|
||||
}
|
||||
|
||||
export function SystemObservabilityPage({
|
||||
result,
|
||||
}: SystemObservabilityPageProps) {
|
||||
let body: ReactNode;
|
||||
if (!result) {
|
||||
body = pageState(
|
||||
"loading",
|
||||
"Loading observability data",
|
||||
"Reading metrics and operational logs.",
|
||||
);
|
||||
} else if (!result.ok) {
|
||||
body =
|
||||
result.error.code === "FORBIDDEN"
|
||||
? forbiddenState()
|
||||
: pageState(
|
||||
"error",
|
||||
"Observability data unavailable",
|
||||
`Request ${result.correlationId} could not be completed.`,
|
||||
);
|
||||
} else if (isEmpty(result.data)) {
|
||||
body = pageState(
|
||||
"empty",
|
||||
"No observations",
|
||||
"The selected source returned no records.",
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<div
|
||||
data-housekeeping-state={
|
||||
result.data.partialDependencies.length > 0 ? "partial" : "ready"
|
||||
}
|
||||
className="space-y-4"
|
||||
>
|
||||
{result.data.partialDependencies.length > 0 ? (
|
||||
<HousekeepingPageState
|
||||
state="partial"
|
||||
partialLabel="Partial data"
|
||||
title="Some observability data is unavailable"
|
||||
description={`Unavailable: ${result.data.partialDependencies.join(", ")}`}
|
||||
/>
|
||||
) : null}
|
||||
{observabilityContent(result.data)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HousekeepingPageShell
|
||||
title="System observability"
|
||||
description="Review metrics, service health, and operational records."
|
||||
>
|
||||
{body}
|
||||
</HousekeepingPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
export async function renderSystemObservabilityPage(
|
||||
input: HousekeepingPageInput,
|
||||
) {
|
||||
const routeId = input.match
|
||||
.routeId as SystemObservabilityQueryInput["routeId"];
|
||||
const supported = routeId.startsWith("system.observability.");
|
||||
const result = supported
|
||||
? await systemObservabilityQuery.run(input.context, { routeId })
|
||||
: fail("NOT_FOUND", "errors.housekeeping.notFound", createCorrelationId());
|
||||
return <SystemObservabilityPage result={result} />;
|
||||
}
|
||||
Reference in New Issue
Block a user