166 lines
4.6 KiB
TypeScript
166 lines
4.6 KiB
TypeScript
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 SystemConfigurationQueryData,
|
|
type SystemConfigurationQueryInput,
|
|
type SystemSettingRow,
|
|
systemConfigurationQuery,
|
|
} from "../queries/configuration";
|
|
|
|
interface SystemConfigurationPageProps {
|
|
readonly result?: HousekeepingResult<SystemConfigurationQueryData>;
|
|
}
|
|
|
|
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 system configuration.
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function settingList(title: string, rows: readonly SystemSettingRow[]) {
|
|
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>
|
|
<dl className="mt-3 divide-y divide-[var(--admin-border)]">
|
|
{rows.map((row) => (
|
|
<div
|
|
key={row.key}
|
|
className="grid gap-1 py-2 text-sm md:grid-cols-[minmax(12rem,1fr)_2fr]"
|
|
>
|
|
<dt className="font-medium text-[var(--admin-text)]">{row.key}</dt>
|
|
<dd className="break-words text-[var(--admin-text-muted)]">
|
|
{row.value}
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function configurationContent(data: SystemConfigurationQueryData) {
|
|
return data.kind === "settings" ? (
|
|
settingList("Website settings", data.settings)
|
|
) : (
|
|
<div className="grid gap-4 xl:grid-cols-2">
|
|
{settingList("Emulator settings", data.settings)}
|
|
<div className="space-y-2">
|
|
{settingList("Emulator texts", data.texts)}
|
|
<p className="text-xs text-[var(--admin-text-muted)]">
|
|
Showing {data.texts.length} of {data.textsTotal} text entries.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SystemConfigurationPage({
|
|
result,
|
|
}: SystemConfigurationPageProps) {
|
|
let body: ReactNode;
|
|
if (!result) {
|
|
body = pageState(
|
|
"loading",
|
|
"Loading configuration",
|
|
"Reading CMS and emulator settings.",
|
|
);
|
|
} else if (!result.ok) {
|
|
body =
|
|
result.error.code === "FORBIDDEN"
|
|
? forbiddenState()
|
|
: pageState(
|
|
"error",
|
|
"Configuration unavailable",
|
|
`Request ${result.correlationId} could not be completed.`,
|
|
);
|
|
} else {
|
|
const data = result.data;
|
|
const empty =
|
|
data.kind === "settings"
|
|
? data.settings.length === 0
|
|
: data.settings.length === 0 && data.texts.length === 0;
|
|
if (empty && data.partialDependencies.length === 0) {
|
|
body = pageState(
|
|
"empty",
|
|
"No configuration entries",
|
|
"The selected configuration source returned no entries.",
|
|
);
|
|
} else {
|
|
body = (
|
|
<div
|
|
data-housekeeping-state={
|
|
data.partialDependencies.length > 0 ? "partial" : "ready"
|
|
}
|
|
className="space-y-4"
|
|
>
|
|
{data.partialDependencies.length > 0 ? (
|
|
<HousekeepingPageState
|
|
state="partial"
|
|
partialLabel="Partial data"
|
|
title="Some configuration data is unavailable"
|
|
description={`Unavailable: ${data.partialDependencies.join(", ")}`}
|
|
/>
|
|
) : null}
|
|
{configurationContent(data)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<HousekeepingPageShell
|
|
title="System configuration"
|
|
description="Review CMS and emulator configuration sources."
|
|
>
|
|
{body}
|
|
</HousekeepingPageShell>
|
|
);
|
|
}
|
|
|
|
export async function renderSystemConfigurationPage(
|
|
input: HousekeepingPageInput,
|
|
) {
|
|
const queryInput: SystemConfigurationQueryInput | null =
|
|
input.match.routeId === "system.configuration.settings"
|
|
? { routeId: "system.configuration.settings" }
|
|
: input.match.routeId === "system.configuration.emulator"
|
|
? { routeId: "system.configuration.emulator" }
|
|
: null;
|
|
const result = queryInput
|
|
? await systemConfigurationQuery.run(input.context, queryInput)
|
|
: fail("NOT_FOUND", "errors.housekeeping.notFound", createCorrelationId());
|
|
return <SystemConfigurationPage result={result} />;
|
|
}
|