Release Admin v0.1.19 guided configuration packages
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-22 18:04:34 +02:00
parent c22da17c64
commit ed424c729c
9 changed files with 248 additions and 20 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/admin-webui",
"version": "0.1.18",
"version": "0.1.19",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -17,7 +17,7 @@
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@govoplan/core-webui": "^0.1.35",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
@@ -27,6 +27,11 @@ for (const source of [changes, packages, templates, modules]) {
assert.match(changes, /DataGrid/);
assert.match(packages, /ReferenceSelect/);
assert.match(packages, /manualReferences/);
assert.match(packages, /reviewedFingerprint === currentFingerprint/);
assert.match(packages, /RequiredDataInputs/);
assert.match(packages, /type=\{item\.secret \? "password"/);
assert.match(packages, /ConfigurationRollbackState/);
assert.match(packages, /result\.provenance/);
assert.match(templates, /TableActionGroup/);
assert.match(modules, /StageRail/);
assert.match(modules, /ActionBlockerHint/);
+22
View File
@@ -129,6 +129,26 @@ export type ConfigurationPackageFragment = {
payload: Record<string, unknown>;
};
export type ConfigurationPackageRollback = {
status: "blocked_before_apply" | "not_required" | "database_restore_required" | "partial_apply_requires_recovery";
summary: string;
recovery_action?: string | null;
};
export type ConfigurationPackageExportProvenance = {
exported_at: string;
source_core_version: string;
module_versions: Record<string, string>;
tenant_id?: string | null;
exporter_id?: string | null;
selection: {
scopes: string[];
module_ids: string[];
object_refs: string[];
};
redacted_secret_keys: string[];
};
export type ConfigurationPackageRunPayload = {
package: Record<string, unknown>;
tenant_id?: string | null;
@@ -963,6 +983,7 @@ export function applyConfigurationPackage(settings: ApiSettings, payload: Config
diagnostics: ConfigurationPackageDiagnostic[];
created_refs: Record<string, string>;
updated_refs: Record<string, string>;
rollback?: ConfigurationPackageRollback | null;
}> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/apply", {
method: "POST",
@@ -979,6 +1000,7 @@ export function exportConfigurationPackage(settings: ApiSettings, payload: {
fragments: ConfigurationPackageFragment[];
data_requirements: ConfigurationPackageRequiredData[];
diagnostics: ConfigurationPackageDiagnostic[];
provenance?: ConfigurationPackageExportProvenance | null;
}> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/export", {
method: "POST",
@@ -11,6 +11,7 @@ import {
type ConfigurationChangeRequest,
type ConfigurationPackageDiagnostic,
type ConfigurationPackagePlanItem,
type ConfigurationPackageRollback,
type ConfigurationPackageRequiredData } from
"../../api/admin";
import {
@@ -63,6 +64,7 @@ type ApplyResult = {
diagnostics: ConfigurationPackageDiagnostic[];
created_refs: Record<string, string>;
updated_refs: Record<string, string>;
rollback?: ConfigurationPackageRollback | null;
};
export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: {settings: ApiSettings;auth: AuthInfo;canWrite: boolean;}) {
@@ -74,6 +76,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
const [changeRequestId, setChangeRequestId] = useState("");
const [manualReferences, setManualReferences] = useState(false);
const [dryRun, setDryRun] = useState<DryRunResult | null>(null);
const [reviewedFingerprint, setReviewedFingerprint] = useState("");
const [applyResult, setApplyResult] = useState<ApplyResult | null>(null);
const [exportText, setExportText] = useState("");
const [lastRequest, setLastRequest] = useState<ConfigurationChangeRequest | null>(null);
@@ -86,6 +89,22 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
const parsedPackage = useMemo(() => parseObject(packageText), [packageText]);
const parsedSuppliedData = useMemo(() => parseObject(suppliedDataText), [suppliedDataText]);
const canRun = Boolean(parsedPackage.value && parsedSuppliedData.value);
const currentFingerprint = useMemo(
() => packageRunFingerprint(parsedPackage.value, parsedSuppliedData.value, tenantId),
[parsedPackage.value, parsedSuppliedData.value, tenantId]
);
const dryRunReady = Boolean(
dryRun &&
reviewedFingerprint === currentFingerprint &&
!dryRun.diagnostics.some((item) => item.severity === "blocker")
);
const applyDisabledReason = !dryRun
? "i18n:govoplan-admin.run_preflight_before_apply.c214a009"
: reviewedFingerprint !== currentFingerprint
? "i18n:govoplan-admin.preflight_is_stale.c214a010"
: dryRun.diagnostics.some((item) => item.severity === "blocker")
? "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b"
: undefined;
const tenantProvider = useMemo(
() => createTenantReferenceProvider(settings),
[settings.accessToken, settings.apiBaseUrl, settings.apiKey]
@@ -132,6 +151,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
try {
const result = await dryRunConfigurationPackage(settings, runPayload(manifest, suppliedData));
setDryRun(result);
setReviewedFingerprint(packageRunFingerprint(manifest, suppliedData, tenantId));
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));
@@ -182,27 +202,31 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
}
}
async function exportAccessPackage() {
async function exportPackage() {
const manifest = requireParsedPackage();
if (!manifest) return;
const moduleIds = packageProviderModuleIds(manifest);
setBusy("export");
setError("");
setMessage("");
try {
const result = await exportConfigurationPackage(settings, {
tenant_id: tenantId.trim() || null,
module_ids: ["access"],
module_ids: moduleIds,
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",
package_id: `${String(manifest.package_id || "govoplan.configuration")}.export`,
name: String(manifest.name || "i18n:govoplan-admin.export.f3e4fadb"),
version: "0.1.0",
required_modules: [{ module_id: "access" }],
required_capabilities: ["configuration.provider", "access.configuration"],
required_modules: moduleIds.map((module_id) => ({ module_id })),
required_capabilities: ["configuration.provider", ...moduleIds.map((module_id) => `${module_id}.configuration`)],
fragments: result.fragments,
data_requirements: result.data_requirements
data_requirements: result.data_requirements,
provenance: result.provenance
};
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");
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.export_finished_with_blockers.e2f70611" : "i18n:govoplan-admin.export.f3e4fadb");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
@@ -210,6 +234,23 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
}
}
function setSuppliedDataValue(item: ConfigurationPackageRequiredData, rawValue: string | boolean) {
const current = parsedSuppliedData.value ?? {};
const next = { ...current };
if (rawValue === "") {
delete next[item.key];
} else if (item.data_type === "boolean") {
next[item.key] = Boolean(rawValue);
} else if (item.data_type === "integer") {
next[item.key] = Number.parseInt(String(rawValue), 10);
} else if (item.data_type === "number") {
next[item.key] = Number.parseFloat(String(rawValue));
} else {
next[item.key] = rawValue;
}
setSuppliedDataText(JSON.stringify(next, null, 2));
}
function runPayload(manifest: Record<string, unknown>, suppliedData: Record<string, unknown>, requestId?: string | null) {
return {
package: manifest,
@@ -306,14 +347,19 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
<div className="button-row compact-actions">
<Button onClick={() => void runDryRun()} disabled={!canRun || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : undefined}><Play size={16} /> i18n:govoplan-admin.dry_run.3d14659c</Button>
<Button onClick={() => void requestApproval()} disabled={!canWrite || !parsedPackage.value || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !parsedPackage.value ? ADMIN_INTERFACE_I18N.packageRequired : undefined}><Check size={16} /> i18n:govoplan-admin.request_approval.6245aea1</Button>
<Button variant="primary" onClick={() => setConfirmApply(true)} disabled={!canWrite || !canRun || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : undefined}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button>
<Button onClick={() => void exportAccessPackage()} disabled={Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : undefined}><Download size={16} /> i18n:govoplan-admin.export_access.9a2eb91e</Button>
<Button variant="primary" onClick={() => setConfirmApply(true)} disabled={!canWrite || !canRun || !dryRunReady || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : applyDisabledReason}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button>
<Button onClick={() => void exportPackage()} disabled={!parsedPackage.value || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !parsedPackage.value ? ADMIN_INTERFACE_I18N.packageRequired : undefined}><Download size={16} /> i18n:govoplan-admin.export.f3e4fadb</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} />
<RequiredDataInputs
items={dryRun.required_data}
values={parsedSuppliedData.value ?? {}}
onChange={setSuppliedDataValue}
/>
<RequiredData items={dryRun.required_data} />
<PackagePlan items={dryRun.plan} />
</Card>}
@@ -322,6 +368,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
<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} />
{applyResult.rollback && <ConfigurationRollbackState rollback={applyResult.rollback} />}
</Card>}
{exportText && <Card title="i18n:govoplan-admin.export.f3e4fadb">
@@ -354,6 +401,87 @@ function parseObject(text: string): {value: Record<string, unknown> | null;error
}
}
function packageRunFingerprint(manifest: Record<string, unknown> | null, suppliedData: Record<string, unknown> | null, tenantId: string): string {
if (!manifest || !suppliedData) return "";
return JSON.stringify({ manifest, suppliedData, tenantId: tenantId.trim() || null });
}
function packageProviderModuleIds(manifest: Record<string, unknown>): string[] {
const fragments = Array.isArray(manifest.fragments) ? manifest.fragments : [];
const moduleIds = fragments.flatMap((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
const moduleId = (item as Record<string, unknown>).module_id;
return typeof moduleId === "string" && moduleId.trim() ? [moduleId.trim()] : [];
});
return Array.from(new Set(moduleIds.length > 0 ? moduleIds : ["access"])).sort();
}
function RequiredDataInputs({ items, values, onChange }: {
items: ConfigurationPackageRequiredData[];
values: Record<string, unknown>;
onChange: (item: ConfigurationPackageRequiredData, value: string | boolean) => void;
}) {
if (items.length === 0) return null;
return (
<div className="module-installer-request-grid">
{items.map((item) => {
const value = values[item.key];
return (
<FormField key={item.key} label={item.label} documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
{item.data_type === "boolean" ? (
<ToggleSwitch
checked={Boolean(value)}
onChange={(checked) => onChange(item, checked)}
label={item.label}
help={item.description || undefined}
/>
) : (
<>
<input
type={item.secret ? "password" : item.data_type === "integer" || item.data_type === "number" ? "number" : "text"}
value={value === undefined || value === null ? "" : String(value)}
required={item.required}
autoComplete={item.secret ? "off" : undefined}
onChange={(event) => onChange(item, event.target.value)}
/>
{item.description ? <span className="muted small-note">{item.description}</span> : null}
</>
)}
</FormField>
);
})}
</div>
);
}
function ConfigurationRollbackState({ rollback }: { rollback: ConfigurationPackageRollback }) {
const status = rollback.status === "not_required" ? "success" : rollback.status === "blocked_before_apply" ? "inactive" : "warning";
const copy = {
blocked_before_apply: {
label: "i18n:govoplan-admin.package_no_changes_applied.c214a001",
summary: "i18n:govoplan-admin.package_blocked_before_apply.c214a002"
},
not_required: {
label: "i18n:govoplan-admin.package_no_rollback_needed.c214a003",
summary: "i18n:govoplan-admin.package_all_fragments_noop.c214a004"
},
database_restore_required: {
label: "i18n:govoplan-admin.package_keep_database_snapshot.c214a005",
summary: "i18n:govoplan-admin.package_snapshot_is_rollback.c214a006"
},
partial_apply_requires_recovery: {
label: "i18n:govoplan-admin.package_partial_apply.c214a007",
summary: "i18n:govoplan-admin.package_recover_before_retry.c214a008"
}
}[rollback.status];
return (
<div className="stack compact-stack">
<StatusBadge status={status} label={copy.label} />
<p>{copy.summary}</p>
</div>
);
}
function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) {
if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>;
const columns: DataGridColumn<ConfigurationPackageDiagnostic>[] = [
+20
View File
@@ -2,6 +2,16 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"en": {
"i18n:govoplan-admin.package_no_changes_applied.c214a001": "No changes applied",
"i18n:govoplan-admin.package_blocked_before_apply.c214a002": "Preflight or the first provider blocked before configuration changed.",
"i18n:govoplan-admin.package_no_rollback_needed.c214a003": "No rollback needed",
"i18n:govoplan-admin.package_all_fragments_noop.c214a004": "All package fragments were already current and produced no changes.",
"i18n:govoplan-admin.package_keep_database_snapshot.c214a005": "Keep the database snapshot",
"i18n:govoplan-admin.package_snapshot_is_rollback.c214a006": "Provider-owned configuration changed. Retain the reviewed pre-apply database snapshot until verification is complete.",
"i18n:govoplan-admin.package_partial_apply.c214a007": "Partial apply—recovery required",
"i18n:govoplan-admin.package_recover_before_retry.c214a008": "A provider blocked after earlier changes committed. Restore the snapshot or complete an explicitly supported compensation before retrying.",
"i18n:govoplan-admin.run_preflight_before_apply.c214a009": "Run preflight before applying this package.",
"i18n:govoplan-admin.preflight_is_stale.c214a010": "Package, deployment data, or tenant changed. Run preflight again before applying.",
"i18n:govoplan-admin.appearance_defaults": "Appearance defaults",
"i18n:govoplan-admin.system_palette_default": "System palette default",
"i18n:govoplan-admin.system_palette_default_help": "Validated default inherited by tenants and users without an explicit child choice.",
@@ -539,6 +549,16 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-admin.working.049ac820": "Working..."
},
"de": {
"i18n:govoplan-admin.package_no_changes_applied.c214a001": "Keine Änderungen angewendet",
"i18n:govoplan-admin.package_blocked_before_apply.c214a002": "Die Vorprüfung oder der erste Provider hat blockiert, bevor Konfiguration geändert wurde.",
"i18n:govoplan-admin.package_no_rollback_needed.c214a003": "Keine Rücksetzung erforderlich",
"i18n:govoplan-admin.package_all_fragments_noop.c214a004": "Alle Paketfragmente waren bereits aktuell und haben nichts geändert.",
"i18n:govoplan-admin.package_keep_database_snapshot.c214a005": "Datenbank-Snapshot aufbewahren",
"i18n:govoplan-admin.package_snapshot_is_rollback.c214a006": "Modul-eigene Konfiguration wurde geändert. Der geprüfte Datenbank-Snapshot vor der Anwendung muss bis zum Abschluss der Verifikation erhalten bleiben.",
"i18n:govoplan-admin.package_partial_apply.c214a007": "Teilanwendung Wiederherstellung erforderlich",
"i18n:govoplan-admin.package_recover_before_retry.c214a008": "Ein Provider hat nach bereits festgeschriebenen Änderungen blockiert. Vor einem neuen Versuch muss der Snapshot wiederhergestellt oder eine ausdrücklich unterstützte Kompensation abgeschlossen werden.",
"i18n:govoplan-admin.run_preflight_before_apply.c214a009": "Vor dem Anwenden dieses Pakets muss die Vorprüfung ausgeführt werden.",
"i18n:govoplan-admin.preflight_is_stale.c214a010": "Paket, Einsatzdaten oder Mandant wurden geändert. Vor dem Anwenden muss die Vorprüfung erneut ausgeführt werden.",
"i18n:govoplan-admin.appearance_defaults": "Darstellungsstandards",
"i18n:govoplan-admin.system_palette_default": "Systemstandard für die Farbpalette",
"i18n:govoplan-admin.system_palette_default_help": "Geprüfter Standard für Mandanten und Benutzer ohne eigene Auswahl.",