chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:16 +02:00
parent 0f2a9beca7
commit 970625edff
43 changed files with 2959 additions and 518 deletions
@@ -0,0 +1,374 @@
import { useEffect, useMemo, useState } from "react";
import { Check, Download, Play, RefreshCw } from "lucide-react";
import type { ApiSettings } from "@govoplan/core-webui";
import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, i18nMessage } from "@govoplan/core-webui";
import {
applyConfigurationPackage,
createConfigurationChangeRequest,
dryRunConfigurationPackage,
exportConfigurationPackage,
fetchConfigurationPackageCatalogValidation,
type ConfigurationChangeRequest,
type ConfigurationPackageDiagnostic,
type ConfigurationPackagePlanItem,
type ConfigurationPackageRequiredData } from
"../../api/admin";
const SAMPLE_ACCESS_PACKAGE = {
package_id: "govoplan.access.minimal-office",
name: "i18n:govoplan-admin.minimal_office_access.af48f49a",
version: "0.1.0",
required_modules: [{ module_id: "access" }],
required_capabilities: ["configuration.provider", "access.configuration"],
fragments: [
{
module_id: "access",
fragment_type: "roles",
payload: {
items: [
{
slug: "case-clerk",
name: "i18n:govoplan-admin.case_clerk.b78a314a",
description: "i18n:govoplan-admin.handles_incoming_administrative_work.dc80c349",
permissions: ["admin:users:read"]
}]
}
},
{
module_id: "access",
fragment_type: "groups",
payload: { items: [{ slug: "front-office", name: "i18n:govoplan-admin.front_office.d9dcfee1" }] }
},
{
module_id: "access",
fragment_type: "group_role_assignments",
payload: { items: [{ group: "front-office", role: "case-clerk" }] }
}]
};
type DryRunResult = {
diagnostics: ConfigurationPackageDiagnostic[];
required_data: ConfigurationPackageRequiredData[];
plan: ConfigurationPackagePlanItem[];
};
type ApplyResult = {
diagnostics: ConfigurationPackageDiagnostic[];
created_refs: Record<string, string>;
updated_refs: Record<string, string>;
};
export default function ConfigurationPackagesPanel({ settings, canWrite }: {settings: ApiSettings;canWrite: boolean;}) {
const [catalogValidation, setCatalogValidation] = useState<Record<string, unknown> | null>(null);
const [packageText, setPackageText] = useState(() => JSON.stringify(SAMPLE_ACCESS_PACKAGE, null, 2));
const [tenantId, setTenantId] = useState("");
const [suppliedDataText, setSuppliedDataText] = useState("{}");
const [changeRequestId, setChangeRequestId] = useState("");
const [dryRun, setDryRun] = useState<DryRunResult | null>(null);
const [applyResult, setApplyResult] = useState<ApplyResult | null>(null);
const [exportText, setExportText] = useState("");
const [lastRequest, setLastRequest] = useState<ConfigurationChangeRequest | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState("");
const [error, setError] = useState("");
const [message, setMessage] = useState("");
const parsedPackage = useMemo(() => parseObject(packageText), [packageText]);
const parsedSuppliedData = useMemo(() => parseObject(suppliedDataText), [suppliedDataText]);
const canRun = Boolean(parsedPackage.value && parsedSuppliedData.value);
async function loadCatalog() {
setLoading(true);
setError("");
try {
const response = await fetchConfigurationPackageCatalogValidation(settings);
setCatalogValidation(response.validation);
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setLoading(false);
}
}
useEffect(() => {void loadCatalog();}, [settings.accessToken, settings.apiBaseUrl]);
async function runDryRun() {
const manifest = requireParsedPackage();
const suppliedData = requireParsedSuppliedData();
if (!manifest || !suppliedData) return;
setBusy("dry-run");
setError("");
setMessage("");
setApplyResult(null);
try {
const result = await dryRunConfigurationPackage(settings, runPayload(manifest, suppliedData));
setDryRun(result);
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b" : "i18n:govoplan-admin.preflight_passed.c0c99055");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
async function requestApproval() {
const manifest = requireParsedPackage();
if (!manifest) return;
setBusy("approval");
setError("");
setMessage("");
try {
const request = await createConfigurationChangeRequest(settings, {
key: "configuration_packages.apply",
value: manifest,
dry_run: true,
target: tenantId.trim() ? { tenant_id: tenantId.trim() } : {},
reason: "i18n:govoplan-admin.configuration_package_apply.c270e5f3"
});
setLastRequest(request);
setChangeRequestId(request.id);
setMessage(i18nMessage("i18n:govoplan-admin.change_request_created_value.4af6d3d2", { value0: request.id }));
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
async function applyPackage() {
const manifest = requireParsedPackage();
const suppliedData = requireParsedSuppliedData();
if (!manifest || !suppliedData) return;
setBusy("apply");
setError("");
setMessage("");
try {
const result = await applyConfigurationPackage(settings, runPayload(manifest, suppliedData, changeRequestId.trim() || null));
setApplyResult(result);
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.apply_finished_with_blockers.78487a12" : "i18n:govoplan-admin.package_applied.63782ce7");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
async function exportAccessPackage() {
setBusy("export");
setError("");
setMessage("");
try {
const result = await exportConfigurationPackage(settings, {
tenant_id: tenantId.trim() || null,
module_ids: ["access"],
scopes: tenantId.trim() ? ["tenant"] : ["system"]
});
const exported = {
package_id: tenantId.trim() ? "govoplan.export.access-tenant" : "govoplan.export.access-system",
name: tenantId.trim() ? "i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c" : "i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c",
version: "0.1.0",
required_modules: [{ module_id: "access" }],
required_capabilities: ["configuration.provider", "access.configuration"],
fragments: result.fragments,
data_requirements: result.data_requirements
};
setExportText(JSON.stringify(exported, null, 2));
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.export_finished_with_blockers.e2f70611" : "i18n:govoplan-admin.access_configuration_exported.098f200d");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
function runPayload(manifest: Record<string, unknown>, suppliedData: Record<string, unknown>, requestId?: string | null) {
return {
package: manifest,
tenant_id: tenantId.trim() || null,
supplied_data: suppliedData,
change_request_id: requestId ?? null
};
}
function requireParsedPackage(): Record<string, unknown> | null {
if (!parsedPackage.value) {
setError(parsedPackage.error || "i18n:govoplan-admin.package_json_must_be_an_object.db825bb3");
return null;
}
return parsedPackage.value;
}
function requireParsedSuppliedData(): Record<string, unknown> | null {
if (!parsedSuppliedData.value) {
setError(parsedSuppliedData.error || "i18n:govoplan-admin.supplied_data_json_must_be_an_object.b265eb92");
return null;
}
return parsedSuppliedData.value;
}
return (
<AdminPageLayout
title="i18n:govoplan-admin.configuration_packages.eb2f05f1"
description="i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929"
loading={loading}
error={error || parsedPackage.error || parsedSuppliedData.error || ""}
success={message}
actions={<Button onClick={() => void loadCatalog()} disabled={loading || Boolean(busy)}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button>}>
<Card title="i18n:govoplan-admin.catalog.4a88d27b">
<div className="button-row compact-actions">
<StatusBadge status={catalogValidation?.valid ? "success" : catalogValidation?.configured ? "warning" : "inactive"} label={catalogValidation?.valid ? "i18n:govoplan-admin.valid.a4aefa35" : catalogValidation?.configured ? "i18n:govoplan-admin.needs_attention.a126722e" : "i18n:govoplan-admin.not_configured.811931bb"} />
{catalogValidation?.signed !== undefined && <StatusBadge status={catalogValidation.signed ? "success" : "inactive"} label={catalogValidation.signed ? "i18n:govoplan-admin.signed.6e3665d8" : "i18n:govoplan-admin.unsigned.e91344ea"} />}
{catalogValidation?.trusted !== undefined && <StatusBadge status={catalogValidation.trusted ? "success" : "warning"} label={catalogValidation.trusted ? "i18n:govoplan-admin.trusted.99f7ed54" : "i18n:govoplan-admin.untrusted.cdc7838a"} />}
</div>
<pre className="code-panel module-install-plan-commands">{JSON.stringify(catalogValidation ?? {}, null, 2)}</pre>
</Card>
<Card title="i18n:govoplan-admin.package.7431e3df">
<div className="module-installer-request-grid">
<label className="wide"><span>i18n:govoplan-admin.tenant_id.59eba244</span><input value={tenantId} onChange={(event) => setTenantId(event.target.value)} placeholder="active tenant" /></label>
<label className="wide"><span>i18n:govoplan-admin.change_request_id.96ee3239</span><input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." /></label>
<label className="wide"><span>i18n:govoplan-admin.package_json.a2b10f38</span><textarea rows={18} value={packageText} onChange={(event) => setPackageText(event.target.value)} /></label>
<label className="wide"><span>i18n:govoplan-admin.supplied_data_json.6932bfb7</span><textarea rows={6} value={suppliedDataText} onChange={(event) => setSuppliedDataText(event.target.value)} /></label>
</div>
<div className="button-row compact-actions">
<Button onClick={() => void runDryRun()} disabled={!canRun || Boolean(busy)}><Play size={16} /> i18n:govoplan-admin.dry_run.3d14659c</Button>
<Button onClick={() => void requestApproval()} disabled={!canWrite || !parsedPackage.value || Boolean(busy)}><Check size={16} /> i18n:govoplan-admin.request_approval.6245aea1</Button>
<Button variant="primary" onClick={() => void applyPackage()} disabled={!canWrite || !canRun || Boolean(busy)}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button>
<Button onClick={() => void exportAccessPackage()} disabled={Boolean(busy)}><Download size={16} /> i18n:govoplan-admin.export_access.9a2eb91e</Button>
</div>
{lastRequest && <p className="muted small-note">i18n:govoplan-admin.last_request.4508ef35 {lastRequest.id} ({lastRequest.status})</p>}
</Card>
{dryRun && <Card title="i18n:govoplan-admin.preflight.8016a487">
<PackageDiagnostics diagnostics={dryRun.diagnostics} />
<RequiredData items={dryRun.required_data} />
<PackagePlan items={dryRun.plan} />
</Card>}
{applyResult && <Card title="i18n:govoplan-admin.apply_result.0fde1c3c">
<PackageDiagnostics diagnostics={applyResult.diagnostics} />
<ReferenceMap title="i18n:govoplan-admin.created.accf40c8" refs={applyResult.created_refs} />
<ReferenceMap title="i18n:govoplan-admin.updated.f2f8570d" refs={applyResult.updated_refs} />
</Card>}
{exportText && <Card title="i18n:govoplan-admin.export.f3e4fadb">
<textarea className="code-panel module-install-plan-commands" rows={16} value={exportText} onChange={(event) => setExportText(event.target.value)} />
</Card>}
</AdminPageLayout>);
}
function parseObject(text: string): {value: Record<string, unknown> | null;error: string;} {
try {
const value = JSON.parse(text) as unknown;
if (!value || typeof value !== "object" || Array.isArray(value)) return { value: null, error: "i18n:govoplan-admin.json_must_be_an_object.848569c9" };
return { value: value as Record<string, unknown>, error: "" };
} catch (err) {
return { value: null, error: err instanceof Error ? err.message : "i18n:govoplan-admin.invalid_json.01ccb74f" };
}
}
function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) {
if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>;
return (
<div className="admin-table-wrap">
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.severity.de314fa0</th><th>i18n:govoplan-admin.code.adac6937</th><th>i18n:govoplan-admin.owner.89ff3122</th><th>i18n:govoplan-admin.object.2883f191</th><th>i18n:govoplan-admin.message.68f4145f</th></tr></thead>
<tbody>
{diagnostics.map((item, index) =>
<tr key={`${item.code}-${index}`}>
<td><StatusBadge status={diagnosticTone(item.severity)} label={item.severity} /></td>
<td><code>{item.code}</code></td>
<td>{item.module_id || "-"}</td>
<td>{item.object_ref || "-"}</td>
<td>{item.message}{item.resolution ? <span className="muted block">{item.resolution}</span> : null}</td>
</tr>
)}
</tbody>
</table>
</div>);
}
function RequiredData({ items }: {items: ConfigurationPackageRequiredData[];}) {
if (items.length === 0) return null;
return (
<>
<h3>i18n:govoplan-admin.required_data.1b1c1b34</h3>
<div className="admin-table-wrap">
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.key.c67dd20e</th><th>i18n:govoplan-admin.label.74341e3c</th><th>i18n:govoplan-admin.type.3deb7456</th><th>i18n:govoplan-admin.required.eed6bfb4</th><th>i18n:govoplan-admin.secret.f4e7a874</th></tr></thead>
<tbody>
{items.map((item) =>
<tr key={item.key}>
<td><code>{item.key}</code></td>
<td>{item.label}</td>
<td>{item.data_type}</td>
<td>{item.required ? "yes" : "no"}</td>
<td>{item.secret ? "yes" : "no"}</td>
</tr>
)}
</tbody>
</table>
</div>
</>);
}
function PackagePlan({ items }: {items: ConfigurationPackagePlanItem[];}) {
if (items.length === 0) return <p className="muted">i18n:govoplan-admin.no_plan_items.7108c582</p>;
return (
<>
<h3>i18n:govoplan-admin.plan.ae2f98a0</h3>
<div className="admin-table-wrap">
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.action.97c89a4d</th><th>i18n:govoplan-admin.module.b8ff0289</th><th>i18n:govoplan-admin.fragment.3f19d616</th><th>i18n:govoplan-admin.id.474ae526</th><th>i18n:govoplan-admin.summary.12b71c3e</th></tr></thead>
<tbody>
{items.map((item, index) =>
<tr key={`${item.module_id}-${item.fragment_type}-${item.fragment_id ?? index}`}>
<td><StatusBadge status={planTone(item.action)} label={item.action} /></td>
<td>{item.module_id}</td>
<td>{item.fragment_type}</td>
<td>{item.fragment_id || "-"}</td>
<td>{item.summary || "-"}</td>
</tr>
)}
</tbody>
</table>
</div>
</>);
}
function ReferenceMap({ title, refs }: {title: string;refs: Record<string, string>;}) {
const entries = Object.entries(refs);
if (entries.length === 0) return null;
return (
<>
<h3>{title}</h3>
<div className="admin-table-wrap">
<table className="admin-table">
<tbody>
{entries.map(([key, value]) => <tr key={key}><td><code>{key}</code></td><td>{value}</td></tr>)}
</tbody>
</table>
</div>
</>);
}
function diagnosticTone(severity: ConfigurationPackageDiagnostic["severity"]): string {
if (severity === "blocker") return "error";
if (severity === "warning") return "warning";
return "inactive";
}
function planTone(action: ConfigurationPackagePlanItem["action"]): string {
if (action === "blocked") return "error";
if (action === "skip" || action === "noop") return "inactive";
if (action === "update" || action === "bind") return "warning";
return "success";
}