Release Admin v0.1.19 guided configuration packages
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
+2
-2
@@ -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/);
|
||||
|
||||
@@ -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>[] = [
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user