Files
EpicNext-Cms/src/features/housekeeping/domains/system/pages/configuration.tsx
T
Simo c325c53774
CI / check (pull_request) Successful in 33s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
fix(housekeeping): harden system workflow boundaries
2026-08-29 00:25:47 +02:00

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} />;
}