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:
@@ -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>[] = [
|
||||
|
||||
Reference in New Issue
Block a user