Expose searchable module catalog compatibility

This commit is contained in:
2026-08-06 22:42:13 +02:00
parent 1e55a80d3c
commit 9d522ef6e0
8 changed files with 501 additions and 20 deletions
+34
View File
@@ -419,6 +419,30 @@ export type ModuleInterfaceRequirementItem = {
optional: boolean;
};
export type ModulePackageCatalogSource = {
repository: string;
tag: string;
commit: string;
repository_url?: string | null;
revision_url?: string | null;
};
export type ModulePackageArtifactIdentity = {
ref?: string | null;
path?: string | null;
artifact_path?: string | null;
url?: string | null;
filename?: string | null;
sha256?: string | null;
size?: number | null;
integrity?: string | null;
sbom_url?: string | null;
provenance_url?: string | null;
registry_identity?: string | null;
git_ref?: string | null;
source_commit?: string | null;
};
export type ModulePackageCatalogItem = {
module_id: string;
name: string;
@@ -428,6 +452,16 @@ export type ModulePackageCatalogItem = {
installed: boolean;
installed_version?: string | null;
update_available: boolean;
availability: "available" | "withdrawn";
availability_reason?: string | null;
configuration_requirements: string[];
release_notes_url?: string | null;
source?: ModulePackageCatalogSource | null;
artifact_integrity: Record<string, ModulePackageArtifactIdentity>;
compatible: boolean;
plan_allowed: boolean;
compatibility_reasons: string[];
catalog_state: "available" | "installed" | "update_available" | "blocked" | "withdrawn";
dependencies: string[];
optional_dependencies: string[];
migration_safety: "automatic" | "requires_review" | "forward_only" | "destructive";
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, 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 { ActionBlockerHint, AdminPageLayout, adminErrorMessage, Button, ConfirmDialog, dispatchPlatformModulesChanged, DocumentationHelpLink, formatDateTime, MetricCard, SegmentedControl, StageRail, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedDraftGuard, type FormatDateTimeOptions } from "@govoplan/core-webui";
import { Check, Clock, FileText, Pencil, Send } from "lucide-react";
import {
cancelModuleInstallerRequest,
@@ -52,6 +52,8 @@ const MODULE_INSTALLER_I18N = {
resolutionTarget: "i18n:govoplan-admin.where_to_go.f1a20205"
} as const;
type CatalogModuleFilter = "all" | "available" | "updates" | "installed" | "blocked";
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);
@@ -71,6 +73,8 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
const [confirmClearPlan, setConfirmClearPlan] = useState(false);
const [confirmMaintenance, setConfirmMaintenance] = useState(false);
const [cancelRequestId, setCancelRequestId] = useState("");
const [catalogSearch, setCatalogSearch] = useState("");
const [catalogFilter, setCatalogFilter] = useState<CatalogModuleFilter>("all");
async function load() {
setLoading(true);
@@ -108,6 +112,28 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
const planDirty = Boolean(installPlan && JSON.stringify(normalizePlanItems(draftPlanItems)) !== JSON.stringify(normalizePlanItems(installPlan.items)));
const planValid = planValidationError(draftPlanItems) === "";
const latestInstallerRequest = installerRequests?.requests[0] ?? null;
const visibleCatalogModules = useMemo(() => {
const query = catalogSearch.trim().toLocaleLowerCase();
return (packageCatalog?.modules ?? []).filter((item) => {
const matchesFilter =
catalogFilter === "all" ||
(catalogFilter === "available" && item.catalog_state === "available") ||
(catalogFilter === "updates" && item.update_available) ||
(catalogFilter === "installed" && item.installed) ||
(catalogFilter === "blocked" && ["blocked", "withdrawn"].includes(item.catalog_state));
if (!matchesFilter) return false;
if (!query) return true;
return [
item.name,
item.module_id,
item.description ?? "",
item.python_package ?? "",
item.webui_package ?? "",
item.source?.repository ?? "",
...item.tags,
].join(" ").toLocaleLowerCase().includes(query);
});
}, [catalogFilter, catalogSearch, packageCatalog]);
const currentInstallerRequest = latestInstallerRequest && installerRequestMatchesPlan(
installPlan?.updated_at,
latestInstallerRequest.created_at
@@ -494,15 +520,39 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
{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-toolbar">
<label className="module-package-catalog-search">
<span>i18n:govoplan-admin.search_module_directory.7c8f1001</span>
<input
type="search"
value={catalogSearch}
onChange={(event) => setCatalogSearch(event.target.value)}
placeholder="i18n:govoplan-admin.module_name_package_or_tag.7c8f1002" />
</label>
<SegmentedControl<CatalogModuleFilter>
ariaLabel="i18n:govoplan-admin.filter_module_directory.7c8f1003"
value={catalogFilter}
onChange={setCatalogFilter}
options={[
{ id: "all", label: "i18n:govoplan-admin.all_catalog_entries.7c8f1004" },
{ id: "available", label: "i18n:govoplan-admin.available.7c8f1005" },
{ id: "updates", label: "i18n:govoplan-admin.updates.7c8f1006" },
{ id: "installed", label: "i18n:govoplan-admin.installed.7bb4405c" },
{ id: "blocked", label: "i18n:govoplan-admin.blocked.7c8f1007" },
]} />
</div>}
{packageCatalog.modules.length > 0 && visibleCatalogModules.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_catalog_entries_match.7c8f1008</div>}
{packageCatalog.modules.length > 0 && <div className="module-package-catalog-list">
{packageCatalog.modules.map((item) =>
{visibleCatalogModules.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>}
{item.installed && <StatusBadge status={item.update_available ? "warning" : "success"} label={item.update_available ? "i18n:govoplan-admin.plan_update.86e6857a" : "i18n:govoplan-admin.installed.7bb4405c"} />}
<StatusBadge status={catalogStateTone(item)} label={catalogStateLabel(item)} />
</div>
<div className="module-management-details">
{item.python_package && <span>{item.python_package}</span>}
@@ -520,13 +570,24 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
{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.source && <div className="module-package-catalog-provenance">
<span>i18n:govoplan-admin.source.7c8f1009 {item.source.revision_url ? <a href={item.source.revision_url} target="_blank" rel="noreferrer"><code>{item.source.repository}@{item.source.tag}</code></a> : <code>{item.source.repository}@{item.source.tag}</code>}</span>
<code title={item.source.commit}>{shortIdentity(item.source.commit)}</code>
</div>}
{Object.entries(item.artifact_integrity).map(([kind, artifact]) => <div key={kind} className="module-package-catalog-provenance">
<span>i18n:govoplan-admin.artifact.7c8f1010 <code>{kind}</code> {artifact.url ? <a href={artifact.url} target="_blank" rel="noreferrer">{artifact.filename ?? artifact.registry_identity ?? artifact.ref ?? kind}</a> : artifact.filename ?? artifact.registry_identity ?? artifact.ref ?? kind}</span>
{artifact.sha256 && <code title={artifact.sha256}>sha256:{shortIdentity(artifact.sha256)}</code>}
</div>)}
{item.description && <p className="module-package-catalog-description">{item.description}</p>}
{item.configuration_requirements.length > 0 && <p className="module-package-catalog-description">i18n:govoplan-admin.configuration_requirements.7c8f1011 {item.configuration_requirements.join(", ")}</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.release_notes_url && <p className="module-package-catalog-description"><a href={item.release_notes_url} target="_blank" rel="noreferrer">i18n:govoplan-admin.release_notes.7c8f1012</a></p>}
{item.compatibility_reasons.map((compatibilityReason) => <p key={compatibilityReason} className="alert warning">{compatibilityReason}</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)} disabledReason={planBusy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !packageCatalog.valid || !item.license_allowed || !["install", "update"].includes(item.action) ? ADMIN_INTERFACE_I18N.catalogBlocked : undefined}>{catalogPlanButtonLabel(item)}</Button>
<Button onClick={() => void addCatalogItem(item)} disabled={!canWrite || planBusy || !packageCatalog.valid || !item.plan_allowed} disabledReason={planBusy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !packageCatalog.valid || !item.plan_allowed ? ADMIN_INTERFACE_I18N.catalogBlocked : undefined}>{catalogPlanButtonLabel(item)}</Button>
</div>
)}
</div>}
@@ -952,6 +1013,25 @@ function currentWindowLabel(item: Pick<ModulePackageCatalogItem, "current_versio
].filter(Boolean).join(" ") || "any";
}
function catalogStateTone(item: ModulePackageCatalogItem): string {
if (item.catalog_state === "withdrawn") return "locked";
if (item.catalog_state === "blocked" || item.catalog_state === "update_available") return "warning";
if (item.catalog_state === "installed") return "success";
return "inactive";
}
function catalogStateLabel(item: ModulePackageCatalogItem): string {
if (item.catalog_state === "withdrawn") return "i18n:govoplan-admin.withdrawn.7c8f1013";
if (item.catalog_state === "blocked") return "i18n:govoplan-admin.blocked.7c8f1007";
if (item.catalog_state === "update_available") return "i18n:govoplan-admin.update_available.7c8f1014";
if (item.catalog_state === "installed") return "i18n:govoplan-admin.installed.7bb4405c";
return "i18n:govoplan-admin.available.7c8f1005";
}
function shortIdentity(value: string): string {
return value.length > 14 ? `${value.slice(0, 12)}...` : value;
}
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" />;
@@ -960,6 +1040,7 @@ function ModuleStatus({ module, desiredEnabled }: {module: ModuleCatalogItem;des
}
function catalogPlanButtonLabel(item: ModulePackageCatalogItem): string {
if (item.installed && !item.plan_allowed) return "i18n:govoplan-admin.installed.7bb4405c";
return item.action === "update" || item.installed
? "i18n:govoplan-admin.plan_update.86e6857a"
: "i18n:govoplan-admin.plan_install.e82bffe6";
+28
View File
@@ -2,6 +2,20 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"en": {
"i18n:govoplan-admin.search_module_directory.7c8f1001": "Search module directory",
"i18n:govoplan-admin.module_name_package_or_tag.7c8f1002": "Module name, package, or tag",
"i18n:govoplan-admin.filter_module_directory.7c8f1003": "Filter module directory",
"i18n:govoplan-admin.all_catalog_entries.7c8f1004": "All",
"i18n:govoplan-admin.available.7c8f1005": "Available",
"i18n:govoplan-admin.updates.7c8f1006": "Updates",
"i18n:govoplan-admin.blocked.7c8f1007": "Blocked",
"i18n:govoplan-admin.no_catalog_entries_match.7c8f1008": "No catalog entries match the current search and filter.",
"i18n:govoplan-admin.source.7c8f1009": "Source:",
"i18n:govoplan-admin.artifact.7c8f1010": "Artifact:",
"i18n:govoplan-admin.configuration_requirements.7c8f1011": "Configuration requirements:",
"i18n:govoplan-admin.release_notes.7c8f1012": "Release notes",
"i18n:govoplan-admin.withdrawn.7c8f1013": "Withdrawn",
"i18n:govoplan-admin.update_available.7c8f1014": "Update available",
"i18n:govoplan-admin.administration_data_is_loading.6bf3c001": "Administration data is loading.",
"i18n:govoplan-admin.an_administration_operation_is_in_progress.6bf3c002": "An administration operation is in progress.",
"i18n:govoplan-admin.system_administration_write_permission_is_required.6bf3c003": "System administration write permission is required.",
@@ -438,6 +452,20 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-admin.working.049ac820": "Working..."
},
"de": {
"i18n:govoplan-admin.search_module_directory.7c8f1001": "Modulverzeichnis durchsuchen",
"i18n:govoplan-admin.module_name_package_or_tag.7c8f1002": "Modulname, Paket oder Schlagwort",
"i18n:govoplan-admin.filter_module_directory.7c8f1003": "Modulverzeichnis filtern",
"i18n:govoplan-admin.all_catalog_entries.7c8f1004": "Alle",
"i18n:govoplan-admin.available.7c8f1005": "Verfügbar",
"i18n:govoplan-admin.updates.7c8f1006": "Aktualisierungen",
"i18n:govoplan-admin.blocked.7c8f1007": "Blockiert",
"i18n:govoplan-admin.no_catalog_entries_match.7c8f1008": "Keine Katalogeinträge entsprechen der aktuellen Suche und dem Filter.",
"i18n:govoplan-admin.source.7c8f1009": "Quelle:",
"i18n:govoplan-admin.artifact.7c8f1010": "Artefakt:",
"i18n:govoplan-admin.configuration_requirements.7c8f1011": "Konfigurationsvoraussetzungen:",
"i18n:govoplan-admin.release_notes.7c8f1012": "Versionshinweise",
"i18n:govoplan-admin.withdrawn.7c8f1013": "Zurückgezogen",
"i18n:govoplan-admin.update_available.7c8f1014": "Aktualisierung verfügbar",
"i18n:govoplan-admin.administration_data_is_loading.6bf3c001": "Administrationsdaten werden geladen.",
"i18n:govoplan-admin.an_administration_operation_is_in_progress.6bf3c002": "Eine Administrationsaktion wird gerade ausgeführt.",
"i18n:govoplan-admin.system_administration_write_permission_is_required.6bf3c003": "Eine Schreibberechtigung für die Systemadministration ist erforderlich.",