import { useEffect, useState } from "react"; import type { ApiSettings } from "@govoplan/core-webui"; import { ActionBlockerHint, AdminPageLayout, adminErrorMessage, Button, ConfirmDialog, dispatchPlatformModulesChanged, DocumentationHelpLink, formatDateTime, MetricCard, StageRail, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedDraftGuard, type FormatDateTimeOptions } from "@govoplan/core-webui"; import { Check, Clock, FileText, Pencil, Send } from "lucide-react"; 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"; import { installerRequestMatchesPlan, moduleInstallerQueueBlock, moduleInstallerWorkflowStages, type ModuleInstallerQueueBlock, type ModuleInstallerWorkflowStageId, type ModuleInstallerWorkflowStageState } from "./moduleInstallerWorkflow"; import { ADMIN_INTERFACE_I18N, MODULE_LIFECYCLE_DOCUMENTATION } from "./interfacePatterns"; const MODULE_INSTALLER_I18N = { queueUnavailable: "i18n:govoplan-admin.queue_supervised_run_unavailable.f1a20202", operatorPlan: "i18n:govoplan-admin.operator_install_plan.b203aabc", requiredAction: "i18n:govoplan-admin.required_action.f1a20203", responsibleActor: "i18n:govoplan-admin.who_can_fix_it.f1a20204", resolutionTarget: "i18n:govoplan-admin.where_to_go.f1a20205" } as const; 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(""); const [confirmClearPlan, setConfirmClearPlan] = useState(false); const [confirmMaintenance, setConfirmMaintenance] = useState(false); const [cancelRequestId, setCancelRequestId] = 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) === ""; const latestInstallerRequest = installerRequests?.requests[0] ?? null; const currentInstallerRequest = latestInstallerRequest && installerRequestMatchesPlan( installPlan?.updated_at, latestInstallerRequest.created_at ) ? latestInstallerRequest : null; const currentInstallerRun = currentInstallerRequest ? installerRuns?.runs.find((run) => run.request_id === currentInstallerRequest.request_id) ?? null : null; const installerWorkflowInput = { planItemCount: draftPlanItems.length, planDirty, planValid, preflightAllowed: installPlan?.preflight?.allowed ?? null, maintenanceEnabled, canWrite, canAccessMaintenance, requestStatus: currentInstallerRequest?.status, runStatus: currentInstallerRun?.status }; const installerStages = moduleInstallerWorkflowStages(installerWorkflowInput); const installerQueueBlock = moduleInstallerQueueBlock(installerWorkflowInput); const installerQueueBlockReason = installerQueueBlock ? moduleInstallerQueueBlockReason( installerQueueBlock, planValidationError(draftPlanItems), installPlan?.preflight?.issues[0]?.message ) : ""; 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); const saveDisabledReason = busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !dirty ? ADMIN_INTERFACE_I18N.noPendingChanges : !maintenanceEnabled && moduleStateRequest ? ADMIN_INTERFACE_I18N.maintenanceRequired : undefined; 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 && <>
({ id: stage.id, label: moduleInstallerStageLabel(stage.id), icon: stage.id === "plan" ?