929 lines
58 KiB
TypeScript
929 lines
58 KiB
TypeScript
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<ModuleCatalogResponse | null>(null);
|
|
const [installPlan, setInstallPlan] = useState<ModuleInstallPlanResponse | null>(null);
|
|
const [installerRuns, setInstallerRuns] = useState<ModuleInstallerRunListResponse | null>(null);
|
|
const [installerRequests, setInstallerRequests] = useState<ModuleInstallerRequestListResponse | null>(null);
|
|
const [packageCatalog, setPackageCatalog] = useState<ModulePackageCatalogResponse | null>(null);
|
|
const [draftEnabled, setDraftEnabled] = useState<Set<string> | null>(null);
|
|
const [moduleStateRequest, setModuleStateRequest] = useState<ModuleStateRequestState | null>(null);
|
|
const [draftPlanItems, setDraftPlanItems] = useState<ModuleInstallPlanItem[]>([]);
|
|
const [requestOptions, setRequestOptions] = useState<InstallerRequestFormOptions>(() => 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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<ModuleInstallPlanItem>) {
|
|
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 (
|
|
<AdminPageLayout
|
|
title="i18n:govoplan-admin.system_modules.629539f2"
|
|
description="i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd"
|
|
loading={loading}
|
|
error={error}
|
|
success={success}
|
|
actions={<><Button onClick={() => void load()} disabled={loading || busy}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={saveDisabled}>{busy ? "i18n:govoplan-admin.working.049ac820" : saveLabel}</Button></>}>
|
|
|
|
{catalog && <>
|
|
<div className="metric-grid module-management-metrics">
|
|
<ModuleMetric label="i18n:govoplan-admin.installed.7bb4405c" value={catalog.modules.length} detail="i18n:govoplan-admin.discovered_packages.660902de" />
|
|
<ModuleMetric label="i18n:govoplan-admin.active.a733b809" value={activeCount} detail="i18n:govoplan-admin.running_registry.5274b24b" />
|
|
<ModuleMetric label="i18n:govoplan-admin.saved.c0ae8f6e" value={desiredCount} detail="i18n:govoplan-admin.startup_state.d1300be6" />
|
|
<ModuleMetric label="i18n:govoplan-admin.drift.4876f7b9" value={pendingCount} detail={pendingCount ? "i18n:govoplan-admin.runtime_differs.43d1fd78" : "i18n:govoplan-admin.runtime_matches.a84afa48"} tone={pendingCount ? "warning" : "good"} />
|
|
<ModuleMetric label="i18n:govoplan-admin.maintenance.94de303b" value={maintenanceEnabled ? "i18n:govoplan-admin.on.e0049a66" : "i18n:govoplan-admin.off.e3de5ab0"} detail={canAccessMaintenance ? "i18n:govoplan-admin.bypass_allowed.4e347c27" : "i18n:govoplan-admin.bypass_denied.ab987400"} tone={maintenanceEnabled ? "warning" : "info"} />
|
|
</div>
|
|
|
|
{dirty && <div className="module-management-notes">
|
|
<p className="alert warning">i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed</p>
|
|
{moduleStateRequest && <p>i18n:govoplan-admin.change_request_ready.b17e8c57 <code>{moduleStateRequest.request.id}</code></p>}
|
|
{!maintenanceEnabled && <div className="button-row compact-actions">
|
|
<Button onClick={() => void enableMaintenanceMode()} disabled={!canWrite || !canAccessMaintenance || maintenanceBusy}>
|
|
{maintenanceBusy ? "i18n:govoplan-admin.enabling.2b8e03e6" : "i18n:govoplan-admin.enable_maintenance_mode.75f98d57"}
|
|
</Button>
|
|
</div>}
|
|
{!maintenanceEnabled && !canAccessMaintenance && <p className="alert warning">i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968</p>}
|
|
</div>}
|
|
|
|
<div className="module-management-list">
|
|
{catalog.modules.map((module) => {
|
|
const desiredEnabled = desired.has(module.id);
|
|
return (
|
|
<div key={module.id} className={`module-management-row${module.restart_required || module.current_enabled !== desiredEnabled ? " pending" : ""}`}>
|
|
<div className="module-management-main">
|
|
<div className="module-management-title">
|
|
<strong>{module.name}</strong>
|
|
<code>{module.id}</code>
|
|
<span>v{module.version}</span>
|
|
</div>
|
|
<div className="module-management-meta">
|
|
<ModuleStatus module={module} desiredEnabled={desiredEnabled} />
|
|
{module.protected && <StatusBadge status="locked" label="i18n:govoplan-admin.locked.a798882f" />}
|
|
{module.frontend_package && <span>{module.frontend_package}</span>}
|
|
{module.migration_module_id && <span>i18n:govoplan-admin.db.e355c23a {module.migration_module_id}</span>}
|
|
</div>
|
|
<div className="module-management-details">
|
|
<span>i18n:govoplan-admin.requires.a4fc9357 {module.dependencies.length ? module.dependencies.join(", ") : "none"}</span>
|
|
<span>i18n:govoplan-admin.optional.166b8369 {module.optional_dependencies.length ? module.optional_dependencies.join(", ") : "none"}</span>
|
|
<span>i18n:govoplan-admin.dependents.072665c4 {module.dependents.length ? module.dependents.join(", ") : "none"}</span>
|
|
</div>
|
|
</div>
|
|
<div className="module-management-toggle">
|
|
<ToggleSwitch
|
|
label={desiredEnabled ? "i18n:govoplan-admin.enabled.df174a3f" : "i18n:govoplan-admin.disabled.f4f4473d"}
|
|
checked={desiredEnabled}
|
|
disabled={!canWrite || module.protected || busy}
|
|
onChange={(checked) => setModuleDesired(module, checked)} />
|
|
|
|
{module.install_uninstall_supported && <Button onClick={() => void addUninstallPlan(module)} disabled={!canWrite || planBusy || module.current_enabled || module.desired_enabled}>i18n:govoplan-admin.plan_uninstall.e62804ab</Button>}
|
|
</div>
|
|
</div>);
|
|
|
|
})}
|
|
</div>
|
|
|
|
{catalog.notes.length > 0 && <div className="module-management-notes">
|
|
{catalog.notes.map((note) => <p key={note}>{note}</p>)}
|
|
</div>}
|
|
|
|
{packageCatalog && <div className="module-package-catalog">
|
|
<div className="module-install-plan-heading">
|
|
<div>
|
|
<h2>i18n:govoplan-admin.package_catalog.b36e6d31</h2>
|
|
<p>i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16</p>
|
|
</div>
|
|
<div className="button-row compact-actions">
|
|
{packageCatalog.channel && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.channel_value.d3894efe", { value0: packageCatalog.channel })} />}
|
|
{packageCatalog.sequence !== null && packageCatalog.sequence !== undefined && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.seq_value.0ae5fa47", { value0: packageCatalog.sequence })} />}
|
|
<StatusBadge
|
|
status={packageCatalog.trusted ? "success" : packageCatalog.signed ? "warning" : "inactive"}
|
|
label={packageCatalog.trusted ? "i18n:govoplan-admin.signed_and_trusted.bee31302" : packageCatalog.signed ? "i18n:govoplan-admin.signed_untrusted.864ed50c" : "i18n:govoplan-admin.unsigned.e91344ea"} />
|
|
|
|
</div>
|
|
</div>
|
|
{(packageCatalog.source || packageCatalog.path) && <p className="module-package-catalog-description">i18n:govoplan-admin.catalog.4a88d27b {packageCatalog.source_type ?? "source"}: <code>{packageCatalog.source ?? packageCatalog.path}</code>{packageCatalog.cache_used && packageCatalog.cache_path ? <> i18n:govoplan-admin.cached_from.d73c5b2a <code>{packageCatalog.cache_path}</code></> : null}</p>}
|
|
{(packageCatalog.generated_at || packageCatalog.not_before || packageCatalog.expires_at) && <p className="module-package-catalog-description">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"}</p>}
|
|
<LicenseStatus license={packageCatalog.license} />
|
|
{packageCatalog.error && <p className="alert warning">{packageCatalog.error}</p>}
|
|
{packageCatalog.warnings.map((warning) => <p key={warning} className="alert warning">{warning}</p>)}
|
|
{!packageCatalog.configured && <div className="module-install-plan-empty">i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3</div>}
|
|
{packageCatalog.configured && packageCatalog.modules.length === 0 && !packageCatalog.error && <div className="module-install-plan-empty">i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb</div>}
|
|
{packageCatalog.modules.length > 0 && <div className="module-package-catalog-list">
|
|
{packageCatalog.modules.map((item) =>
|
|
<div key={`${item.module_id}-${item.version ?? "latest"}`} className="module-package-catalog-row">
|
|
<div className="module-management-main">
|
|
<div className="module-management-title">
|
|
<strong>{item.name}</strong>
|
|
<code>{item.module_id}</code>
|
|
{item.version && <span>v{item.version}</span>}
|
|
</div>
|
|
<div className="module-management-details">
|
|
{item.python_package && <span>{item.python_package}</span>}
|
|
{item.webui_package && <span>{item.webui_package}</span>}
|
|
{item.tags.length > 0 && <span>{item.tags.join(", ")}</span>}
|
|
{item.dependencies.length > 0 && <span>i18n:govoplan-admin.dependencies.9f4f78d1 {item.dependencies.join(", ")}</span>}
|
|
{item.migration_safety !== "automatic" && <span>i18n:govoplan-admin.migration_safety.729bdb3c {migrationSafetyLabel(item.migration_safety)}</span>}
|
|
{item.migration_after.length > 0 && <span>i18n:govoplan-admin.after.695c89be {item.migration_after.join(", ")}</span>}
|
|
{item.migration_before.length > 0 && <span>i18n:govoplan-admin.before.a11cf31f {item.migration_before.join(", ")}</span>}
|
|
{item.bridge_release && <span>i18n:govoplan-admin.bridge_release.176cf241</span>}
|
|
{(item.current_version_min || item.current_version_max_exclusive) && <span>i18n:govoplan-admin.current_window.73e25c9f {currentWindowLabel(item)}</span>}
|
|
{item.recovery_tested && <span>i18n:govoplan-admin.recovery_tested.e831e2f1</span>}
|
|
{item.license_features.length > 0 && <span>i18n:govoplan-admin.license.de13bf1a {item.license_features.join(", ")}</span>}
|
|
{item.license_missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {item.license_missing_features.join(", ")}</span>}
|
|
{item.provides_interfaces.length > 0 && <span>i18n:govoplan-admin.provides.221b70d9 {providedInterfacesLabel(item)}</span>}
|
|
{item.requires_interfaces.length > 0 && <span>i18n:govoplan-admin.requires.a4fc9357 {requiredInterfacesLabel(item)}</span>}
|
|
</div>
|
|
{item.description && <p className="module-package-catalog-description">{item.description}</p>}
|
|
{item.migration_notes && <p className="module-package-catalog-description">{item.migration_notes}</p>}
|
|
{item.bridge_notes && <p className="module-package-catalog-description">{item.bridge_notes}</p>}
|
|
{item.recovery_notes && <p className="module-package-catalog-description">{item.recovery_notes}</p>}
|
|
{item.license_reason && <p className={`module-package-catalog-description${item.license_allowed ? "" : " alert warning"}`}>{item.license_reason}</p>}
|
|
</div>
|
|
<Button onClick={() => void addCatalogItem(item)} disabled={!canWrite || planBusy || !packageCatalog.valid || !item.license_allowed || !["install", "update"].includes(item.action)}>{catalogPlanButtonLabel(item, catalog)}</Button>
|
|
</div>
|
|
)}
|
|
</div>}
|
|
</div>}
|
|
|
|
{installPlan && <div className="module-install-plan-panel">
|
|
<div className="module-install-plan-heading">
|
|
<div>
|
|
<h2>i18n:govoplan-admin.operator_install_plan.b203aabc</h2>
|
|
<p>i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf</p>
|
|
</div>
|
|
<div className="button-row compact-actions">
|
|
<Button onClick={addPlanItem} disabled={!canWrite || planBusy}>i18n:govoplan-admin.add_plan_item.fa55f028</Button>
|
|
<Button onClick={() => void clearPlan()} disabled={!canWrite || planBusy || draftPlanItems.length === 0}>i18n:govoplan-admin.clear.719ea396</Button>
|
|
<Button variant="primary" onClick={() => void savePlan()} disabled={!canWrite || planBusy || !planDirty || !planValid}>{planBusy ? "i18n:govoplan-admin.saving.ae7e8875" : "i18n:govoplan-admin.save_plan.842dc280"}</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{planValidationError(draftPlanItems) && <p className="alert warning">{planValidationError(draftPlanItems)}</p>}
|
|
{!maintenanceEnabled && <p className="alert warning">i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629</p>}
|
|
|
|
{installPlan.preflight && <div className={`module-install-preflight ${installPlan.preflight.allowed ? "is-allowed" : "is-blocked"}`}>
|
|
<div className="module-install-preflight-title">
|
|
<strong>{installPlan.preflight.allowed ? "i18n:govoplan-admin.installer_preflight_passed.6d5f8060" : "i18n:govoplan-admin.installer_preflight_blocked.cf83bf79"}</strong>
|
|
<span>{installPlan.preflight.restart_required ? "i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63" : "i18n:govoplan-admin.no_package_restart_pending.efe6b8ce"}</span>
|
|
{installPlan.preflight.frontend_rebuild_required && <span>i18n:govoplan-admin.webui_rebuild_required.010ce49f</span>}
|
|
</div>
|
|
{installPlan.preflight.target_plan.length > 0 && <div className="module-install-checklist">
|
|
<strong>i18n:govoplan-admin.target_plan.524ea0c8</strong>
|
|
{installPlan.preflight.target_plan.map((target) =>
|
|
<div key={`${target.module_id}-${target.action}`} className={`module-install-checklist-item status-${targetStatus(target)}`}>
|
|
<strong>{target.module_id} - {target.action}</strong>
|
|
<span>{targetVersionLabel(target)}</span>
|
|
<p>{migrationSafetyLabel(target.migration_safety)}{target.data_safety_acknowledged ? " - i18n:govoplan-admin.acknowledged.d08d7c6d" : ""}</p>
|
|
{target.bridge_release && <p>i18n:govoplan-admin.bridge_release.176cf241</p>}
|
|
{target.recovery_tested && <p>i18n:govoplan-admin.recovery_tested.e831e2f1</p>}
|
|
{target.python_ref && <p>{target.python_ref}</p>}
|
|
{target.migration_notes && <p>{target.migration_notes}</p>}
|
|
{target.bridge_notes && <p>{target.bridge_notes}</p>}
|
|
{target.recovery_notes && <p>{target.recovery_notes}</p>}
|
|
</div>
|
|
)}
|
|
</div>}
|
|
{(installPlan.preflight.migration_plan.steps.length > 0 || installPlan.preflight.migration_plan.tasks.length > 0) && <div className="module-install-checklist">
|
|
<strong>i18n:govoplan-admin.migration_plan.f42b9d90</strong>
|
|
<span>i18n:govoplan-admin.enabled_modules.3c38e9ff {installPlan.preflight.migration_plan.enabled_modules.join(", ")}</span>
|
|
{installPlan.preflight.migration_plan.steps.map((step, index) =>
|
|
<div key={`${step.module_id}-${step.phase}-${index}`} className={`module-install-checklist-item status-${migrationStepStatus(step)}`}>
|
|
<strong>{index + 1}. {step.module_id} - {migrationPhaseLabel(step.phase)}</strong>
|
|
<span>{migrationSourceLabel(step.source)}{step.metadata_pending ? " - i18n:govoplan-admin.metadata_pending.25190d87" : ""}</span>
|
|
<p>{migrationSafetyLabel(step.migration_safety)}</p>
|
|
{step.reason && <p>{step.reason}</p>}
|
|
</div>
|
|
)}
|
|
{installPlan.preflight.migration_plan.tasks.length > 0 && <strong>i18n:govoplan-admin.migration_tasks.a4410d3a</strong>}
|
|
{installPlan.preflight.migration_plan.tasks.map((task) =>
|
|
<div key={`${task.module_id}-${task.task_id}-${task.phase}`} className={`module-install-checklist-item status-${migrationTaskStatus(task)}`}>
|
|
<strong>{task.module_id} - {migrationTaskPhaseLabel(task.phase)}</strong>
|
|
<span>{task.summary}</span>
|
|
<p>{migrationSourceLabel(task.source)}{task.metadata_pending ? " - i18n:govoplan-admin.executor_pending.7633225b" : ""}{task.has_executor ? " - i18n:govoplan-admin.executor_available.507de429" : ""}</p>
|
|
<p>{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}</p>
|
|
</div>
|
|
)}
|
|
</div>}
|
|
{installPlan.preflight.issues.length > 0 && <div className="module-install-preflight-issues">
|
|
{installPlan.preflight.issues.map((issue, index) =>
|
|
<div key={`${issue.code}-${issue.module_id ?? "global"}-${index}`} className={`module-install-preflight-issue severity-${issue.severity}`}>
|
|
<strong>{issue.severity}</strong>
|
|
<span>{issue.module_id ? `${issue.module_id}: ` : ""}{issue.message}</span>
|
|
</div>
|
|
)}
|
|
</div>}
|
|
{installPlan.preflight.checklist.length > 0 && <div className="module-install-checklist">
|
|
{installPlan.preflight.checklist.map((item) =>
|
|
<div key={item.id} className={`module-install-checklist-item status-${item.status}`}>
|
|
<strong>{item.label}</strong>
|
|
<span>{item.status}</span>
|
|
{item.detail && <p>{item.detail}</p>}
|
|
</div>
|
|
)}
|
|
</div>}
|
|
</div>}
|
|
|
|
<div className="module-install-plan-list">
|
|
{draftPlanItems.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_package_changes_planned.f12d8b33</div>}
|
|
{draftPlanItems.map((item, index) =>
|
|
<div className="module-install-plan-row" key={`${item.module_id}-${index}`}>
|
|
<div className="module-install-plan-row-meta">
|
|
<StatusBadge status={item.source === "catalog" ? "success" : "inactive"} label={item.source === "catalog" ? "i18n:govoplan-admin.catalog.4a88d27b" : "i18n:govoplan-admin.manual.17a486ca"} />
|
|
{item.catalog?.channel && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.channel_value.d3894efe", { value0: String(item.catalog.channel) })} />}
|
|
{item.catalog?.sequence !== null && item.catalog?.sequence !== undefined && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.seq_value.0ae5fa47", { value0: item.catalog.sequence })} />}
|
|
{item.catalog?.key_id && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.key_value.847b914f", { value0: String(item.catalog.key_id) })} />}
|
|
</div>
|
|
<label><span>i18n:govoplan-admin.action.97c89a4d</span><select value={item.action} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { action: event.target.value as ModuleInstallPlanItem["action"] })}><option value="install">i18n:govoplan-admin.install.fd6c3ebf</option><option value="update">i18n:govoplan-admin.update.503a059f</option><option value="uninstall">i18n:govoplan-admin.uninstall.a735da1d</option></select></label>
|
|
<label><span>i18n:govoplan-admin.module.b8ff0289</span><input value={item.module_id} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { module_id: event.target.value })} placeholder="files" /></label>
|
|
<label><span>i18n:govoplan-admin.python_package.34591c71</span><input value={item.python_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { python_package: event.target.value })} placeholder="govoplan-files" /></label>
|
|
<ToggleSwitch label="i18n:govoplan-admin.destroy_data.34557c3c" checked={Boolean(item.destroy_data)} onChange={(checked) => updatePlanItem(index, { destroy_data: checked })} disabled={!canWrite || planBusy || item.action !== "uninstall"} />
|
|
<ToggleSwitch label="i18n:govoplan-admin.data_safety_reviewed.b4774edc" checked={Boolean(item.data_safety_acknowledged)} onChange={(checked) => updatePlanItem(index, { data_safety_acknowledged: checked })} disabled={!canWrite || planBusy || item.action === "uninstall"} />
|
|
<label className="wide"><span>i18n:govoplan-admin.python_ref.1af35d0d</span><input value={item.python_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { python_ref: event.target.value })} placeholder="govoplan-files @ git+ssh://...@v0.1.4" /></label>
|
|
<label><span>i18n:govoplan-admin.webui_package.19645536</span><input value={item.webui_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { webui_package: event.target.value })} placeholder="@govoplan/files-webui" /></label>
|
|
<label className="wide"><span>i18n:govoplan-admin.webui_ref.cbae3559</span><input value={item.webui_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { webui_ref: event.target.value })} placeholder="git+ssh://...#v0.1.4" /></label>
|
|
<label><span>i18n:govoplan-admin.status.bae7d5be</span><select value={item.status} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { status: event.target.value as ModuleInstallPlanItem["status"] })}><option value="planned">i18n:govoplan-admin.planned.9cbe42aa</option><option value="applied">i18n:govoplan-admin.applied.a3e4a569</option><option value="blocked">i18n:govoplan-admin.blocked.99613c74</option></select></label>
|
|
<label className="wide"><span>i18n:govoplan-admin.notes.70440046</span><input value={item.notes ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { notes: event.target.value })} /></label>
|
|
<div className="module-install-plan-actions"><Button onClick={() => setDraftPlanItems((items) => items.filter((_, itemIndex) => itemIndex !== index))} disabled={!canWrite || planBusy}>i18n:govoplan-admin.remove.e963907d</Button></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<pre className="code-panel module-install-plan-commands">{installerCommand(installPlan.preflight?.frontend_rebuild_required ?? false)}</pre>
|
|
{installPlan.preflight?.commands.length ? <pre className="code-panel module-install-plan-commands">{installPlan.preflight.commands.join("\n")}</pre> : installPlan.commands.length > 0 && <pre className="code-panel module-install-plan-commands">{installPlan.commands.join("\n")}</pre>}
|
|
{installPlan.notes.length > 0 && <div className="module-management-notes">{installPlan.notes.map((note) => <p key={note}>{note}</p>)}</div>}
|
|
|
|
<div className="module-installer-request-panel">
|
|
<div className="module-install-plan-heading">
|
|
<div>
|
|
<h2>i18n:govoplan-admin.daemon_execution.cc0fad8d</h2>
|
|
<p>i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e</p>
|
|
</div>
|
|
<Button variant="primary" onClick={() => void queueInstallerRequest()} disabled={!canWrite || !canAccessMaintenance || planBusy || planDirty || !maintenanceEnabled || !installPlan.preflight?.allowed}>
|
|
i18n:govoplan-admin.queue_supervised_run.a53f248c
|
|
</Button>
|
|
</div>
|
|
{planDirty && <p className="alert warning">i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691</p>}
|
|
{!maintenanceEnabled && <p className="alert warning">i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f</p>}
|
|
{!canAccessMaintenance && <p className="alert warning">i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac</p>}
|
|
<div className="module-installer-request-grid">
|
|
<ToggleSwitch label="i18n:govoplan-admin.run_migrations.db6e0ce2" checked={requestOptions.migrateDatabase} onChange={(checked) => setRequestOptions((current) => ({ ...current, migrateDatabase: checked }))} disabled={!canWrite || planBusy} />
|
|
<ToggleSwitch label="i18n:govoplan-admin.build_webui.fe8ccad7" checked={requestOptions.buildWebui} onChange={(checked) => setRequestOptions((current) => ({ ...current, buildWebui: checked }))} disabled={!canWrite || planBusy} />
|
|
<ToggleSwitch label="i18n:govoplan-admin.activate_installs.731d22a1" checked={requestOptions.activateInstalledModules} onChange={(checked) => setRequestOptions((current) => ({ ...current, activateInstalledModules: checked }))} disabled={!canWrite || planBusy} />
|
|
<ToggleSwitch label="i18n:govoplan-admin.drop_uninstalls_from_startup.0e2a5c5b" checked={requestOptions.removeUninstalledModulesFromDesired} onChange={(checked) => setRequestOptions((current) => ({ ...current, removeUninstalledModulesFromDesired: checked }))} disabled={!canWrite || planBusy} />
|
|
<label><span>i18n:govoplan-admin.health_urls.3b86647c</span><textarea value={requestOptions.healthUrls} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, healthUrls: event.target.value }))} placeholder="http://127.0.0.1:8000/health" /></label>
|
|
<label><span>i18n:govoplan-admin.restart_commands.035997f9</span><textarea value={requestOptions.restartCommands} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, restartCommands: event.target.value }))} placeholder="systemctl restart govoplan.service" /></label>
|
|
<label><span>i18n:govoplan-admin.db_backup_command.5bded3ac</span><textarea value={requestOptions.databaseBackupCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseBackupCommand: event.target.value }))} placeholder={"pg_dump --format=custom \"$GOVOPLAN_DATABASE_URL\" > \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
|
|
<label><span>i18n:govoplan-admin.db_restore_command.de11eb01</span><textarea value={requestOptions.databaseRestoreCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCommand: event.target.value }))} placeholder={"pg_restore --clean --if-exists --dbname \"$GOVOPLAN_DATABASE_URL\" \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
|
|
<label><span>i18n:govoplan-admin.db_restore_check_command.750ae936</span><textarea value={requestOptions.databaseRestoreCheckCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCheckCommand: event.target.value }))} placeholder={"pg_restore --list \"$GOVOPLAN_DATABASE_BACKUP_PATH\" >/dev/null"} /></label>
|
|
</div>
|
|
</div>
|
|
</div>}
|
|
|
|
{installerRequests && <div className="module-installer-runs">
|
|
<div className="module-installer-runs-heading">
|
|
<div>
|
|
<h2>i18n:govoplan-admin.installer_requests.b88db439</h2>
|
|
<p>i18n:govoplan-admin.queued_daemon_handoffs_created_from_the_admin_ui.d7ae7914</p>
|
|
</div>
|
|
<StatusBadge
|
|
status={installerRequests.daemon.running ? statusTone(installerRequests.daemon.status) : "inactive"}
|
|
label={installerRequests.daemon.running ? i18nMessage("i18n:govoplan-admin.daemon_value.e54f5a31", { value0: installerRequests.daemon.status }) : "i18n:govoplan-admin.daemon_offline.314f19a4"} />
|
|
|
|
</div>
|
|
{installerRequests.requests.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_installer_requests_recorded_yet.17e630fa</div>}
|
|
{installerRequests.requests.length > 0 && <div className="module-installer-run-list">
|
|
{installerRequests.requests.map((request) =>
|
|
<div key={request.request_id} className="module-installer-run-row">
|
|
<div className="module-installer-run-main">
|
|
<div className="module-installer-run-title">
|
|
<strong>{request.request_id}</strong>
|
|
<StatusBadge status={statusTone(request.status)} label={request.status} />
|
|
</div>
|
|
<div className="module-management-details">
|
|
<span>i18n:govoplan-admin.created.0c78dab1 {formatDateTime(request.created_at)}</span>
|
|
<span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(request.started_at)}</span>
|
|
<span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(request.finished_at)}</span>
|
|
{request.cancelled_at && <span>i18n:govoplan-admin.cancelled.8c9dcfa8 {formatDateTime(request.cancelled_at)}</span>}
|
|
{request.retry_of && <span>i18n:govoplan-admin.retry_of.3a6bb304 {request.retry_of}</span>}
|
|
{traceId(request.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(request.trace)}</code></span>}
|
|
</div>
|
|
{request.error && <p className="module-installer-run-error">{request.error}</p>}
|
|
</div>
|
|
<div className="module-installer-run-actions">
|
|
{request.status === "queued" && <Button onClick={() => void cancelInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>i18n:govoplan-admin.cancel.77dfd213</Button>}
|
|
{(request.status === "failed" || request.status === "cancelled") && <Button onClick={() => void retryInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>i18n:govoplan-admin.retry.9f5cd8a2</Button>}
|
|
{request.record_path && <code>{request.record_path}</code>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>}
|
|
</div>}
|
|
|
|
{installerRuns && <div className="module-installer-runs">
|
|
<div className="module-installer-runs-heading">
|
|
<div>
|
|
<h2>i18n:govoplan-admin.installer_runs.e9e344e1</h2>
|
|
<p>i18n:govoplan-admin.recent_supervised_installer_records_and_the_curr.29860245</p>
|
|
</div>
|
|
<StatusBadge status={installerRuns.lock.locked ? "warning" : "success"} label={installerRuns.lock.locked ? "i18n:govoplan-admin.locked.a798882f" : "i18n:govoplan-admin.unlocked.b3da7025"} />
|
|
</div>
|
|
{installerRuns.runs.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_installer_runs_recorded_yet.f8b61964</div>}
|
|
{installerRuns.runs.length > 0 && <div className="module-installer-run-list">
|
|
{installerRuns.runs.map((run) =>
|
|
<div key={run.run_id} className="module-installer-run-row">
|
|
<div className="module-installer-run-main">
|
|
<div className="module-installer-run-title">
|
|
<strong>{run.run_id}</strong>
|
|
<StatusBadge status={statusTone(run.status)} label={run.status} />
|
|
{run.supervisor_status && <StatusBadge status={statusTone(run.supervisor_status)} label={i18nMessage("i18n:govoplan-admin.supervisor_value.d9f0c8eb", { value0: run.supervisor_status })} />}
|
|
{run.rollback_status && <StatusBadge status={statusTone(run.rollback_status)} label={i18nMessage("i18n:govoplan-admin.rollback_value.47e9c00b", { value0: run.rollback_status })} />}
|
|
</div>
|
|
<div className="module-management-details">
|
|
<span>i18n:govoplan-admin.modules.2dc3b299 {run.planned_modules.length ? run.planned_modules.join(", ") : "none"}</span>
|
|
{run.request_id && <span>i18n:govoplan-admin.request.43c2e7a7 {run.request_id}</span>}
|
|
{traceId(run.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(run.trace)}</code></span>}
|
|
<span>i18n:govoplan-admin.commands.f6a34aed {run.commands_count}</span>
|
|
<span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(run.started_at)}</span>
|
|
<span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(run.finished_at)}</span>
|
|
</div>
|
|
{run.error && <p className="module-installer-run-error">{run.error}</p>}
|
|
</div>
|
|
<code>{run.record_path}</code>
|
|
</div>
|
|
)}
|
|
</div>}
|
|
</div>}
|
|
</>}
|
|
</AdminPageLayout>);
|
|
|
|
}
|
|
|
|
function ModuleMetric({ label, value, detail, tone = "info" }: {label: string;value: string | number;detail: string;tone?: "info" | "good" | "warning";}) {
|
|
return <div className={`metric-card metric-${tone}`}><div className="metric-label">{label}</div><div className="metric-value">{value}</div><div className="metric-detail">{detail}</div></div>;
|
|
}
|
|
|
|
function LicenseStatus({ license }: {license: ModuleLicenseDiagnostics;}) {
|
|
const label = license.license_id ? `${license.license_id}${license.subject ? ` · ${license.subject}` : ""}` : license.configured ? "i18n:govoplan-admin.configured_license.3e73f8f5" : "i18n:govoplan-admin.no_license_configured.693cea1a";
|
|
return (
|
|
<div className="module-license-status">
|
|
<div className="button-row compact-actions">
|
|
<StatusBadge status={licenseTone(license)} label={licenseLabel(license)} />
|
|
<StatusBadge status={license.enforced ? "warning" : "inactive"} label={license.enforced ? "i18n:govoplan-admin.enforced.93ff909c" : "i18n:govoplan-admin.observe_only.c6a1f8bc"} />
|
|
{license.key_id && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.key_value.847b914f", { value0: license.key_id })} />}
|
|
</div>
|
|
<p className="module-package-catalog-description">{label}</p>
|
|
<div className="module-management-details">
|
|
<span>i18n:govoplan-admin.valid.b374b8f9 {formatDateTime(license.valid_from)} to {formatDateTime(license.valid_until)}</span>
|
|
<span>i18n:govoplan-admin.features.5df81ffa {license.features.length ? license.features.join(", ") : "none"}</span>
|
|
{license.required_features.length > 0 && <span>i18n:govoplan-admin.required.d5793988 {license.required_features.join(", ")}</span>}
|
|
{license.missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {license.missing_features.join(", ")}</span>}
|
|
</div>
|
|
{license.reason && <p className={`module-package-catalog-description${license.allowed ? "" : " alert warning"}`}>{license.reason}</p>}
|
|
{license.guidance.map((item) => <p key={item} className="module-package-catalog-description">{item}</p>)}
|
|
</div>);
|
|
|
|
}
|
|
|
|
function providedInterfacesLabel(item: ModulePackageCatalogItem): string {
|
|
return item.provides_interfaces.map((provided) => `${provided.name}@${provided.version}`).join(", ");
|
|
}
|
|
|
|
function requiredInterfacesLabel(item: ModulePackageCatalogItem): string {
|
|
return item.requires_interfaces.map((required) => {
|
|
const range = [
|
|
required.version_min ? `>=${required.version_min}` : "",
|
|
required.version_max_exclusive ? `<${required.version_max_exclusive}` : "",
|
|
].filter(Boolean).join(" ");
|
|
const suffix = range ? ` ${range}` : "";
|
|
return `${required.optional ? "optional " : ""}${required.name}${suffix}`;
|
|
}).join(", ");
|
|
}
|
|
|
|
function migrationSafetyLabel(value: ModulePackageCatalogItem["migration_safety"] | ModuleInstallTargetItem["migration_safety"]): string {
|
|
if (value === "requires_review") return "i18n:govoplan-admin.requires_review.8ba0b8c6";
|
|
if (value === "forward_only") return "i18n:govoplan-admin.forward_only.6c107a46";
|
|
if (value === "destructive") return "i18n:govoplan-admin.destructive.0051026c";
|
|
return "i18n:govoplan-admin.automatic.d9a7260f";
|
|
}
|
|
|
|
function targetStatus(target: ModuleInstallTargetItem): "done" | "warning" | "blocked" {
|
|
if (target.migration_safety === "destructive" && !target.data_safety_acknowledged) return "blocked";
|
|
if (target.migration_safety === "forward_only" && !target.data_safety_acknowledged) return "blocked";
|
|
if (target.migration_safety !== "automatic") return "warning";
|
|
return "done";
|
|
}
|
|
|
|
function targetVersionLabel(target: ModuleInstallTargetItem): string {
|
|
const current = target.current_version ?? "none";
|
|
const next = target.action === "uninstall" ? "removed" : target.target_version ?? "unknown";
|
|
return `${current} -> ${next}`;
|
|
}
|
|
|
|
function migrationPhaseLabel(value: ModuleMigrationPlanStep["phase"]): string {
|
|
if (value === "retirement") return "i18n:govoplan-admin.retirement.6b680dd2";
|
|
return "i18n:govoplan-admin.upgrade.12c5007d";
|
|
}
|
|
|
|
function migrationTaskPhaseLabel(value: ModuleMigrationTaskPlanItem["phase"]): string {
|
|
if (value === "pre_migration_check") return "i18n:govoplan-admin.pre_migration_check.f654164a";
|
|
if (value === "pre_migration_prepare") return "i18n:govoplan-admin.pre_migration_prepare.d18fdd26";
|
|
if (value === "post_migration_backfill") return "i18n:govoplan-admin.post_migration_backfill.21043d74";
|
|
return "i18n:govoplan-admin.post_migration_verify.6af9f28b";
|
|
}
|
|
|
|
function migrationSourceLabel(value: ModuleMigrationPlanStep["source"] | ModuleMigrationTaskPlanItem["source"]): string {
|
|
if (value === "catalog") return "i18n:govoplan-admin.catalog_metadata.9b96c41a";
|
|
if (value === "pending") return "i18n:govoplan-admin.pending_metadata.a24cfeb0";
|
|
return "i18n:govoplan-admin.manifest_metadata.b9ae362a";
|
|
}
|
|
|
|
function migrationStepStatus(step: ModuleMigrationPlanStep): "done" | "pending" | "warning" | "blocked" {
|
|
if (step.metadata_pending) return "warning";
|
|
if (step.migration_safety !== "automatic") return "warning";
|
|
return "pending";
|
|
}
|
|
|
|
function migrationTaskStatus(task: ModuleMigrationTaskPlanItem): "done" | "pending" | "warning" | "blocked" {
|
|
if (!task.idempotent) return "blocked";
|
|
if (!task.metadata_pending && !task.has_executor) return "blocked";
|
|
if (task.safety === "forward_only" || task.safety === "destructive") return "warning";
|
|
if (task.metadata_pending || task.safety === "requires_review") return "warning";
|
|
return "pending";
|
|
}
|
|
|
|
function currentWindowLabel(item: Pick<ModulePackageCatalogItem, "current_version_min" | "current_version_max_exclusive">): string {
|
|
return [
|
|
item.current_version_min ? `>=${item.current_version_min}` : "",
|
|
item.current_version_max_exclusive ? `<${item.current_version_max_exclusive}` : "",
|
|
].filter(Boolean).join(" ") || "any";
|
|
}
|
|
|
|
function ModuleStatus({ module, desiredEnabled }: {module: ModuleCatalogItem;desiredEnabled: boolean;}) {
|
|
if (module.current_enabled && !desiredEnabled) return <StatusBadge status="warning" label="i18n:govoplan-admin.will_disable.14bb193a" />;
|
|
if (!module.current_enabled && desiredEnabled) return <StatusBadge status="warning" label="i18n:govoplan-admin.will_enable.5d52d70b" />;
|
|
if (module.current_enabled) return <StatusBadge status="success" label="i18n:govoplan-admin.active.a733b809" />;
|
|
return <StatusBadge status="inactive" label="i18n:govoplan-admin.inactive.09af574c" />;
|
|
}
|
|
|
|
function catalogPlanButtonLabel(item: ModulePackageCatalogItem, catalog: ModuleCatalogResponse | null): string {
|
|
if (item.action === "update") return "i18n:govoplan-admin.plan_update.86e6857a";
|
|
const installed = catalog?.modules.some((module) => module.id === item.module_id && module.installed) ?? false;
|
|
return installed ? "i18n:govoplan-admin.plan_update.86e6857a" : "i18n:govoplan-admin.plan_install.e82bffe6";
|
|
}
|
|
|
|
function sameSet(left: ReadonlySet<string>, right: ReadonlySet<string>) {
|
|
if (left.size !== right.size) return false;
|
|
for (const item of left) {
|
|
if (!right.has(item)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function emptyPlanItem(): ModuleInstallPlanItem {
|
|
return {
|
|
module_id: "",
|
|
action: "install",
|
|
source: "manual",
|
|
catalog: null,
|
|
python_package: "",
|
|
python_ref: "",
|
|
webui_package: "",
|
|
webui_ref: "",
|
|
data_safety_acknowledged: false,
|
|
destroy_data: false,
|
|
status: "planned",
|
|
notes: ""
|
|
};
|
|
}
|
|
|
|
function normalizePlanItems(items: ModuleInstallPlanItem[]): ModuleInstallPlanItem[] {
|
|
return items.
|
|
map((item) => ({
|
|
module_id: item.module_id.trim(),
|
|
action: item.action,
|
|
source: item.source === "catalog" ? "catalog" : "manual",
|
|
catalog: item.source === "catalog" ? item.catalog ?? null : null,
|
|
python_package: cleanOptional(item.python_package),
|
|
python_ref: item.action !== "uninstall" ? cleanOptional(item.python_ref) : null,
|
|
webui_package: cleanOptional(item.webui_package),
|
|
webui_ref: item.action !== "uninstall" ? cleanOptional(item.webui_ref) : null,
|
|
data_safety_acknowledged: item.action !== "uninstall" && Boolean(item.data_safety_acknowledged),
|
|
destroy_data: item.action === "uninstall" && Boolean(item.destroy_data),
|
|
status: item.status,
|
|
notes: cleanOptional(item.notes)
|
|
})).
|
|
filter((item) => item.module_id || item.python_package || item.python_ref || item.webui_package || item.webui_ref || item.notes);
|
|
}
|
|
|
|
function cleanOptional(value?: string | null): string | null {
|
|
const cleaned = (value ?? "").trim();
|
|
return cleaned || null;
|
|
}
|
|
|
|
function planValidationError(items: ModuleInstallPlanItem[]): string {
|
|
for (const item of normalizePlanItems(items)) {
|
|
if (!item.module_id) return "i18n:govoplan-admin.every_plan_item_needs_a_module_id.23a7f206";
|
|
if (item.action !== "uninstall" && !item.python_ref) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_reference.950b898e", { value0: item.module_id });
|
|
if (item.action !== "uninstall" && item.python_ref && !item.python_package) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_name_for_rollback.6c89ed75", { value0: item.module_id });
|
|
if (item.action === "uninstall" && !item.python_package) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_name_for_uninstall.004ba6fa", { value0: item.module_id });
|
|
if (item.action !== "uninstall" && Boolean(item.webui_package) !== Boolean(item.webui_ref)) return i18nMessage("i18n:govoplan-admin.value_needs_both_webui_package_and_webui_referen.d2bab073", { value0: item.module_id });
|
|
if (item.action === "uninstall" && item.webui_ref && !item.webui_package) return i18nMessage("i18n:govoplan-admin.value_has_a_webui_reference_but_no_webui_package.18f44149", { value0: item.module_id });
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function statusTone(status: string): string {
|
|
const normalized = status.toLowerCase();
|
|
if (normalized.includes("fail") || normalized.includes("blocked")) return "error";
|
|
if (normalized.includes("rollback") || normalized.includes("running") || normalized.includes("dry")) return "warning";
|
|
if (normalized.includes("applied") || normalized.includes("ok") || normalized.includes("unlock")) return "success";
|
|
return "inactive";
|
|
}
|
|
|
|
function licenseTone(license: ModuleLicenseDiagnostics): string {
|
|
if (!license.valid || !license.allowed || license.missing_features.length > 0) return "error";
|
|
if (!license.configured || !license.trusted || license.expires_in_days !== null && license.expires_in_days !== undefined && license.expires_in_days <= 30) return "warning";
|
|
return "success";
|
|
}
|
|
|
|
function licenseLabel(license: ModuleLicenseDiagnostics): string {
|
|
if (!license.configured) return "i18n:govoplan-admin.no_license.eb4bfe19";
|
|
if (!license.valid) return "i18n:govoplan-admin.invalid_license.f16bf749";
|
|
if (license.missing_features.length > 0) return "i18n:govoplan-admin.missing_entitlement.0cafa611";
|
|
if (license.trusted) return "i18n:govoplan-admin.valid_and_trusted.73cf89bc";
|
|
if (license.signed) return "i18n:govoplan-admin.signed_untrusted.864ed50c";
|
|
return "i18n:govoplan-admin.unsigned.e91344ea";
|
|
}
|
|
|
|
function formatDateTime(value?: string | null): string {
|
|
if (!value) return "i18n:govoplan-admin.not_recorded.9925ee3c";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return value;
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
function traceId(value?: Record<string, unknown> | null): string {
|
|
const correlationId = value?.correlation_id;
|
|
return typeof correlationId === "string" && correlationId.trim() ? correlationId : "";
|
|
}
|
|
|
|
function installerCommand(frontendRebuildRequired: boolean): string {
|
|
const buildFlag = frontendRebuildRequired ? " --build-webui" : "";
|
|
return [
|
|
"govoplan-module-installer --format shell",
|
|
`govoplan-module-installer --daemon --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`,
|
|
`govoplan-module-installer --enqueue-supervised --migrate${buildFlag} --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`].
|
|
join("\n");
|
|
}
|
|
|
|
type ModuleStateRequestState = {
|
|
request: ConfigurationChangeRequest;
|
|
payloadKey: string;
|
|
};
|
|
|
|
function moduleStatePayloadKey(enabledModules: string[]): string {
|
|
return JSON.stringify(enabledModules);
|
|
}
|
|
|
|
type InstallerRequestFormOptions = {
|
|
buildWebui: boolean;
|
|
migrateDatabase: boolean;
|
|
healthUrls: string;
|
|
restartCommands: string;
|
|
databaseBackupCommand: string;
|
|
databaseRestoreCommand: string;
|
|
databaseRestoreCheckCommand: string;
|
|
activateInstalledModules: boolean;
|
|
removeUninstalledModulesFromDesired: boolean;
|
|
};
|
|
|
|
function defaultInstallerRequestOptions(): InstallerRequestFormOptions {
|
|
return {
|
|
buildWebui: false,
|
|
migrateDatabase: true,
|
|
healthUrls: "http://127.0.0.1:8000/health",
|
|
restartCommands: "",
|
|
databaseBackupCommand: "",
|
|
databaseRestoreCommand: "",
|
|
databaseRestoreCheckCommand: "",
|
|
activateInstalledModules: true,
|
|
removeUninstalledModulesFromDesired: true
|
|
};
|
|
}
|
|
|
|
function normalizeRequestOptions(form: InstallerRequestFormOptions, installPlan: ModuleInstallPlanResponse): ModuleInstallerRequestOptions {
|
|
return {
|
|
build_webui: form.buildWebui || Boolean(installPlan.preflight?.frontend_rebuild_required),
|
|
migrate_database: form.migrateDatabase,
|
|
restart_commands: splitLines(form.restartCommands),
|
|
health_urls: splitLines(form.healthUrls),
|
|
database_backup_command: cleanOptional(form.databaseBackupCommand),
|
|
database_restore_command: cleanOptional(form.databaseRestoreCommand),
|
|
database_restore_check_command: cleanOptional(form.databaseRestoreCheckCommand),
|
|
activate_installed_modules: form.activateInstalledModules,
|
|
remove_uninstalled_modules_from_desired: form.removeUninstalledModulesFromDesired,
|
|
health_timeout_seconds: 60,
|
|
health_interval_seconds: 2
|
|
};
|
|
}
|
|
|
|
function splitLines(value: string): string[] {
|
|
return value.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
}
|