import { useEffect, useState } from "react"; import type { ApiSettings } from "@govoplan/core-webui"; import { AdminPageLayout, adminErrorMessage, Button, dispatchPlatformModulesChanged, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { cancelModuleInstallerRequest, clearModuleInstallPlan, createConfigurationChangeRequest, createModuleInstallerRequest, fetchModuleCatalog, fetchModuleInstallPlan, fetchModuleInstallerRequests, fetchModuleInstallerRuns, fetchModulePackageCatalog, fetchSystemSettings, planModuleInstallFromCatalog, planModuleUninstall, retryModuleInstallerRequest, updateModuleInstallPlan, updateModuleState, updateSystemSettings, type ConfigurationChangeRequest, type ModuleCatalogItem, type ModuleCatalogResponse, type ModuleInstallerRequestListResponse, type ModuleInstallerRequestOptions, type ModuleInstallerRunListResponse, type ModuleInstallPlanItem, type ModuleInstallPlanResponse, type ModuleInstallTargetItem, type ModuleMigrationPlanStep, type ModuleMigrationTaskPlanItem, type ModuleLicenseDiagnostics, type ModulePackageCatalogResponse, type ModulePackageCatalogItem } from "../../api/admin"; export default function ModuleManagementPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) { const [catalog, setCatalog] = useState(null); const [installPlan, setInstallPlan] = useState(null); const [installerRuns, setInstallerRuns] = useState(null); const [installerRequests, setInstallerRequests] = useState(null); const [packageCatalog, setPackageCatalog] = useState(null); const [draftEnabled, setDraftEnabled] = useState | null>(null); const [moduleStateRequest, setModuleStateRequest] = useState(null); const [draftPlanItems, setDraftPlanItems] = useState([]); const [requestOptions, setRequestOptions] = useState(() => defaultInstallerRequestOptions()); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [maintenanceBusy, setMaintenanceBusy] = useState(false); const [planBusy, setPlanBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); async function load() { setLoading(true); setError(""); try { const [loaded, loadedPlan, loadedRuns, loadedRequests, loadedPackageCatalog] = await Promise.all([ fetchModuleCatalog(settings), fetchModuleInstallPlan(settings), fetchModuleInstallerRuns(settings), fetchModuleInstallerRequests(settings), fetchModulePackageCatalog(settings)] ); setCatalog(loaded); setInstallPlan(loadedPlan); setInstallerRuns(loadedRuns); setInstallerRequests(loadedRequests); setPackageCatalog(loadedPackageCatalog); setDraftEnabled(new Set(loaded.desired_enabled)); setDraftPlanItems(loadedPlan.items); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } } useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl]); const desired = draftEnabled ?? new Set(catalog?.desired_enabled ?? []); const dirty = Boolean(catalog && !sameSet(desired, new Set(catalog.desired_enabled))); const pendingCount = catalog?.modules.filter((module) => module.current_enabled !== desired.has(module.id)).length ?? 0; const activeCount = catalog?.modules.filter((module) => module.current_enabled).length ?? 0; const desiredCount = desired.size; const maintenanceEnabled = Boolean(catalog?.maintenance_mode.enabled || installPlan?.maintenance_mode.enabled); const planDirty = Boolean(installPlan && JSON.stringify(normalizePlanItems(draftPlanItems)) !== JSON.stringify(normalizePlanItems(installPlan.items))); const planValid = planValidationError(draftPlanItems) === ""; useUnsavedDraftGuard({ dirty: dirty || planDirty, onSave: saveDirtyChanges, onDiscard: () => { if (catalog) setDraftEnabled(new Set(catalog.desired_enabled)); if (installPlan) setDraftPlanItems(installPlan.items); } }); const saveLabel = maintenanceEnabled ? moduleStateRequest ? "i18n:govoplan-admin.apply_approved_request.ab218d9a" : "i18n:govoplan-admin.create_request_and_apply.da79e153" : moduleStateRequest ? "i18n:govoplan-admin.waiting_for_maintenance.60d00019" : "i18n:govoplan-admin.create_dry_run_request.05b9cbdb"; const saveDisabled = !canWrite || !dirty || busy || !maintenanceEnabled && Boolean(moduleStateRequest); function setModuleDesired(module: ModuleCatalogItem, enabled: boolean) { if (module.protected) return; setSuccess(""); setModuleStateRequest(null); setDraftEnabled((current) => { const next = new Set(current ?? catalog?.desired_enabled ?? []); if (enabled) next.add(module.id);else next.delete(module.id); return next; }); } async function save(): Promise { if (!draftEnabled) return false; const enabledModules = Array.from(draftEnabled).sort(); const payloadKey = moduleStatePayloadKey(enabledModules); setBusy(true); setError(""); setSuccess(""); try { let request = moduleStateRequest?.payloadKey === payloadKey ? moduleStateRequest.request : null; if (!request) { request = await createConfigurationChangeRequest(settings, { key: "module_management.desired_enabled", value: { enabled_modules: enabledModules }, dry_run: true, target: { scope: "system" }, reason: "i18n:govoplan-admin.update_enabled_modules.9b7ae790" }); setModuleStateRequest({ request, payloadKey }); } if (!maintenanceEnabled) { setSuccess(i18nMessage("i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07", { value0: request.id })); return true; } const updated = await updateModuleState(settings, enabledModules, request.id); setCatalog(updated); setDraftEnabled(new Set(updated.desired_enabled)); setModuleStateRequest(null); setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_state_saved.e3e22105"); dispatchPlatformModulesChanged(); return true; } catch (err) { setError(adminErrorMessage(err)); return false; } finally { setBusy(false); } } async function enableMaintenanceMode() { setMaintenanceBusy(true); setError(""); setSuccess(""); try { const current = await fetchSystemSettings(settings); const updated = await updateSystemSettings(settings, { default_locale: current.default_locale, allow_tenant_custom_groups: current.allow_tenant_custom_groups, allow_tenant_custom_roles: current.allow_tenant_custom_roles, allow_tenant_api_keys: current.allow_tenant_api_keys, maintenance_mode: { enabled: true, message: current.maintenance_mode.message || "i18n:govoplan-admin.module_maintenance_in_progress.f6395152" } }); setCatalog((value) => value ? { ...value, maintenance_mode: updated.maintenance_mode } : value); setInstallPlan((value) => value ? { ...value, maintenance_mode: updated.maintenance_mode, preflight: value.preflight ? { ...value.preflight, maintenance_mode: updated.maintenance_mode.enabled } : value.preflight } : value); setSuccess("i18n:govoplan-admin.maintenance_mode_enabled_apply_the_module_state_.dda13e5c"); } catch (err) { setError(adminErrorMessage(err)); } finally { setMaintenanceBusy(false); } } async function savePlan(): Promise { setPlanBusy(true); setError(""); setSuccess(""); try { const updated = await updateModuleInstallPlan(settings, normalizePlanItems(draftPlanItems)); setInstallPlan(updated); setDraftPlanItems(updated.items); setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_install_plan_saved.5d43d944"); return true; } catch (err) { setError(adminErrorMessage(err)); return false; } finally { setPlanBusy(false); } } async function saveDirtyChanges(): Promise { let ok = true; if (dirty) ok = await save() && ok; if (planDirty) ok = await savePlan() && ok; return ok; } async function clearPlan() { setPlanBusy(true); setError(""); setSuccess(""); try { const updated = await clearModuleInstallPlan(settings); setInstallPlan(updated); setDraftPlanItems([]); setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_install_plan_cleared.afa2698d"); } catch (err) { setError(adminErrorMessage(err)); } finally { setPlanBusy(false); } } function updatePlanItem(index: number, patch: Partial) { setSuccess(""); setDraftPlanItems((items) => items.map((item, itemIndex) => itemIndex === index ? { ...item, ...patch } : item)); } function addPlanItem() { setDraftPlanItems((items) => [...items, emptyPlanItem()]); } async function addCatalogItem(item: ModulePackageCatalogItem) { setPlanBusy(true); setError(""); setSuccess(""); try { const updated = await planModuleInstallFromCatalog(settings, item.module_id); setInstallPlan(updated); setDraftPlanItems(updated.items); setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f"); } catch (err) { setError(adminErrorMessage(err)); } finally { setPlanBusy(false); } } async function addUninstallPlan(module: ModuleCatalogItem) { setPlanBusy(true); setError(""); setSuccess(""); try { const updated = await planModuleUninstall(settings, module.id); setInstallPlan(updated); setDraftPlanItems(updated.items); setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.package_uninstall_planned.56e8566d"); } catch (err) { setError(adminErrorMessage(err)); } finally { setPlanBusy(false); } } async function queueInstallerRequest() { if (!installPlan) return; setPlanBusy(true); setError(""); setSuccess(""); try { const request = await createModuleInstallerRequest(settings, normalizeRequestOptions(requestOptions, installPlan)); setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_queued_value.00726abb", { value0: request.request_id })); const [loadedRuns, loadedRequests] = await Promise.all([ fetchModuleInstallerRuns(settings), fetchModuleInstallerRequests(settings)] ); setInstallerRuns(loadedRuns); setInstallerRequests(loadedRequests); } catch (err) { setError(adminErrorMessage(err)); } finally { setPlanBusy(false); } } async function cancelInstallerRequest(requestId: string) { setPlanBusy(true); setError(""); setSuccess(""); try { const request = await cancelModuleInstallerRequest(settings, requestId); setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_cancelled_value.1d6d099d", { value0: request.request_id })); setInstallerRequests(await fetchModuleInstallerRequests(settings)); } catch (err) { setError(adminErrorMessage(err)); } finally { setPlanBusy(false); } } async function retryInstallerRequest(requestId: string) { setPlanBusy(true); setError(""); setSuccess(""); try { const request = await retryModuleInstallerRequest(settings, requestId); setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_queued_value.00726abb", { value0: request.request_id })); setInstallerRequests(await fetchModuleInstallerRequests(settings)); } catch (err) { setError(adminErrorMessage(err)); } finally { setPlanBusy(false); } } return ( }> {catalog && <>
{dirty &&

i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed

{moduleStateRequest &&

i18n:govoplan-admin.change_request_ready.b17e8c57 {moduleStateRequest.request.id}

} {!maintenanceEnabled &&
} {!maintenanceEnabled && !canAccessMaintenance &&

i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968

}
}
{catalog.modules.map((module) => { const desiredEnabled = desired.has(module.id); return (
{module.name} {module.id} v{module.version}
{module.protected && } {module.frontend_package && {module.frontend_package}} {module.migration_module_id && i18n:govoplan-admin.db.e355c23a {module.migration_module_id}}
i18n:govoplan-admin.requires.a4fc9357 {module.dependencies.length ? module.dependencies.join(", ") : "none"} i18n:govoplan-admin.optional.166b8369 {module.optional_dependencies.length ? module.optional_dependencies.join(", ") : "none"} i18n:govoplan-admin.dependents.072665c4 {module.dependents.length ? module.dependents.join(", ") : "none"}
setModuleDesired(module, checked)} /> {module.install_uninstall_supported && }
); })}
{catalog.notes.length > 0 &&
{catalog.notes.map((note) =>

{note}

)}
} {packageCatalog &&

i18n:govoplan-admin.package_catalog.b36e6d31

i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16

{packageCatalog.channel && } {packageCatalog.sequence !== null && packageCatalog.sequence !== undefined && }
{(packageCatalog.source || packageCatalog.path) &&

i18n:govoplan-admin.catalog.4a88d27b {packageCatalog.source_type ?? "source"}: {packageCatalog.source ?? packageCatalog.path}{packageCatalog.cache_used && packageCatalog.cache_path ? <> i18n:govoplan-admin.cached_from.d73c5b2a {packageCatalog.cache_path} : null}

} {(packageCatalog.generated_at || packageCatalog.not_before || packageCatalog.expires_at) &&

i18n:govoplan-admin.generated.a2edf57c {packageCatalog.generated_at ?? "unknown"} i18n:govoplan-admin.valid_after.c615c873 {packageCatalog.not_before ?? "now"} i18n:govoplan-admin.expires.2d21b7de {packageCatalog.expires_at ?? "unknown"}

} {packageCatalog.error &&

{packageCatalog.error}

} {packageCatalog.warnings.map((warning) =>

{warning}

)} {!packageCatalog.configured &&
i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3
} {packageCatalog.configured && packageCatalog.modules.length === 0 && !packageCatalog.error &&
i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb
} {packageCatalog.modules.length > 0 &&
{packageCatalog.modules.map((item) =>
{item.name} {item.module_id} {item.version && v{item.version}}
{item.python_package && {item.python_package}} {item.webui_package && {item.webui_package}} {item.tags.length > 0 && {item.tags.join(", ")}} {item.dependencies.length > 0 && i18n:govoplan-admin.dependencies.9f4f78d1 {item.dependencies.join(", ")}} {item.migration_safety !== "automatic" && i18n:govoplan-admin.migration_safety.729bdb3c {migrationSafetyLabel(item.migration_safety)}} {item.migration_after.length > 0 && i18n:govoplan-admin.after.695c89be {item.migration_after.join(", ")}} {item.migration_before.length > 0 && i18n:govoplan-admin.before.a11cf31f {item.migration_before.join(", ")}} {item.bridge_release && i18n:govoplan-admin.bridge_release.176cf241} {(item.current_version_min || item.current_version_max_exclusive) && i18n:govoplan-admin.current_window.73e25c9f {currentWindowLabel(item)}} {item.recovery_tested && i18n:govoplan-admin.recovery_tested.e831e2f1} {item.license_features.length > 0 && i18n:govoplan-admin.license.de13bf1a {item.license_features.join(", ")}} {item.license_missing_features.length > 0 && i18n:govoplan-admin.missing.feb2bbaa {item.license_missing_features.join(", ")}} {item.provides_interfaces.length > 0 && i18n:govoplan-admin.provides.221b70d9 {providedInterfacesLabel(item)}} {item.requires_interfaces.length > 0 && i18n:govoplan-admin.requires.a4fc9357 {requiredInterfacesLabel(item)}}
{item.description &&

{item.description}

} {item.migration_notes &&

{item.migration_notes}

} {item.bridge_notes &&

{item.bridge_notes}

} {item.recovery_notes &&

{item.recovery_notes}

} {item.license_reason &&

{item.license_reason}

}
)}
}
} {installPlan &&

i18n:govoplan-admin.operator_install_plan.b203aabc

i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf

{planValidationError(draftPlanItems) &&

{planValidationError(draftPlanItems)}

} {!maintenanceEnabled &&

i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629

} {installPlan.preflight &&
{installPlan.preflight.allowed ? "i18n:govoplan-admin.installer_preflight_passed.6d5f8060" : "i18n:govoplan-admin.installer_preflight_blocked.cf83bf79"} {installPlan.preflight.restart_required ? "i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63" : "i18n:govoplan-admin.no_package_restart_pending.efe6b8ce"} {installPlan.preflight.frontend_rebuild_required && i18n:govoplan-admin.webui_rebuild_required.010ce49f}
{installPlan.preflight.target_plan.length > 0 &&
i18n:govoplan-admin.target_plan.524ea0c8 {installPlan.preflight.target_plan.map((target) =>
{target.module_id} - {target.action} {targetVersionLabel(target)}

{migrationSafetyLabel(target.migration_safety)}{target.data_safety_acknowledged ? " - i18n:govoplan-admin.acknowledged.d08d7c6d" : ""}

{target.bridge_release &&

i18n:govoplan-admin.bridge_release.176cf241

} {target.recovery_tested &&

i18n:govoplan-admin.recovery_tested.e831e2f1

} {target.python_ref &&

{target.python_ref}

} {target.migration_notes &&

{target.migration_notes}

} {target.bridge_notes &&

{target.bridge_notes}

} {target.recovery_notes &&

{target.recovery_notes}

}
)}
} {(installPlan.preflight.migration_plan.steps.length > 0 || installPlan.preflight.migration_plan.tasks.length > 0) &&
i18n:govoplan-admin.migration_plan.f42b9d90 i18n:govoplan-admin.enabled_modules.3c38e9ff {installPlan.preflight.migration_plan.enabled_modules.join(", ")} {installPlan.preflight.migration_plan.steps.map((step, index) =>
{index + 1}. {step.module_id} - {migrationPhaseLabel(step.phase)} {migrationSourceLabel(step.source)}{step.metadata_pending ? " - i18n:govoplan-admin.metadata_pending.25190d87" : ""}

{migrationSafetyLabel(step.migration_safety)}

{step.reason &&

{step.reason}

}
)} {installPlan.preflight.migration_plan.tasks.length > 0 && i18n:govoplan-admin.migration_tasks.a4410d3a} {installPlan.preflight.migration_plan.tasks.map((task) =>
{task.module_id} - {migrationTaskPhaseLabel(task.phase)} {task.summary}

{migrationSourceLabel(task.source)}{task.metadata_pending ? " - i18n:govoplan-admin.executor_pending.7633225b" : ""}{task.has_executor ? " - i18n:govoplan-admin.executor_available.507de429" : ""}

{migrationSafetyLabel(task.safety)} - {task.idempotent ? "i18n:govoplan-admin.idempotent.47a0f2d6" : "i18n:govoplan-admin.not_idempotent.d3014c71"} - i18n:govoplan-admin.task_version.f0f26b92 {task.task_version}

)}
} {installPlan.preflight.issues.length > 0 &&
{installPlan.preflight.issues.map((issue, index) =>
{issue.severity} {issue.module_id ? `${issue.module_id}: ` : ""}{issue.message}
)}
} {installPlan.preflight.checklist.length > 0 &&
{installPlan.preflight.checklist.map((item) =>
{item.label} {item.status} {item.detail &&

{item.detail}

}
)}
}
}
{draftPlanItems.length === 0 &&
i18n:govoplan-admin.no_package_changes_planned.f12d8b33
} {draftPlanItems.map((item, index) =>
{item.catalog?.channel && } {item.catalog?.sequence !== null && item.catalog?.sequence !== undefined && } {item.catalog?.key_id && }
updatePlanItem(index, { destroy_data: checked })} disabled={!canWrite || planBusy || item.action !== "uninstall"} /> updatePlanItem(index, { data_safety_acknowledged: checked })} disabled={!canWrite || planBusy || item.action === "uninstall"} />
)}
{installerCommand(installPlan.preflight?.frontend_rebuild_required ?? false)}
{installPlan.preflight?.commands.length ?
{installPlan.preflight.commands.join("\n")}
: installPlan.commands.length > 0 &&
{installPlan.commands.join("\n")}
} {installPlan.notes.length > 0 &&
{installPlan.notes.map((note) =>

{note}

)}
}

i18n:govoplan-admin.daemon_execution.cc0fad8d

i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e

{planDirty &&

i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691

} {!maintenanceEnabled &&

i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f

} {!canAccessMaintenance &&

i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac

}
setRequestOptions((current) => ({ ...current, migrateDatabase: checked }))} disabled={!canWrite || planBusy} /> setRequestOptions((current) => ({ ...current, buildWebui: checked }))} disabled={!canWrite || planBusy} /> setRequestOptions((current) => ({ ...current, activateInstalledModules: checked }))} disabled={!canWrite || planBusy} /> setRequestOptions((current) => ({ ...current, removeUninstalledModulesFromDesired: checked }))} disabled={!canWrite || planBusy} />