diff --git a/.gitignore b/.gitignore index 5fd4061..2a33867 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,256 @@ webui/.module-test-build/ webui/.policy-test-build/ webui/.template-preview-test-build/ webui/.import-test-build/ + +# GovOPlaN shared ignore rules from govoplan-core +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# Runtime data +pids +*.pid +*.seed +*.pid.lock +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov +# Coverage directory used by tools like istanbul +coverage +*.lcov +# nyc test coverage +.nyc_output +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt +# Bower dependency directory (https://bower.io/) +bower_components +# node-waf configuration +.lock-wscript +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release +# Dependency directories +jspm_packages/ +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ +# TypeScript cache +# Optional npm cache directory +.npm +# Optional eslint cache +.eslintcache +# Optional stylelint cache +.stylelintcache +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ +# Optional REPL history +.node_repl_history +# Output of 'npm pack' +*.tgz +# Yarn Integrity file +.yarn-integrity +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache +# Next.js build output +.next +out +# Nuxt.js build / generate output +.nuxt +dist +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public +# vuepress build output +.vuepress/dist +# vuepress v2.x temp and cache directory +.temp +.cache +# vitepress build output +**/.vitepress/dist +# vitepress cache directory +**/.vitepress/cache +# Docusaurus cache and generated files +.docusaurus +# Serverless directories +.serverless/ +# FuseBox cache +.fusebox/ +# DynamoDB Local files +.dynamodb/ +# TernJS port file +.tern-port +# Stores VSCode versions used for testing VSCode extensions +.vscode-test +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* +# Local WebUI test/build scratch directories +# ---> Python +# Byte-compiled / optimized / DLL files +*$py.class +# C extensions +*.so +# Distribution / packaging +.Python +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +.installed.cfg +*.egg +MANIFEST +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +cover/ +# Translations +*.mo +*.pot +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +# Flask stuff: +instance/ +.webassets-cache +# Scrapy stuff: +.scrapy +# Sphinx documentation +docs/_build/ +# PyBuilder +.pybuilder/ +target/ +# Jupyter Notebook +.ipynb_checkpoints +# IPython +profile_default/ +ipython_config.py +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ +# Celery stuff +celerybeat-schedule +celerybeat.pid +# SageMath parsed files +*.sage.py +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +# Spyder project settings +.spyderproject +.spyproject +# Rope project settings +.ropeproject +# mkdocs documentation +/site +# mypy +.dmypy.json +dmypy.json +# Pyre type checker +.pyre/ +# pytype static type analyzer +.pytype/ +# Cython debug symbols +cython_debug/ +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +# Ruff stuff: +# PyPI configuration file +.pypirc +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets +# Local History for Visual Studio Code +.history/ +# Built Visual Studio Code Extensions +*.vsix +*.db +# GovOPlaN local runtime state +runtime/ +# GovOPlaN WebUI test output diff --git a/src/govoplan_admin/backend/api/v1/routes.py b/src/govoplan_admin/backend/api/v1/routes.py index b60979b..a23735a 100644 --- a/src/govoplan_admin/backend/api/v1/routes.py +++ b/src/govoplan_admin/backend/api/v1/routes.py @@ -465,6 +465,8 @@ def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str, return ModuleInstallPlanItem( module_id=str(raw_item["module_id"]), action="install", + source="catalog", + catalog=_catalog_plan_metadata(result), python_package=raw_item.get("python_package") if isinstance(raw_item.get("python_package"), str) else None, python_ref=raw_item.get("python_ref") if isinstance(raw_item.get("python_ref"), str) else None, webui_package=raw_item.get("webui_package") if isinstance(raw_item.get("webui_package"), str) else None, @@ -474,6 +476,28 @@ def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str, raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Catalog install entry not found: {module_id}") +def _catalog_plan_metadata(validation: dict[str, object]) -> dict[str, object]: + metadata: dict[str, object] = { + "source": validation.get("source") or validation.get("path"), + "source_type": validation.get("source_type"), + "cache_used": bool(validation.get("cache_used")), + "cache_path": validation.get("cache_path"), + "channel": validation.get("channel"), + "sequence": validation.get("sequence"), + "generated_at": validation.get("generated_at"), + "not_before": validation.get("not_before"), + "expires_at": validation.get("expires_at"), + "signed": bool(validation.get("signed")), + "trusted": bool(validation.get("trusted")), + "key_id": validation.get("key_id"), + } + return { + key: value + for key, value in metadata.items() + if value is not None and value != "" + } + + def _catalog_license_features(raw_item: dict[str, object]) -> list[str]: value = raw_item.get("license_features") if not isinstance(value, list): diff --git a/src/govoplan_admin/backend/api/v1/schemas.py b/src/govoplan_admin/backend/api/v1/schemas.py index 75cbaa6..1665253 100644 --- a/src/govoplan_admin/backend/api/v1/schemas.py +++ b/src/govoplan_admin/backend/api/v1/schemas.py @@ -176,6 +176,8 @@ class ModuleInstallPlanItem(BaseModel): module_id: str = Field(min_length=1, max_length=120) action: Literal["install", "uninstall"] + source: Literal["manual", "catalog"] = "manual" + catalog: dict[str, Any] | None = None python_package: str | None = Field(default=None, max_length=200) python_ref: str | None = Field(default=None, max_length=1000) webui_package: str | None = Field(default=None, max_length=200) @@ -308,6 +310,18 @@ class ModuleInstallerRequestCreateRequest(BaseModel): options: ModuleInstallerRequestOptions = Field(default_factory=ModuleInstallerRequestOptions) +class ModuleInterfaceProviderItem(BaseModel): + name: str + version: str + + +class ModuleInterfaceRequirementItem(BaseModel): + name: str + version_min: str | None = None + version_max_exclusive: str | None = None + optional: bool = False + + class ModulePackageCatalogItem(BaseModel): module_id: str name: str @@ -318,6 +332,8 @@ class ModulePackageCatalogItem(BaseModel): python_ref: str | None = None webui_package: str | None = None webui_ref: str | None = None + provides_interfaces: list[ModuleInterfaceProviderItem] = Field(default_factory=list) + requires_interfaces: list[ModuleInterfaceRequirementItem] = Field(default_factory=list) license_features: list[str] = Field(default_factory=list) license_allowed: bool = True license_enforced: bool = False diff --git a/webui/src/api/admin.ts b/webui/src/api/admin.ts index 61e6f96..36e588a 100644 --- a/webui/src/api/admin.ts +++ b/webui/src/api/admin.ts @@ -250,6 +250,8 @@ export type ModuleCatalogResponse = { export type ModuleInstallPlanItem = { module_id: string; action: "install" | "uninstall"; + source: "manual" | "catalog"; + catalog?: Record | null; python_package?: string | null; python_ref?: string | null; webui_package?: string | null; @@ -370,6 +372,18 @@ export type ModuleInstallerRequestListResponse = { full?: boolean; }; +export type ModuleInterfaceProviderItem = { + name: string; + version: string; +}; + +export type ModuleInterfaceRequirementItem = { + name: string; + version_min?: string | null; + version_max_exclusive?: string | null; + optional: boolean; +}; + export type ModulePackageCatalogItem = { module_id: string; name: string; @@ -380,6 +394,8 @@ export type ModulePackageCatalogItem = { python_ref?: string | null; webui_package?: string | null; webui_ref?: string | null; + provides_interfaces: ModuleInterfaceProviderItem[]; + requires_interfaces: ModuleInterfaceRequirementItem[]; license_features: string[]; license_allowed: boolean; license_enforced: boolean; diff --git a/webui/src/features/admin/ModuleManagementPanel.tsx b/webui/src/features/admin/ModuleManagementPanel.tsx index 73df015..8503eff 100644 --- a/webui/src/features/admin/ModuleManagementPanel.tsx +++ b/webui/src/features/admin/ModuleManagementPanel.tsx @@ -416,6 +416,8 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai {item.tags.length > 0 && {item.tags.join(", ")}} {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.license_reason &&

{item.license_reason}

} @@ -471,6 +473,12 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai {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 && } +
@@ -621,6 +629,21 @@ function LicenseStatus({ license }: {license: ModuleLicenseDiagnostics;}) { } +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 ModuleStatus({ module, desiredEnabled }: {module: ModuleCatalogItem;desiredEnabled: boolean;}) { if (module.current_enabled && !desiredEnabled) return ; if (!module.current_enabled && desiredEnabled) return ; @@ -640,6 +663,8 @@ function emptyPlanItem(): ModuleInstallPlanItem { return { module_id: "", action: "install", + source: "manual", + catalog: null, python_package: "", python_ref: "", webui_package: "", @@ -655,6 +680,8 @@ function normalizePlanItems(items: ModuleInstallPlanItem[]): ModuleInstallPlanIt 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 === "install" ? cleanOptional(item.python_ref) : null, webui_package: cleanOptional(item.webui_package), diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 4c8627f..a08300d 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -44,6 +44,7 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.case_clerk.b78a314a": "Case clerk", "i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f": "Catalog install entry planned.", "i18n:govoplan-admin.catalog.4a88d27b": "Catalog", + "i18n:govoplan-admin.manual.17a486ca": "Manual", "i18n:govoplan-admin.central_groups.5c9b5b66": "Central groups", "i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9": "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local.", "i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1": "Centrally governed tenant roles and their availability across tenants.", @@ -232,6 +233,7 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b": "Preflight finished with blockers.", "i18n:govoplan-admin.preflight_passed.c0c99055": "Preflight passed.", "i18n:govoplan-admin.preflight.8016a487": "Preflight", + "i18n:govoplan-admin.provides.221b70d9": "Provides:", "i18n:govoplan-admin.python_package.34591c71": "Python package", "i18n:govoplan-admin.python_ref.1af35d0d": "Python ref", "i18n:govoplan-admin.queue_supervised_run.a53f248c": "Queue supervised run", @@ -397,6 +399,7 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.case_clerk.b78a314a": "Case clerk", "i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f": "Catalog install entry planned.", "i18n:govoplan-admin.catalog.4a88d27b": "Katalog", + "i18n:govoplan-admin.manual.17a486ca": "Manuell", "i18n:govoplan-admin.central_groups.5c9b5b66": "Central groups", "i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9": "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local.", "i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1": "Centrally governed tenant roles and their availability across tenants.", @@ -585,6 +588,7 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b": "Preflight finished with blockers.", "i18n:govoplan-admin.preflight_passed.c0c99055": "Preflight passed.", "i18n:govoplan-admin.preflight.8016a487": "Preflight", + "i18n:govoplan-admin.provides.221b70d9": "Provides:", "i18n:govoplan-admin.python_package.34591c71": "Python-Paket", "i18n:govoplan-admin.python_ref.1af35d0d": "Python-Referenz", "i18n:govoplan-admin.queue_supervised_run.a53f248c": "Queue supervised run",