5 Commits
Author SHA1 Message Date
zemion a9b840f66d fix(ui): anchor help to headings and document grouped navigation
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:30 +02:00
zemion f401b134e3 Release govoplan-admin v0.1.23: unify interface contracts and documentation
Module Package Release / publish-packages (push) Successful in 12s
2026-09-08 01:32:23 +02:00
zemion 3b45064069 fix(webui): classify consequential administration controls
Module Package Release / publish-packages (push) Successful in 12s
2026-08-24 11:36:32 +02:00
zemion c71ef013f1 docs: complete German structured documentation
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 01:15:31 +02:00
zemion 28e6dac289 docs(admin): complete German reference coverage
Module Package Release / publish-packages (push) Successful in 12s
2026-08-23 20:11:46 +02:00
16 changed files with 228 additions and 40 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/admin-webui", "name": "@govoplan/admin-webui",
"version": "0.1.19", "version": "0.1.23",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "webui/src/index.ts", "main": "webui/src/index.ts",
@@ -18,7 +18,7 @@
"LICENSE" "LICENSE"
], ],
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.35", "@govoplan/core-webui": "^0.1.45",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": ">=19.2.7 <20", "react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20",
+2 -2
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-admin" name = "govoplan-admin"
version = "0.1.19" version = "0.1.23"
description = "GovOPlaN generic administration module." description = "GovOPlaN generic administration module."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = [ dependencies = [
"govoplan-core>=0.1.35", "govoplan-core>=0.1.45",
"govoplan-access>=0.1.20", "govoplan-access>=0.1.20",
] ]
+1 -6
View File
@@ -88,7 +88,6 @@ from govoplan_core.core.module_installer_notifications import (
) )
from govoplan_core.core.module_license import module_license_decision, module_license_diagnostics from govoplan_core.core.module_license import module_license_decision, module_license_diagnostics
from govoplan_core.core.navigation import ( from govoplan_core.core.navigation import (
NavigationPreferences,
navigation_preferences_from_settings, navigation_preferences_from_settings,
update_navigation_preferences, update_navigation_preferences,
) )
@@ -1885,11 +1884,7 @@ def write_system_settings(
if "navigation" in payload.model_fields_set: if "navigation" in payload.model_fields_set:
item.settings = update_navigation_preferences( item.settings = update_navigation_preferences(
item.settings, item.settings,
NavigationPreferences( payload.navigation.model_dump(mode="json") if payload.navigation is not None else None,
order=tuple(payload.navigation.order),
hidden=tuple(payload.navigation.hidden),
locked=tuple(payload.navigation.locked),
) if payload.navigation is not None else None,
) )
if payload.privacy_retention_policy is not None: if payload.privacy_retention_policy is not None:
set_privacy_policy(item, payload.privacy_retention_policy.model_dump(mode="json")) set_privacy_policy(item, payload.privacy_retention_policy.model_dump(mode="json"))
@@ -0,0 +1,31 @@
"""German translations for public structured documentation metadata."""
from __future__ import annotations
from typing import Any
GERMAN_STRUCTURED_TRANSLATIONS: dict[str, dict[str, Any]] = {'admin.governance-and-module-lifecycle': {'limitations': ['Konfigurationspakete installieren '
'keine Module oder gewähren '
'Berechtigungen.',
'Generisches Rollback ist Snapshot '
'Recovery; Anbieter können eine engere '
'Vergütung separat aussetzen.',
'Geheimwerte werden niemals in '
'Exportprovenienz oder tragbare '
'Fragmente aufgenommen.'],
'operational_consequences': ['Das Ändern von '
'Paketeingaben ungültig '
'macht den vorherigen '
'Preflight und erfordert '
'eine weitere Überprüfung.',
'Eine teilweise Anwendung '
'stoppt vor späteren '
'Fragmenten und muss vor '
'dem erneuten Versuch '
'wiederhergestellt werden.',
'Bewahren Sie den '
'Pre-Apply-Datenbank-Snapshot '
'auf, bis die Überprüfung '
'nach der Anwendung '
'abgeschlossen ist.']}}
+144 -2
View File
@@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
from govoplan_core.core.modules import with_documentation_structured_translations
from govoplan_admin.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS
from govoplan_admin.backend.db import models as admin_models # noqa: F401 - populate Admin ORM metadata from govoplan_admin.backend.db import models as admin_models # noqa: F401 - populate Admin ORM metadata
from govoplan_core.core.access import ( from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -78,7 +81,7 @@ ADMIN_ROLE_TEMPLATES = (
manifest = ModuleManifest( manifest = ModuleManifest(
id="admin", id="admin",
name="Admin", name="Admin",
version="0.1.19", version="0.1.23",
permissions=ADMIN_PERMISSIONS, permissions=ADMIN_PERMISSIONS,
role_templates=ADMIN_ROLE_TEMPLATES, role_templates=ADMIN_ROLE_TEMPLATES,
required_capabilities=( required_capabilities=(
@@ -123,6 +126,24 @@ manifest = ModuleManifest(
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("privacy_officer", "system_admin", "auditor"), audience=("privacy_officer", "system_admin", "auditor"),
related_modules=("core", "access", "audit", "ops"), related_modules=("core", "access", "audit", "ops"),
translations={
"de": {
"title": "Abdeckung von Betroffenenanfragen in der Administration",
"summary": (
"Die von Admin verwalteten Governance-Vorlagen enthalten keine Felder mit Betroffenen- oder "
"Betriebsidentitäten."
),
"body": (
"Das Admin-Modul speichert wiederverwendbare Definitionen von Governance-Vorlagen und deren Verfügbarkeit für "
"Mandanten. Diese Tabellen enthalten Berechtigungen, Vorlagenbezeichnungen und Mandantenkennungen, aber keine "
"Konten, Mitgliedschaften, Identitäten, E-Mail-Adressen, Autorinnen, Freigebenden oder andere Betroffenenverweise. "
"Admin stellt deshalb einen ausdrücklichen Provider mit leerem Ergebnis bereit, damit der Datenschutz-Arbeitsbereich "
"eine geprüfte Nichtanwendbarkeit von einer ungeklärten Abdeckungslücke unterscheiden kann. Akteursnachweise zu "
"Modul-Lebenszyklus und Konfigurationsänderungen gehören weiterhin Core, Audit, Access oder Ops und werden von deren "
"Providern geliefert."
),
}
},
metadata={ metadata={
"help_contexts": ["admin.privacy.data-subject-requests"], "help_contexts": ["admin.privacy.data-subject-requests"],
"coverage_classification": "reviewed_no_subject_data", "coverage_classification": "reviewed_no_subject_data",
@@ -132,9 +153,35 @@ manifest = ModuleManifest(
id="admin.workspace", id="admin.workspace",
title="Use the administration workspace", title="Use the administration workspace",
summary="The administration workspace shows only the sections supplied by enabled modules and allowed by the current account's permissions.", summary="The administration workspace shows only the sections supplied by enabled modules and allowed by the current account's permissions.",
body="System and tenant administration share one workspace. Available sections can include settings, configuration changes and packages, governance templates, groups, and module lifecycle controls. A missing section normally means that its owning module is disabled or the current account lacks the required authority. System appearance settings select a validated palette default. Changing the separate palette lock additionally requires policy-write authority; a system lock suppresses tenant and personal palette choices, while an unlocked default remains inheritable and overridable. The separate advanced-override policy is disabled by default and also requires policy-write authority. Enabling it permits tenants to inherit or narrow access to Core's versioned, accessibility-validated personal accent, surface, and status editor; palette locks continue to win.", body="The documentation book belongs beside each administration heading, including module and "
"language management; operational actions remain in the action row. "
"System and tenant administration share one workspace. Available sections can include settings, configuration changes and packages, governance templates, groups, and module lifecycle controls. A missing section normally means that its owning module is disabled or the current account lacks the required authority. System appearance settings select a validated palette default. Changing the separate palette lock additionally requires policy-write authority; a system lock suppresses tenant and personal palette choices, while an unlocked default remains inheritable and overridable. The separate advanced-override policy is disabled by default and also requires policy-write authority. Enabling it permits tenants to inherit or narrow access to Core's versioned, accessibility-validated personal accent, surface, and status editor; palette locks continue to win. Opening lists and creation or edit dialogs does not save changes. If an authorized section reports a load failure, reload after deployment and report the frontend diagnostic; do not broaden permissions or recreate records to work around a render failure.",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("tenant_admin", "system_admin", "operator"), audience=("tenant_admin", "system_admin", "operator"),
translations={
"de": {
"title": "Administrationsarbeitsbereich verwenden",
"summary": (
"Der Administrationsarbeitsbereich zeigt nur Bereiche, die aktivierte Module bereitstellen und für die das aktuelle "
"Konto berechtigt ist."
),
"body": (
"Das Dokumentationsbuch steht neben der jeweiligen Verwaltungsüberschrift, auch bei Modul- "
"und Sprachverwaltung; ausführbare Aktionen bleiben in der Aktionszeile. "
"Das Öffnen von Listen sowie Anlage- oder Bearbeitungsdialogen speichert keine Änderungen. Meldet ein berechtigter Bereich einen Ladefehler, laden Sie nach der Bereitstellung neu und melden Sie die Oberflächendiagnose; erweitern Sie keine Berechtigungen und legen Sie keine Datensätze erneut an, um einen Darstellungsfehler zu umgehen. "
"System- und Mandantenadministration teilen sich einen Arbeitsbereich. Verfügbare Bereiche können Einstellungen, "
"Konfigurationsänderungen und -pakete, Governance-Vorlagen, Gruppen und die Steuerung des Modul-Lebenszyklus umfassen. "
"Fehlt ein Bereich, ist normalerweise sein zuständiges Modul deaktiviert oder das aktuelle Konto besitzt nicht die "
"erforderliche Berechtigung. Die Systemeinstellungen für das Erscheinungsbild wählen eine validierte Standardpalette. "
"Das Ändern der getrennten Palettensperre erfordert zusätzlich die Berechtigung zum Schreiben von Richtlinien; eine "
"Systemsperre unterdrückt Mandanten- und persönliche Paletten, ein ungesperrter Standard bleibt vererbbar und "
"überschreibbar. Die getrennte Richtlinie für erweiterte Anpassungen ist standardmäßig deaktiviert und verlangt ebenfalls "
"die Richtlinienschreibberechtigung. Wird sie aktiviert, können Mandanten den Zugriff auf Cores versionierten und auf "
"Barrierefreiheit geprüften Editor für persönliche Akzent-, Flächen- und Statusfarben erben oder einschränken; "
"Palettensperren behalten Vorrang."
),
}
},
metadata={ metadata={
"kind": "reference", "kind": "reference",
"help_contexts": [ "help_contexts": [
@@ -155,6 +202,27 @@ manifest = ModuleManifest(
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin", "module_admin", "tenant_admin"), audience=("system_admin", "module_admin", "tenant_admin"),
related_modules=("access", "policy", "views", "encryption"), related_modules=("access", "policy", "views", "encryption"),
translations={
"de": {
"title": "Module je Mandant steuern",
"summary": (
"Systemadministrierende legen Modulobergrenzen und erzwungene Module je Mandant fest; "
"Mandanten-Moduladministrierende wählen innerhalb dieser Grenzen."
),
"body": (
"Die Aktivierung im Deployment installiert und lädt Modulcode für die gesamte Instanz. Die Modulsteuerung pro Mandant "
"ist eine getrennte Berechtigungsebene: Systemadministrierende markieren Module für einen Mandanten als nicht verfügbar, "
"verfügbar oder erzwungen und können auch dessen Auswahl ändern. Mandanten-Moduladministrierende dürfen nur verfügbare "
"Module aktivieren oder deaktivieren; erzwungene Module und erforderliche Abhängigkeiten bleiben wirksam. Eine "
"Modulberechtigung gewährt niemals Zugriffsrechte, und fehlerhafte Richtlinien schließen geschützte Administrationsmodule "
"sicher aus. Das Deaktivieren eines Moduls stoppt neue API-, Fähigkeits-, Zeitplan- und Worker-Aufnahmen für den Mandanten. "
"Bereits angenommene dauerhafte Arbeit bleibt in der Warteschlange und erfordert eine Betriebsentscheidung, statt "
"ausgeführt oder verworfen zu werden. Die Aktivierung eines Fähigkeitsmoduls wie Encryption stellt nur dessen Dienste "
"bereit; die Verschlüsselung von Daten bleibt eine ausdrückliche Richtlinien- oder Migrationsentscheidung des "
"zuständigen Moduls."
),
}
},
metadata={ metadata={
"kind": "workflow", "kind": "workflow",
"help_contexts": [ "help_contexts": [
@@ -228,6 +296,27 @@ manifest = ModuleManifest(
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin", "tenant_admin"), audience=("system_admin", "tenant_admin"),
related_modules=("tenancy", "access", "docs"), related_modules=("tenancy", "access", "docs"),
translations={
"de": {
"title": "Referenzsprache und Sprachpakete verwalten",
"summary": (
"Deutsch ist Referenzsprache und Standard für Neuinstallationen, während konfigurierte Sprachwahlen von Mandanten und "
"Benutzenden wirksam bleiben."
),
"body": (
"Deutsch ist die maßgebliche Sprache für die Produktabnahme und Standard für ein neues System oder einen neuen Mandanten. "
"Englisch bleibt als Rückfall für den Quellcode installiert. Der eigene Administrationsbereich für Sprachpakete trennt "
"Installation, Aktivierung, Deaktivierung, Deinstallierbarkeit und den Standard für neu angelegte Mandanten von den "
"allgemeinen Systemeinstellungen. Die Kompatibilitätsmatrix leitet den Zustand jedes Pakets aus installierter Modulkennung, "
"exakter Modulversion und vorhandenem Laufzeit-Übersetzungskatalog ab; ein fehlender Katalog wird als inkompatibel angezeigt "
"und nicht als übersetzt behandelt. Deutsch, Englisch und der aktive Standard können nicht deinstalliert werden. "
"Systemadministrierende steuern verfügbare und aktivierte Sprachpakete sowie den Systemstandard; Mandanten und Benutzende "
"können nur Sprachen wählen, die von den übergeordneten Ebenen erlaubt sind. Bereits ausdrücklich gespeicherte Präferenzen "
"bleiben bei Einführung dieses Standards erhalten. Das Deaktivieren einer Sprache darf weder übersetzte Inhalte verwerfen "
"noch eine gespeicherte Präferenz stillschweigend umschreiben."
),
}
},
metadata={ metadata={
"kind": "reference", "kind": "reference",
"route": "/admin?section=system-language-packages", "route": "/admin?section=system-language-packages",
@@ -244,10 +333,31 @@ manifest = ModuleManifest(
summary="System administrators define the instance-wide side-rail order, visibility baseline, and entries that lower scopes must keep visible.", summary="System administrators define the instance-wide side-rail order, visibility baseline, and entries that lower scopes must keep visible.",
body=( body=(
"The system navigation setting starts from module-declared defaults. Tenant settings take precedence over the system order and visibility, and personal settings take precedence over both. A system lock prevents tenant and user preferences from hiding an entry but does not prevent them from moving it. Removing the system preference restores module defaults. Navigation policy changes presentation only: module entitlement, View policy, and permissions still decide whether a surface is accessible." "The system navigation setting starts from module-declared defaults. Tenant settings take precedence over the system order and visibility, and personal settings take precedence over both. A system lock prevents tenant and user preferences from hiding an entry but does not prevent them from moving it. Removing the system preference restores module defaults. Navigation policy changes presentation only: module entitlement, View policy, and permissions still decide whether a surface is accessible."
" Use the shared layout editor to drag modules and separators, add or restore available modules, remove entries from the rail, and label groups. Keyboard handles support Space, arrows, Enter, and Escape; arrow buttons also move individual entries. Collapsed rails retain group dividers. Changes stay in the page draft until Save, and Use inherited layout restores live module defaults rather than copying them. Removing a navigation entry never uninstalls a module or deletes its records."
" Standard grouping needs no saved system configuration: Work; Services and cases; Records and documents; Communication; Meetings and decisions; Data and assurance; People and responsibility. Empty or unauthorized groups stay absent. Expanded rails show headings without divider lines; collapsed rails replace headings with horizontal group boundaries. The editor distinguishes inheritance, custom grouping and an explicitly flat layout. A deliberately flat layout is not silently reset. Use inherited layout and Save removes only the system override; it does not erase tenant or personal preferences. Personal over tenant over system over standard remains the baseline, and higher-level visibility locks remain enforced."
), ),
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin",), audience=("system_admin",),
related_modules=("tenancy", "access", "views"), related_modules=("tenancy", "access", "views"),
translations={
"de": {
"title": "Systemweite Navigationsgrundlage festlegen",
"summary": (
"Systemadministrierende definieren die instanzweite Reihenfolge und Sichtbarkeitsgrundlage der Seitenleiste sowie "
"Einträge, die untergeordnete Ebenen sichtbar halten müssen."
),
"body": (
"Die systemweite Navigation beginnt mit den von den Modulen deklarierten Standardwerten. Mandanteneinstellungen haben "
"bei Reihenfolge und Sichtbarkeit Vorrang vor dem System, persönliche Einstellungen vor beiden. Eine Systemsperre "
"verhindert, dass Mandanten- oder Benutzereinstellungen einen Eintrag ausblenden, erlaubt aber weiterhin dessen "
"Verschiebung. Das Entfernen der Systempräferenz stellt die Modulstandardwerte wieder her. Navigationsrichtlinien ändern "
"nur die Darstellung; Modulberechtigung, View-Richtlinie und Zugriffsrechte bestimmen weiterhin, ob eine Oberfläche "
"erreichbar ist."
" Im gemeinsamen Anordnungseditor ziehen Sie Module und Trennlinien, ergänzen verfügbare Module, entfernen Einträge aus der Leiste und benennen Gruppen. Tastaturgriffe unterstützen Leertaste, Pfeile, Eingabe und Escape; Pfeilschaltflächen verschieben einzelne Einträge ebenfalls. Eingeklappte Leisten behalten Gruppentrenner. Änderungen bleiben bis zum Speichern im Seitenentwurf. Geerbte Anordnung verwenden stellt die aktuellen Modulstandardwerte wieder her, statt sie zu kopieren. Das Entfernen eines Navigationseintrags deinstalliert kein Modul und löscht keine Datensätze."
" Die Standardgruppierung benötigt keine gespeicherte Systemkonfiguration: Arbeit; Leistungen und Vorgänge; Akten und Dokumente; Kommunikation; Termine und Entscheidungen; Daten und Qualitätssicherung; Personen und Verantwortung. Leere oder nicht berechtigte Gruppen bleiben verborgen. Ausgeklappte Leisten zeigen Überschriften ohne Trennlinien; eingeklappt werden daraus horizontale Gruppengrenzen. Der Editor unterscheidet Vererbung, eigene Gruppierung und eine ausdrücklich ungegliederte Anordnung. Eine bewusst ungegliederte Anordnung wird nicht stillschweigend zurückgesetzt. Geerbte Anordnung verwenden und Speichern entfernt nur die Systemanpassung, nicht Mandanten- oder persönliche Einstellungen. Die Rangfolge persönlich vor Mandant vor System vor Standard bleibt erhalten; übergeordnete Sichtbarkeitssperren bleiben wirksam."
),
}
},
metadata={ metadata={
"kind": "reference", "kind": "reference",
"help_contexts": ["admin.system-settings"], "help_contexts": ["admin.system-settings"],
@@ -267,6 +377,33 @@ manifest = ModuleManifest(
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin", "operator", "module_admin"), audience=("system_admin", "operator", "module_admin"),
related_modules=("ops", "audit"), related_modules=("ops", "audit"),
translations={
"de": {
"title": "Änderungen am Modul-Lebenszyklus planen und überwachen",
"summary": (
"Einen geprüften Modulpaketplan durch Vorprüfung, wartungsgeschützte Einreihung, Daemon-Ausführung und dauerhafte "
"Laufnachweise führen."
),
"body": (
"Der Administrationsbereich Module bildet einen Betriebsablauf ab: Paketplan speichern, Befunde der Vorprüfung beheben, "
"mit der erforderlichen Berechtigung in den Wartungsmodus wechseln, einen überwachten Installer-Auftrag einreihen und "
"den zugehörigen Laufdatensatz prüfen. Ist kein deploymentspezifischer Katalog konfiguriert, ermittelt das "
"Paketverzeichnis den signierten öffentlichen stabilen GovOPlaN-Katalog. Verfügbare, installierte, aktualisierbare, "
"blockierte und zurückgezogene Einträge lassen sich suchen und filtern. Jede Zeile zeigt signierte Quellrevision, "
"Artefakt-Digest, Versionshinweise, Konfigurationsanforderungen und die vom Manifest deklarierten Berechtigungen des "
"Katalogs. Diese Offenlegung unterstützt die Prüfung, gewährt aber keine Berechtigung. Fehlende Abhängigkeiten, "
"inkompatible benannte Schnittstellen, nicht unterstützte Aktualisierungsfenster und zurückgezogene Releases blockieren "
"die Planerstellung. Die Auswahl eines geeigneten Eintrags kopiert seine exakten signierten Registry-Kennungen in den Plan; "
"Download und Digest-Prüfung erfolgen ausschließlich im vertrauenswürdigen Installer. Die Stufenanzeige wird aus "
"Speicherzeitpunkt des Plans, neuestem passendem Auftrag und dessen Lauf abgeleitet; ein älterer Auftrag gilt nie als "
"Nachweis für einen neueren Plan. Deaktivierte Einreihungsaktionen nennen den frühesten Blocker, die zuständige Person und "
"die Planoberfläche für die Fortsetzung. Paketänderungen bleiben außerhalb des FastAPI-Prozesses, "
"Wiederherstellungsnachweise dauerhaft im Installer-Ledger. Gemeinsame Deployments benötigen ein unveränderliches "
"Image-Rollout statt lokaler Änderungen je Knoten. Mandantenberechtigung und Benutzer-/View-Sichtbarkeit bleiben "
"Richtlinieneinstellungen und sind keine Paket-Lebenszyklusaktionen."
),
}
},
metadata={ metadata={
"kind": "workflow", "kind": "workflow",
"context_ids": [ "context_ids": [
@@ -434,5 +571,10 @@ manifest = ModuleManifest(
) )
manifest = with_documentation_structured_translations(
manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS
)
def get_manifest() -> ModuleManifest: def get_manifest() -> ModuleManifest:
return manifest return manifest
+19 -5
View File
@@ -6,6 +6,18 @@ from govoplan_admin.backend.manifest import manifest
class InterfaceDocumentationContractTests(unittest.TestCase): class InterfaceDocumentationContractTests(unittest.TestCase):
def test_all_static_topics_have_complete_german_content(self) -> None:
for topic in manifest.documentation:
german = (topic.translations or {}).get("de", {})
self.assertEqual(
{"title", "summary", "body"},
set(german),
topic.id,
)
self.assertTrue(
all(str(value).strip() for value in german.values()), topic.id
)
def test_admin_topics_publish_stable_help_contexts(self) -> None: def test_admin_topics_publish_stable_help_contexts(self) -> None:
topics = {topic.id: topic for topic in manifest.documentation} topics = {topic.id: topic for topic in manifest.documentation}
expected = { expected = {
@@ -40,9 +52,7 @@ class InterfaceDocumentationContractTests(unittest.TestCase):
) )
def test_admin_contributed_surfaces_remain_declared(self) -> None: def test_admin_contributed_surfaces_remain_declared(self) -> None:
surface_ids = { surface_ids = {surface.id for surface in manifest.frontend.view_surfaces}
surface.id for surface in manifest.frontend.view_surfaces
}
self.assertEqual( self.assertEqual(
surface_ids, surface_ids,
{ {
@@ -86,10 +96,14 @@ class InterfaceDocumentationContractTests(unittest.TestCase):
def test_module_administrator_has_only_tenant_module_permissions(self) -> None: def test_module_administrator_has_only_tenant_module_permissions(self) -> None:
permissions = {item.scope for item in manifest.permissions} permissions = {item.scope for item in manifest.permissions}
template = next(item for item in manifest.role_templates if item.slug == "module_admin") template = next(
item for item in manifest.role_templates if item.slug == "module_admin"
)
self.assertEqual({"admin:module:read", "admin:module:write"}, permissions) self.assertEqual({"admin:module:read", "admin:module:write"}, permissions)
self.assertEqual(tuple(sorted(permissions)), tuple(sorted(template.permissions))) self.assertEqual(
tuple(sorted(permissions)), tuple(sorted(template.permissions))
)
if __name__ == "__main__": if __name__ == "__main__":
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/admin-webui", "name": "@govoplan/admin-webui",
"version": "0.1.19", "version": "0.1.23",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -17,7 +17,7 @@
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs" "test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.35", "@govoplan/core-webui": "^0.1.45",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": ">=19.2.7 <20", "react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20",
@@ -30,7 +30,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
some((value) => value !== null && value !== undefined)); some((value) => value !== null && value !== undefined));
return ( return (
<AdminPageLayout title="i18n:govoplan-admin.administration.b8be3d12" description="i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499" loading={loading} error={error} actions={<><DocumentationHelpLink reference={ADMIN_WORKSPACE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : undefined}>i18n:govoplan-admin.reload.cce71553</Button></>}> <AdminPageLayout title="i18n:govoplan-admin.administration.b8be3d12" titleHelp={<DocumentationHelpLink reference={ADMIN_WORKSPACE_DOCUMENTATION} />} description="i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499" loading={loading} error={error} actions={<><Button onClick={() => void load()} disabled={loading} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : undefined}>i18n:govoplan-admin.reload.cce71553</Button></>}>
{overview && <> {overview && <>
{hasSystemMetrics && <> {hasSystemMetrics && <>
<div className="admin-overview-section-label">i18n:govoplan-admin.system.bc0792d8</div> <div className="admin-overview-section-label">i18n:govoplan-admin.system.bc0792d8</div>
@@ -63,7 +63,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
{availableSections.has("system-users") && <AreaLink title="i18n:govoplan-admin.users.57f2b181" text="i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f" onClick={() => onSelect("system-users")} />} {availableSections.has("system-users") && <AreaLink title="i18n:govoplan-admin.users.57f2b181" text="i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f" onClick={() => onSelect("system-users")} />}
{availableSections.has("system-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("system-file-connectors")} />} {availableSections.has("system-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("system-file-connectors")} />}
{availableSections.has("system-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("system-mail-servers")} />} {availableSections.has("system-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("system-mail-servers")} />}
{availableSections.has("system-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069" onClick={() => onSelect("system-retention")} />} {availableSections.has("system-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069" helpRiskReviewed="standard" onClick={() => onSelect("system-retention")} />}
{availableSections.has("system-view-policy") && <AreaLink title="View policy" text="Limit View actions, definitions, and surfaces across the instance." onClick={() => onSelect("system-view-policy")} />} {availableSections.has("system-view-policy") && <AreaLink title="View policy" text="Limit View actions, definitions, and surfaces across the instance." onClick={() => onSelect("system-view-policy")} />}
</div> </div>
</Card>} </Card>}
@@ -84,7 +84,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
{availableSections.has("tenant-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("tenant-file-connectors")} />} {availableSections.has("tenant-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("tenant-file-connectors")} />}
{availableSections.has("tenant-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("tenant-mail-servers")} />} {availableSections.has("tenant-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("tenant-mail-servers")} />}
{availableSections.has("tenant-api-keys") && <AreaLink title="i18n:govoplan-admin.api_keys.94fcf3c2" text="i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54" onClick={() => onSelect("tenant-api-keys")} />} {availableSections.has("tenant-api-keys") && <AreaLink title="i18n:govoplan-admin.api_keys.94fcf3c2" text="i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54" onClick={() => onSelect("tenant-api-keys")} />}
{availableSections.has("tenant-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d" onClick={() => onSelect("tenant-retention")} />} {availableSections.has("tenant-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d" helpRiskReviewed="standard" onClick={() => onSelect("tenant-retention")} />}
{availableSections.has("tenant-modules") && <AreaLink title="Modules" text="Choose modules made available to this tenant by system policy." onClick={() => onSelect("tenant-modules")} />} {availableSections.has("tenant-modules") && <AreaLink title="Modules" text="Choose modules made available to this tenant by system policy." onClick={() => onSelect("tenant-modules")} />}
{availableSections.has("tenant-view-policy") && <AreaLink title="View policy" text="Narrow inherited View actions and available surfaces for this tenant." onClick={() => onSelect("tenant-view-policy")} />} {availableSections.has("tenant-view-policy") && <AreaLink title="View policy" text="Narrow inherited View actions and available surfaces for this tenant." onClick={() => onSelect("tenant-view-policy")} />}
{availableSections.has("tenant-settings") && <AreaLink title="i18n:govoplan-admin.general.9239ee2c" text="i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b" onClick={() => onSelect("tenant-settings")} />} {availableSections.has("tenant-settings") && <AreaLink title="i18n:govoplan-admin.general.9239ee2c" text="i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b" onClick={() => onSelect("tenant-settings")} />}
@@ -96,7 +96,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
<div className="admin-overview-grid"> <div className="admin-overview-grid">
{availableSections.has("tenant-group-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.group_file_connector_policy_limits" onClick={() => onSelect("tenant-group-file-connectors")} />} {availableSections.has("tenant-group-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.group_file_connector_policy_limits" onClick={() => onSelect("tenant-group-file-connectors")} />}
{availableSections.has("tenant-group-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.group_mail_server_policy_limits" onClick={() => onSelect("tenant-group-mail-servers")} />} {availableSections.has("tenant-group-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.group_mail_server_policy_limits" onClick={() => onSelect("tenant-group-mail-servers")} />}
{availableSections.has("tenant-group-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.group_retention_policy_limits" onClick={() => onSelect("tenant-group-retention")} />} {availableSections.has("tenant-group-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.group_retention_policy_limits" helpRiskReviewed="standard" onClick={() => onSelect("tenant-group-retention")} />}
{availableSections.has("group-view-policy") && <AreaLink title="View policy" text="Set View limits for a selected group." onClick={() => onSelect("group-view-policy")} />} {availableSections.has("group-view-policy") && <AreaLink title="View policy" text="Set View limits for a selected group." onClick={() => onSelect("group-view-policy")} />}
</div> </div>
</Card>} </Card>}
@@ -105,7 +105,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
<div className="admin-overview-grid"> <div className="admin-overview-grid">
{availableSections.has("tenant-user-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.user_file_connector_policy_limits" onClick={() => onSelect("tenant-user-file-connectors")} />} {availableSections.has("tenant-user-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.user_file_connector_policy_limits" onClick={() => onSelect("tenant-user-file-connectors")} />}
{availableSections.has("tenant-user-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.user_mail_server_policy_limits" onClick={() => onSelect("tenant-user-mail-servers")} />} {availableSections.has("tenant-user-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.user_mail_server_policy_limits" onClick={() => onSelect("tenant-user-mail-servers")} />}
{availableSections.has("tenant-user-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.user_retention_policy_limits" onClick={() => onSelect("tenant-user-retention")} />} {availableSections.has("tenant-user-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.user_retention_policy_limits" helpRiskReviewed="standard" onClick={() => onSelect("tenant-user-retention")} />}
{availableSections.has("user-view-policy") && <AreaLink title="View policy" text="Set View limits for a selected user." onClick={() => onSelect("user-view-policy")} />} {availableSections.has("user-view-policy") && <AreaLink title="View policy" text="Set View limits for a selected user." onClick={() => onSelect("user-view-policy")} />}
</div> </div>
</Card>} </Card>}
@@ -124,6 +124,6 @@ function hasAnySection(sections: ReadonlySet<string>, candidates: readonly strin
return candidates.some((section) => sections.has(section)); return candidates.some((section) => sections.has(section));
} }
function AreaLink({ title, text, onClick }: {title: string;text: string;onClick: () => void;}) { function AreaLink({ title, text, helpRiskReviewed, onClick }: {title: string;text: string;helpRiskReviewed?: "standard";onClick: () => void;}) {
return <button className="admin-overview-link" onClick={onClick}><strong>{title}</strong><span>{text}</span></button>; return <button className="admin-overview-link" data-help-risk-reviewed={helpRiskReviewed} onClick={onClick}><strong>{title}</strong><span>{text}</span></button>;
} }
@@ -128,11 +128,12 @@ export default function ConfigurationChangesPanel({ settings, canApprove }: {set
<> <>
<AdminPageLayout <AdminPageLayout
title="i18n:govoplan-admin.configuration_changes.82933bbb" title="i18n:govoplan-admin.configuration_changes.82933bbb"
titleHelp={<DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} />}
description="i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509" description="i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509"
loading={loading} loading={loading}
error={error} error={error}
success={message} success={message}
actions={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || Boolean(busyId)} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busyId ? ADMIN_INTERFACE_I18N.busy : undefined}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button></>}> actions={<><Button onClick={() => void load()} disabled={loading || Boolean(busyId)} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busyId ? ADMIN_INTERFACE_I18N.busy : undefined}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button></>}>
<Card title="i18n:govoplan-admin.requests.f7194e6a"> <Card title="i18n:govoplan-admin.requests.f7194e6a">
<DataGrid <DataGrid
@@ -280,11 +280,12 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
<> <>
<AdminPageLayout <AdminPageLayout
title="i18n:govoplan-admin.configuration_packages.eb2f05f1" title="i18n:govoplan-admin.configuration_packages.eb2f05f1"
titleHelp={<DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} />}
description="i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929" description="i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929"
loading={loading} loading={loading}
error={error || parsedPackage.error || parsedSuppliedData.error || ""} error={error || parsedPackage.error || parsedSuppliedData.error || ""}
success={message} success={message}
actions={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void loadCatalog()} disabled={loading || Boolean(busy)} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button></>}> actions={<><Button onClick={() => void loadCatalog()} disabled={loading || Boolean(busy)} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button></>}>
<Card title="i18n:govoplan-admin.catalog.4a88d27b"> <Card title="i18n:govoplan-admin.catalog.4a88d27b">
<div className="button-row compact-actions"> <div className="button-row compact-actions">
@@ -1,8 +1,8 @@
import { DescriptionItem, DescriptionList } from "@govoplan/core-webui"; import { DescriptionItem, DescriptionList, FormGrid } from "@govoplan/core-webui";
import { MetricGrid } from "@govoplan/core-webui"; import { MetricGrid } from "@govoplan/core-webui";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Download, Eye, Play, Plus, RefreshCw, Search, ShieldCheck } from "lucide-react"; import { Download, Eye, Play, Plus, RefreshCw, Search, ShieldCheck } from "lucide-react";
import type { FormGrid, ApiSettings, DataGridColumn } from "@govoplan/core-webui"; import type { ApiSettings, DataGridColumn } from "@govoplan/core-webui";
import { import {
AdminIconButton, AdminIconButton,
AdminPageLayout, AdminPageLayout,
@@ -1,7 +1,7 @@
import { DescriptionItem, DescriptionList } from "@govoplan/core-webui"; import { DescriptionItem, DescriptionList, FormGrid } from "@govoplan/core-webui";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Search, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; import { Search, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
import type { FormGrid, ApiSettings } from "@govoplan/core-webui"; import type { ApiSettings } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui";
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
@@ -225,11 +225,12 @@ export default function GovernanceTemplatesPanel({
<> <>
<AdminPageLayout <AdminPageLayout
title={title} title={title}
titleHelp={<DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} />}
description={description} description={description}
loading={loading} loading={loading}
error={error} error={error}
success={success} success={success}
actions={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><AdminIconButton label={kind === "group" ? "i18n:govoplan-admin.add_group_template.b74d8f0f" : "i18n:govoplan-admin.add_tenant_role.fcd904ea"} icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canWrite} disabledReason={!canWrite ? ADMIN_INTERFACE_I18N.governanceWriteRequired : undefined} /></>}> actions={<><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><AdminIconButton label={kind === "group" ? "i18n:govoplan-admin.add_group_template.b74d8f0f" : "i18n:govoplan-admin.add_tenant_role.fcd904ea"} icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canWrite} disabledReason={!canWrite ? ADMIN_INTERFACE_I18N.governanceWriteRequired : undefined} /></>}>
<div className="admin-table-surface"> <div className="admin-table-surface">
<DataGrid id={`admin-system-${kind}-templates-v3`} rows={items} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText={i18nMessage("i18n:govoplan-admin.no_central_value_templates_found.081149fa", { value0: kind })} /> <DataGrid id={`admin-system-${kind}-templates-v3`} rows={items} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText={i18nMessage("i18n:govoplan-admin.no_central_value_templates_found.081149fa", { value0: kind })} />
@@ -192,12 +192,12 @@ export default function LanguagePackagesPanel({
return ( return (
<AdminPageLayout <AdminPageLayout
title="i18n:govoplan-admin.language_package_administration.lp001" title="i18n:govoplan-admin.language_package_administration.lp001"
titleHelp={<DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} />}
description="i18n:govoplan-admin.language_package_administration_description.lp001" description="i18n:govoplan-admin.language_package_administration_description.lp001"
loading={loading} loading={loading}
error={error} error={error}
success={success} success={success}
actions={<> actions={<>
<DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} />
<Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button> <Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button>
<Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button> <Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button>
</>} </>}
@@ -1,6 +1,6 @@
import { MetricGrid } from "@govoplan/core-webui"; import { ActionToolbar, MetricGrid } from "@govoplan/core-webui";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import type { ActionToolbar, ApiSettings } from "@govoplan/core-webui"; import type { ApiSettings } 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 { 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 { Check, Clock, FileText, Pencil, Send } from "lucide-react";
import { import {
@@ -401,11 +401,12 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
<> <>
<AdminPageLayout <AdminPageLayout
title="i18n:govoplan-admin.system_modules.629539f2" title="i18n:govoplan-admin.system_modules.629539f2"
titleHelp={<DocumentationHelpLink reference={MODULE_LIFECYCLE_DOCUMENTATION} />}
description="i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd" description="i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd"
loading={loading} loading={loading}
error={error} error={error}
success={success} success={success}
actions={<><DocumentationHelpLink reference={MODULE_LIFECYCLE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={saveDisabled} disabledReason={saveDisabledReason}>{busy ? "i18n:govoplan-admin.working.049ac820" : saveLabel}</Button></>}> actions={<><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={saveDisabled} disabledReason={saveDisabledReason}>{busy ? "i18n:govoplan-admin.working.049ac820" : saveLabel}</Button></>}>
{catalog && <> {catalog && <>
<MetricGrid columns={5} minimum="compact"> <MetricGrid columns={5} minimum="compact">
@@ -695,7 +696,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
<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.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.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> <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"} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : planBusy ? ADMIN_INTERFACE_I18N.busy : item.action !== "uninstall" ? ADMIN_INTERFACE_I18N.uninstallOnly : undefined} /> <ToggleSwitch label="i18n:govoplan-admin.destroy_data.34557c3c" helpContextId="admin.module-lifecycle.plan" helpModuleId="admin" checked={Boolean(item.destroy_data)} onChange={(checked) => updatePlanItem(index, { destroy_data: checked })} disabled={!canWrite || planBusy || item.action !== "uninstall"} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : planBusy ? ADMIN_INTERFACE_I18N.busy : item.action !== "uninstall" ? ADMIN_INTERFACE_I18N.uninstallOnly : undefined} />
<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"} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : planBusy ? ADMIN_INTERFACE_I18N.busy : item.action === "uninstall" ? ADMIN_INTERFACE_I18N.installUpdateOnly : undefined} /> <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"} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : planBusy ? ADMIN_INTERFACE_I18N.busy : item.action === "uninstall" ? ADMIN_INTERFACE_I18N.installUpdateOnly : undefined} />
<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 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><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>
@@ -82,11 +82,12 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
return ( return (
<AdminPageLayout <AdminPageLayout
title="i18n:govoplan-admin.system_general_settings.7cd662ed" title="i18n:govoplan-admin.system_general_settings.7cd662ed"
titleHelp={<DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} />}
description="i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97" description="i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97"
loading={loading} loading={loading}
error={error} error={error}
success={success} success={success}
actions={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}> actions={<><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}>
<div className="admin-settings-form"> <div className="admin-settings-form">
<Card title="i18n:govoplan-admin.tenant_administration_capabilities.5d265972"> <Card title="i18n:govoplan-admin.tenant_administration_capabilities.5d265972">
@@ -100,6 +101,7 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
<Card title="System navigation order"> <Card title="System navigation order">
<NavigationPreferenceEditor <NavigationPreferenceEditor
items={navigationItems} items={navigationItems}
productAreas={modules.flatMap((module) => module.productAreas ?? [])}
value={draft.navigation} value={draft.navigation}
onChange={(navigation) => setDraft({ ...draft, navigation })} onChange={(navigation) => setDraft({ ...draft, navigation })}
scope="system" scope="system"
@@ -211,6 +211,7 @@ export default function TenantModuleManagementPanel({ settings, scope, canWrite
return ( return (
<AdminPageLayout <AdminPageLayout
title={labels.title} title={labels.title}
titleHelp={<DocumentationHelpLink reference={DOCUMENTATION} label="Open module governance documentation" />}
description={labels.description} description={labels.description}
loading={loading} loading={loading}
error={error} error={error}
@@ -231,7 +232,6 @@ export default function TenantModuleManagementPanel({ settings, scope, canWrite
<Button variant="primary" onClick={() => void save()} disabled={!canWrite || !dirty || busy}> <Button variant="primary" onClick={() => void save()} disabled={!canWrite || !dirty || busy}>
<Save size={16} /> {busy ? "Saving..." : "Save"} <Save size={16} /> {busy ? "Saving..." : "Save"}
</Button> </Button>
<DocumentationHelpLink reference={DOCUMENTATION} label="Open module governance documentation" />
</> </>
} }
> >