From e8ea347654a924fdeee9729752a7dd8b937e04ed Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Thu, 30 Jul 2026 17:42:06 +0200 Subject: [PATCH] feat: organize administration as a settings tree --- webui/src/features/admin/AdminPage.tsx | 132 ++++++++++++++++++++----- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/webui/src/features/admin/AdminPage.tsx b/webui/src/features/admin/AdminPage.tsx index e20e1e1..f14d375 100644 --- a/webui/src/features/admin/AdminPage.tsx +++ b/webui/src/features/admin/AdminPage.tsx @@ -13,7 +13,10 @@ import type { import { fetchShellAuth } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { PageScrollViewport } from "@govoplan/core-webui"; -import { ModuleSubnav, type ModuleSubnavGroup } from "@govoplan/core-webui"; +import { + TreeSubnav, + type TreeSubnavNode +} from "@govoplan/core-webui"; import { adminReadScopes, hasAnyScope, hasScope } from "@govoplan/core-webui"; import SystemUsersPanel from "./SystemUsersPanel"; import TenantSettingsPanel from "./TenantSettingsPanel"; @@ -36,7 +39,17 @@ import { } from "@govoplan/core-webui"; type AdminSection = string; -type OrderedAdminNavItem = { id: AdminSection; label: string; order: number }; +type OrderedAdminNavItem = { + id: AdminSection; + label: string; + order: number; + moduleId?: string; + kind?: "management" | "settings"; +}; +type AdminNavGroup = { + title: string; + items: OrderedAdminNavItem[]; +}; const handledAdminSectionIds = new Set([ "overview", @@ -91,6 +104,26 @@ const builtInAdminSurfaceIds: Record = { "tenant-user-file-connectors": "files.admin.user-connectors" }; +const builtInAdminSectionMetadata: Record< + string, + Pick +> = { + "system-settings": { moduleId: "admin", kind: "settings" }, + "system-file-connectors": { moduleId: "files", kind: "settings" }, + "system-mail-servers": { moduleId: "mail", kind: "settings" }, + "system-credentials": { moduleId: "access", kind: "settings" }, + "tenant-settings": { moduleId: "access", kind: "settings" }, + "tenant-file-connectors": { moduleId: "files", kind: "settings" }, + "tenant-mail-servers": { moduleId: "mail", kind: "settings" }, + "tenant-credentials": { moduleId: "access", kind: "settings" }, + "tenant-group-file-connectors": { moduleId: "files", kind: "settings" }, + "tenant-group-mail-servers": { moduleId: "mail", kind: "settings" }, + "tenant-group-credentials": { moduleId: "access", kind: "settings" }, + "tenant-user-file-connectors": { moduleId: "files", kind: "settings" }, + "tenant-user-mail-servers": { moduleId: "mail", kind: "settings" }, + "tenant-user-credentials": { moduleId: "access", kind: "settings" } +}; + export default function AdminPage({ settings, auth, @@ -206,11 +239,10 @@ export default function AdminPage({ ); } - const adminSubnav: ModuleSubnavGroup[] = [ + const adminNavGroups: AdminNavGroup[] = [ { title: "ADMINISTRATION", - items: asSubnavItems( - sortNavItems([ + items: sortNavItems([ ...contributedNavItems(contributedSections, available, "ROOT"), visibleNavItem(available, "system-modules", "i18n:govoplan-access.modules.04e9462c", 10), visibleNavItem(available, "system-configuration-packages", "i18n:govoplan-access.packages.0a999012", 20), @@ -218,12 +250,10 @@ export default function AdminPage({ visibleNavItem(available, "system-configuration-changes", "i18n:govoplan-access.changes.8aa57de6", 40), ...contributedNavItems(contributedSections, available, "ADMINISTRATION", handledAdminSectionIds) ]) - ) }, { title: "GLOBAL", - items: asSubnavItems( - sortNavItems([ + items: sortNavItems([ visibleNavItem(available, "system-tenants", "i18n:govoplan-access.tenants.1f7ae776", 10), visibleNavItem(available, "system-roles", "i18n:govoplan-access.system_roles.a9461aa6", 20), visibleNavItem(available, "system-role-templates", "i18n:govoplan-access.tenant_role_templates", 30), @@ -235,12 +265,10 @@ export default function AdminPage({ ...contributedNavItems(contributedSections, available, "GLOBAL", handledAdminSectionIds), ...contributedNavItems(contributedSections, available, "SYSTEM", handledAdminSectionIds) ]) - ) }, { title: "TENANT", - items: asSubnavItems( - sortNavItems([ + items: sortNavItems([ visibleNavItem(available, "tenant-roles", "i18n:govoplan-access.roles.47dcc27d", 10), visibleNavItem(available, "tenant-function-role-mappings", "i18n:govoplan-access.function_role_mappings.2b64e9c3", 20), visibleNavItem(available, "tenant-groups", "i18n:govoplan-access.groups.ae9629f4", 30), @@ -252,37 +280,38 @@ export default function AdminPage({ visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 90), ...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds) ]) - ) }, { title: "GROUP", - items: asSubnavItems( - sortNavItems([ + items: sortNavItems([ visibleNavItem(available, "tenant-group-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), visibleNavItem(available, "tenant-group-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20), visibleNavItem(available, "tenant-group-credentials", "i18n:govoplan-core.credentials.dd097a22", 30), ...contributedNavItems(contributedSections, available, "GROUP", handledAdminSectionIds) ]) - ) }, { title: "USER", - items: asSubnavItems( - sortNavItems([ + items: sortNavItems([ visibleNavItem(available, "tenant-user-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), visibleNavItem(available, "tenant-user-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20), visibleNavItem(available, "tenant-user-credentials", "i18n:govoplan-core.credentials.dd097a22", 30), ...contributedNavItems(contributedSections, available, "USER", handledAdminSectionIds) ]) - ) } ].filter((group) => group.items.length > 0); + const adminTree = adminNavigationTree(adminNavGroups); const contributedSection = contributionById.get(active); const contributionContext = { settings, auth, onAuthChange, refreshAuth, availableSections: available, selectSection }; return (
- +
{contributedSection && contributedSection.render(contributionContext)} @@ -345,17 +374,74 @@ function contributedNavItems( ): OrderedAdminNavItem[] { return sections .filter((section) => (section.group ?? "SYSTEM") === group && available.has(section.id) && !excludedIds.has(section.id)) - .map((section) => ({ id: section.id, label: section.label, order: section.order ?? 100 })); + .map((section) => ({ + id: section.id, + label: section.label, + order: section.order ?? 100, + moduleId: section.moduleId, + kind: section.kind + })); } function visibleNavItem(available: ReadonlySet, id: AdminSection, label: string, order: number): OrderedAdminNavItem | null { - return available.has(id) ? { id, label, order } : null; + return available.has(id) + ? { id, label, order, ...builtInAdminSectionMetadata[id] } + : null; } function sortNavItems(items: Array): OrderedAdminNavItem[] { return items.filter((item): item is OrderedAdminNavItem => item !== null).sort((left, right) => left.order - right.order); } -function asSubnavItems(items: OrderedAdminNavItem[]) { - return items.map(({ id, label }) => ({ id, label })); +function adminNavigationTree( + groups: AdminNavGroup[] +): TreeSubnavNode[] { + return groups.map((group) => { + const managementItems = group.items.filter( + (item) => item.kind !== "settings" + ); + const settingsItems = group.items.filter( + (item) => item.kind === "settings" + ); + const children: TreeSubnavNode[] = managementItems.map( + ({ id, label }) => ({ id, label }) + ); + if (settingsItems.length > 0) { + const byModule = new Map(); + for (const item of settingsItems) { + const moduleId = item.moduleId ?? "platform"; + byModule.set(moduleId, [...(byModule.get(moduleId) ?? []), item]); + } + children.push({ + branchId: `admin-${group.title.toLowerCase()}-settings`, + label: "i18n:govoplan-core.settings.c7f73bb5", + defaultExpanded: false, + children: [...byModule.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([moduleId, items]) => ({ + branchId: `admin-${group.title.toLowerCase()}-settings-${moduleId}`, + label: moduleLabel(moduleId), + defaultExpanded: items.some( + (item) => item.id === "system-settings" + ), + children: items.map(({ id, label }) => ({ id, label })) + })) + }); + } + return { + branchId: `admin-${group.title.toLowerCase()}`, + label: group.title, + defaultExpanded: group.title === "ADMINISTRATION", + children + }; + }); +} + +function moduleLabel(moduleId: string): string { + if (moduleId === "platform") return "Platform"; + return moduleId + .split(/[-_]/) + .filter(Boolean) + .map((part) => part[0].toUpperCase() + part.slice(1)) + .join(" "); }