From d428f3390a73fe6a3838efab1974fb275c25da37 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 3 Aug 2026 11:30:01 +0200 Subject: [PATCH] Complete Admin interface patterns --- README.md | 4 + docs/INTERFACE_PATTERN_MIGRATION.md | 41 ++++++++ src/govoplan_admin/backend/manifest.py | 27 +++++- .../test_interface_documentation_contract.py | 53 +++++++++++ webui/package.json | 3 +- .../test-interface-pattern-language.mjs | 39 ++++++++ .../src/features/admin/AdminOverviewPanel.tsx | 15 +-- .../admin/ConfigurationChangesPanel.tsx | 33 +++++-- .../admin/ConfigurationPackagesPanel.tsx | 48 ++++++---- .../admin/GovernanceTemplatesPanel.tsx | 11 ++- .../features/admin/ModuleManagementPanel.tsx | 93 +++++++++++++++---- .../features/admin/SystemSettingsPanel.tsx | 22 +++-- webui/src/features/admin/interfacePatterns.ts | 77 +++++++++++++++ webui/src/i18n/generatedTranslations.ts | 78 ++++++++++++++++ webui/src/module.ts | 16 ++-- 15 files changed, 485 insertions(+), 75 deletions(-) create mode 100644 docs/INTERFACE_PATTERN_MIGRATION.md create mode 100644 tests/test_interface_documentation_contract.py create mode 100644 webui/scripts/test-interface-pattern-language.mjs create mode 100644 webui/src/features/admin/interfacePatterns.ts diff --git a/README.md b/README.md index c1d445d..2eb0e4e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # GovOPlaN Admin +The Admin-owned workspace sections, lifecycle stages, consequence classes, +contextual-help contract, and verification evidence are recorded in +[docs/INTERFACE_PATTERN_MIGRATION.md](docs/INTERFACE_PATTERN_MIGRATION.md). + **Repository type:** module (platform). diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..c883e4b --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,41 @@ +# Admin interface pattern migration + +This document records the interface-pattern coverage for the surfaces +contributed by `govoplan-admin`. The Access module hosts the administration +tree; Admin supplies only its declared sections through `admin.sections`. + +## Surface inventory + +| Surface | Archetype | Authority and state model | +| --- | --- | --- | +| Administration overview | Navigation dashboard | Counts are server projections. Links exist only for sections already admitted by capabilities, permissions, and the active View. | +| System settings | Adaptive configuration | A local draft is compared with the saved server state. Reload preserves an unsaved draft, Save requires system write authority, and maintenance controls also require maintenance authority. | +| Configuration changes | Governed work queue and evidence list | Open approval requests and immutable applied history are separate server-authoritative grids. Approval requires an explicit confirmation. | +| Configuration packages | Guided preflight/apply/export workspace | JSON input is validated locally, dry-run evidence is shown separately, approval is optional where policy allows it, and application is explicitly confirmed. Reference selectors replace raw IDs unless historical manual mode is selected deliberately. | +| Role and group templates | Governed definition directory | Definitions, tenant availability, and role permissions are edited in one draft. Deletion is confirmed and remains blocked by materialized dependencies in the backend. | +| Module management | Guided lifecycle workflow and operations evidence | Desired runtime state, package plan, preflight, maintenance gate, daemon request, and durable run evidence are distinct stages. Disabled controls name the earliest blocker. | + +## Consequence classes + +- Overview navigation, reload, filtering, inspecting evidence, dry runs, and + exports are reversible. +- Settings, templates, desired module state, package plans, and approval + requests are governed mutations with permission and validation reasons. +- Approving a change, applying a configuration package, enabling maintenance, + clearing a saved module plan, cancelling an installer request, and deleting + a governance template require shared confirmation surfaces. +- Actual package mutation remains outside the API process and is performed by + the supervised installer. Run and rollback evidence remains durable. + +## Shared controls and verification + +Admin uses Core `AdminPageLayout`, `DataGrid`, `ReferenceSelect`, `StageRail`, +`Dialog`, `ConfirmDialog`, `TableActionGroup`, `ActionBlockerHint`, +`DocumentationHelpLink`, `ToggleSwitch`, and status/alert primitives. This +inherits Core focus restoration, keyboard order, responsive overflow, +disabled-action tooltips, and accessible dialog semantics. + +The focused WebUI check rejects browser-native confirmation calls, private +sibling-module imports, untranslated Admin-owned structural headings, missing +contextual help, and missing disabled reasons on consequential controls. The +manifest regression test fixes the help-context and surface declaration. diff --git a/src/govoplan_admin/backend/manifest.py b/src/govoplan_admin/backend/manifest.py index 83e2cc6..8beef04 100644 --- a/src/govoplan_admin/backend/manifest.py +++ b/src/govoplan_admin/backend/manifest.py @@ -28,9 +28,16 @@ manifest = ModuleManifest( 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.", 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.", - documentation_types=("user",), + documentation_types=("admin", "user"), audience=("tenant_admin", "system_admin", "operator"), - metadata={"kind": "reference"}, + metadata={ + "kind": "reference", + "help_contexts": [ + "admin.workspace", + "admin.overview", + "admin.section-navigation", + ], + }, ), DocumentationTopic( id="admin.governance-and-module-lifecycle", @@ -40,7 +47,15 @@ manifest = ModuleManifest( documentation_types=("admin",), audience=("system_admin", "operator", "module_admin"), related_modules=("access", "audit", "ops"), - metadata={"kind": "reference"}, + metadata={ + "kind": "reference", + "help_contexts": [ + "admin.system-settings", + "admin.configuration-changes", + "admin.configuration-packages", + "admin.governance-templates", + ], + }, ), DocumentationTopic( id="admin.module-lifecycle-workflow", @@ -61,6 +76,12 @@ manifest = ModuleManifest( "admin.module-lifecycle", "admin.module-lifecycle.queue-blocker", ], + "help_contexts": [ + "admin.module-lifecycle", + "admin.module-lifecycle.queue-blocker", + "admin.module-lifecycle.plan", + "admin.module-lifecycle.evidence", + ], }, ), ), diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py new file mode 100644 index 0000000..7e9d20d --- /dev/null +++ b/tests/test_interface_documentation_contract.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import unittest + +from govoplan_admin.backend.manifest import manifest + + +class InterfaceDocumentationContractTests(unittest.TestCase): + def test_admin_topics_publish_stable_help_contexts(self) -> None: + topics = {topic.id: topic for topic in manifest.documentation} + expected = { + "admin.workspace": {"admin.workspace", "admin.overview"}, + "admin.governance-and-module-lifecycle": { + "admin.system-settings", + "admin.configuration-changes", + "admin.configuration-packages", + "admin.governance-templates", + }, + "admin.module-lifecycle-workflow": { + "admin.module-lifecycle", + "admin.module-lifecycle.queue-blocker", + "admin.module-lifecycle.plan", + "admin.module-lifecycle.evidence", + }, + } + for topic_id, contexts in expected.items(): + self.assertIn(topic_id, topics) + metadata = topics[topic_id].metadata or {} + self.assertTrue( + contexts.issubset(set(metadata.get("help_contexts", ()))), + topic_id, + ) + + def test_admin_contributed_surfaces_remain_declared(self) -> None: + surface_ids = { + surface.id for surface in manifest.frontend.view_surfaces + } + self.assertEqual( + surface_ids, + { + "admin.section.overview", + "admin.section.system-settings", + "admin.section.system-configuration-changes", + "admin.section.system-configuration-packages", + "admin.section.system-role-templates", + "admin.section.system-groups", + "admin.section.system-modules", + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/package.json b/webui/package.json index 77a7ab0..e470dd7 100644 --- a/webui/package.json +++ b/webui/package.json @@ -13,7 +13,8 @@ } }, "scripts": { - "test:installer-workflow": "node --experimental-strip-types --test tests/module-installer-workflow.test.ts" + "test:installer-workflow": "node --experimental-strip-types --test tests/module-installer-workflow.test.ts", + "test:interface-patterns": "node scripts/test-interface-pattern-language.mjs" }, "peerDependencies": { "@govoplan/core-webui": "^0.1.9", diff --git a/webui/scripts/test-interface-pattern-language.mjs b/webui/scripts/test-interface-pattern-language.mjs new file mode 100644 index 0000000..b7f0fa9 --- /dev/null +++ b/webui/scripts/test-interface-pattern-language.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const read = (path) => readFileSync(resolve(root, path), "utf8"); + +const overview = read("src/features/admin/AdminOverviewPanel.tsx"); +const settings = read("src/features/admin/SystemSettingsPanel.tsx"); +const changes = read("src/features/admin/ConfigurationChangesPanel.tsx"); +const packages = read("src/features/admin/ConfigurationPackagesPanel.tsx"); +const templates = read("src/features/admin/GovernanceTemplatesPanel.tsx"); +const modules = read("src/features/admin/ModuleManagementPanel.tsx"); +const moduleSource = read("src/module.ts"); +const allSource = [overview, settings, changes, packages, templates, modules].join("\n"); + +for (const source of [overview, settings, changes, packages, templates, modules]) { + assert.match(source, /AdminPageLayout/); + assert.match(source, /DocumentationHelpLink/); + assert.match(source, /disabledReason|help=/); +} + +for (const source of [changes, packages, templates, modules]) { + assert.match(source, /ConfirmDialog/); +} + +assert.match(changes, /DataGrid/); +assert.match(packages, /ReferenceSelect/); +assert.match(packages, /manualReferences/); +assert.match(templates, /TableActionGroup/); +assert.match(modules, /StageRail/); +assert.match(modules, /ActionBlockerHint/); +assert.match(moduleSource, /version: "0\.1\.8"/); + +assert.doesNotMatch(overview, /title="(?:ADMINISTRATION|GLOBAL|TENANT|GROUP|USER)"/); +assert.doesNotMatch(allSource, /window\.(alert|confirm|prompt)\s*\(/); +assert.doesNotMatch(allSource, /@govoplan\/(?!core-webui)[^"']+-webui\//); + +console.log("Admin interface pattern-language checks passed."); diff --git a/webui/src/features/admin/AdminOverviewPanel.tsx b/webui/src/features/admin/AdminOverviewPanel.tsx index 6c81c8c..876829a 100644 --- a/webui/src/features/admin/AdminOverviewPanel.tsx +++ b/webui/src/features/admin/AdminOverviewPanel.tsx @@ -3,7 +3,8 @@ import type { ApiSettings } from "@govoplan/core-webui"; import { fetchAdminOverview, type AdminOverview } from "../../api/admin"; import { Card, MetricCard } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui"; -import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui"; +import { AdminPageLayout, DocumentationHelpLink, adminErrorMessage } from "@govoplan/core-webui"; +import { ADMIN_INTERFACE_I18N, ADMIN_WORKSPACE_DOCUMENTATION } from "./interfacePatterns"; export default function AdminOverviewPanel({ settings, onSelect, availableSections }: {settings: ApiSettings;onSelect: (section: string) => void;availableSections: ReadonlySet;}) { const [overview, setOverview] = useState(null); @@ -28,7 +29,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio some((value) => value !== null && value !== undefined)); return ( - void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553}> + }> {overview && <> {hasSystemMetrics && <>
i18n:govoplan-admin.system.bc0792d8
@@ -40,7 +41,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio } - {hasAnySection(availableSections, platformSectionIds) && + {hasAnySection(availableSections, platformSectionIds) &&
{availableSections.has("system-modules") && onSelect("system-modules")} />} {availableSections.has("system-configuration-packages") && onSelect("system-configuration-packages")} />} @@ -50,7 +51,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
} - {hasAnySection(availableSections, globalSectionIds) && + {hasAnySection(availableSections, globalSectionIds) &&
{availableSections.has("system-tenants") && onSelect("system-tenants")} />} {availableSections.has("system-roles") && onSelect("system-roles")} />} @@ -71,7 +72,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
- {hasAnySection(availableSections, tenantSectionIds) && + {hasAnySection(availableSections, tenantSectionIds) &&
{availableSections.has("tenant-roles") && onSelect("tenant-roles")} />} {availableSections.has("tenant-groups") && onSelect("tenant-groups")} />} @@ -85,7 +86,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
} - {hasAnySection(availableSections, groupSectionIds) && + {hasAnySection(availableSections, groupSectionIds) &&
{availableSections.has("tenant-group-file-connectors") && onSelect("tenant-group-file-connectors")} />} {availableSections.has("tenant-group-mail-servers") && onSelect("tenant-group-mail-servers")} />} @@ -93,7 +94,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
} - {hasAnySection(availableSections, userSectionIds) && + {hasAnySection(availableSections, userSectionIds) &&
{availableSections.has("tenant-user-file-connectors") && onSelect("tenant-user-file-connectors")} />} {availableSections.has("tenant-user-mail-servers") && onSelect("tenant-user-mail-servers")} />} diff --git a/webui/src/features/admin/ConfigurationChangesPanel.tsx b/webui/src/features/admin/ConfigurationChangesPanel.tsx index 3656ee9..d6f786c 100644 --- a/webui/src/features/admin/ConfigurationChangesPanel.tsx +++ b/webui/src/features/admin/ConfigurationChangesPanel.tsx @@ -1,13 +1,14 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Check, RefreshCw } from "lucide-react"; import type { ApiSettings } from "@govoplan/core-webui"; -import { AdminPageLayout, Button, Card, DataGrid, StatusBadge, TableActionGroup, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks, type DataGridColumn } from "@govoplan/core-webui"; +import { AdminPageLayout, Button, Card, ConfirmDialog, DataGrid, DocumentationHelpLink, StatusBadge, TableActionGroup, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks, type DataGridColumn } from "@govoplan/core-webui"; import { approveConfigurationChangeRequest, fetchConfigurationChangesDelta, type ConfigurationChangeRecord, type ConfigurationChangeRequest } from "../../api/admin"; +import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N } from "./interfacePatterns"; const DELTA_KEY = "admin:configuration-changes"; @@ -19,6 +20,7 @@ export default function ConfigurationChangesPanel({ settings, canApprove }: {set const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const [loading, setLoading] = useState(true); const [busyId, setBusyId] = useState(""); + const [approving, setApproving] = useState(null); const [error, setError] = useState(""); const [message, setMessage] = useState(""); @@ -68,6 +70,7 @@ export default function ConfigurationChangesPanel({ settings, canApprove }: {set setError(adminErrorMessage(err)); } finally { setBusyId(""); + setApproving(null); } } @@ -98,9 +101,16 @@ export default function ConfigurationChangesPanel({ settings, canApprove }: {set id: "approve", label: "i18n:govoplan-admin.approve.7b2c7f14", icon: