feat: organize administration as a settings tree

This commit is contained in:
2026-07-30 17:42:06 +02:00
parent a758c8f2da
commit e8ea347654

View File

@@ -13,7 +13,10 @@ import type {
import { fetchShellAuth } from "@govoplan/core-webui"; import { fetchShellAuth } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui";
import { PageScrollViewport } 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 { adminReadScopes, hasAnyScope, hasScope } from "@govoplan/core-webui";
import SystemUsersPanel from "./SystemUsersPanel"; import SystemUsersPanel from "./SystemUsersPanel";
import TenantSettingsPanel from "./TenantSettingsPanel"; import TenantSettingsPanel from "./TenantSettingsPanel";
@@ -36,7 +39,17 @@ import {
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
type AdminSection = string; 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<string>([ const handledAdminSectionIds = new Set<string>([
"overview", "overview",
@@ -91,6 +104,26 @@ const builtInAdminSurfaceIds: Record<string, string> = {
"tenant-user-file-connectors": "files.admin.user-connectors" "tenant-user-file-connectors": "files.admin.user-connectors"
}; };
const builtInAdminSectionMetadata: Record<
string,
Pick<OrderedAdminNavItem, "moduleId" | "kind">
> = {
"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({ export default function AdminPage({
settings, settings,
auth, auth,
@@ -206,11 +239,10 @@ export default function AdminPage({
); );
} }
const adminSubnav: ModuleSubnavGroup<AdminSection>[] = [ const adminNavGroups: AdminNavGroup[] = [
{ {
title: "ADMINISTRATION", title: "ADMINISTRATION",
items: asSubnavItems( items: sortNavItems([
sortNavItems([
...contributedNavItems(contributedSections, available, "ROOT"), ...contributedNavItems(contributedSections, available, "ROOT"),
visibleNavItem(available, "system-modules", "i18n:govoplan-access.modules.04e9462c", 10), visibleNavItem(available, "system-modules", "i18n:govoplan-access.modules.04e9462c", 10),
visibleNavItem(available, "system-configuration-packages", "i18n:govoplan-access.packages.0a999012", 20), 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), visibleNavItem(available, "system-configuration-changes", "i18n:govoplan-access.changes.8aa57de6", 40),
...contributedNavItems(contributedSections, available, "ADMINISTRATION", handledAdminSectionIds) ...contributedNavItems(contributedSections, available, "ADMINISTRATION", handledAdminSectionIds)
]) ])
)
}, },
{ {
title: "GLOBAL", title: "GLOBAL",
items: asSubnavItems( items: sortNavItems([
sortNavItems([
visibleNavItem(available, "system-tenants", "i18n:govoplan-access.tenants.1f7ae776", 10), visibleNavItem(available, "system-tenants", "i18n:govoplan-access.tenants.1f7ae776", 10),
visibleNavItem(available, "system-roles", "i18n:govoplan-access.system_roles.a9461aa6", 20), visibleNavItem(available, "system-roles", "i18n:govoplan-access.system_roles.a9461aa6", 20),
visibleNavItem(available, "system-role-templates", "i18n:govoplan-access.tenant_role_templates", 30), 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, "GLOBAL", handledAdminSectionIds),
...contributedNavItems(contributedSections, available, "SYSTEM", handledAdminSectionIds) ...contributedNavItems(contributedSections, available, "SYSTEM", handledAdminSectionIds)
]) ])
)
}, },
{ {
title: "TENANT", title: "TENANT",
items: asSubnavItems( items: sortNavItems([
sortNavItems([
visibleNavItem(available, "tenant-roles", "i18n:govoplan-access.roles.47dcc27d", 10), 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-function-role-mappings", "i18n:govoplan-access.function_role_mappings.2b64e9c3", 20),
visibleNavItem(available, "tenant-groups", "i18n:govoplan-access.groups.ae9629f4", 30), 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), visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 90),
...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds) ...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds)
]) ])
)
}, },
{ {
title: "GROUP", title: "GROUP",
items: asSubnavItems( items: sortNavItems([
sortNavItems([
visibleNavItem(available, "tenant-group-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), 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-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
visibleNavItem(available, "tenant-group-credentials", "i18n:govoplan-core.credentials.dd097a22", 30), visibleNavItem(available, "tenant-group-credentials", "i18n:govoplan-core.credentials.dd097a22", 30),
...contributedNavItems(contributedSections, available, "GROUP", handledAdminSectionIds) ...contributedNavItems(contributedSections, available, "GROUP", handledAdminSectionIds)
]) ])
)
}, },
{ {
title: "USER", title: "USER",
items: asSubnavItems( items: sortNavItems([
sortNavItems([
visibleNavItem(available, "tenant-user-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), 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-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
visibleNavItem(available, "tenant-user-credentials", "i18n:govoplan-core.credentials.dd097a22", 30), visibleNavItem(available, "tenant-user-credentials", "i18n:govoplan-core.credentials.dd097a22", 30),
...contributedNavItems(contributedSections, available, "USER", handledAdminSectionIds) ...contributedNavItems(contributedSections, available, "USER", handledAdminSectionIds)
]) ])
)
} }
].filter((group) => group.items.length > 0); ].filter((group) => group.items.length > 0);
const adminTree = adminNavigationTree(adminNavGroups);
const contributedSection = contributionById.get(active); const contributedSection = contributionById.get(active);
const contributionContext = { settings, auth, onAuthChange, refreshAuth, availableSections: available, selectSection }; const contributionContext = { settings, auth, onAuthChange, refreshAuth, availableSections: available, selectSection };
return ( return (
<div className="workspace module-workspace"> <div className="workspace module-workspace">
<ModuleSubnav active={active} groups={adminSubnav} onSelect={selectSection} /> <TreeSubnav
active={active}
nodes={adminTree}
onSelect={selectSection}
ariaLabel="i18n:govoplan-access.admin.4e7afebc"
/>
<section className="workspace-content"> <section className="workspace-content">
<div className="content-pad workspace-data-page"> <div className="content-pad workspace-data-page">
{contributedSection && contributedSection.render(contributionContext)} {contributedSection && contributedSection.render(contributionContext)}
@@ -345,17 +374,74 @@ function contributedNavItems(
): OrderedAdminNavItem[] { ): OrderedAdminNavItem[] {
return sections return sections
.filter((section) => (section.group ?? "SYSTEM") === group && available.has(section.id) && !excludedIds.has(section.id)) .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<string>, id: AdminSection, label: string, order: number): OrderedAdminNavItem | null { function visibleNavItem(available: ReadonlySet<string>, 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 | null>): OrderedAdminNavItem[] { function sortNavItems(items: Array<OrderedAdminNavItem | null>): OrderedAdminNavItem[] {
return items.filter((item): item is OrderedAdminNavItem => item !== null).sort((left, right) => left.order - right.order); return items.filter((item): item is OrderedAdminNavItem => item !== null).sort((left, right) => left.order - right.order);
} }
function asSubnavItems(items: OrderedAdminNavItem[]) { function adminNavigationTree(
return items.map(({ id, label }) => ({ id, label })); groups: AdminNavGroup[]
): TreeSubnavNode<AdminSection>[] {
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<AdminSection>[] = managementItems.map(
({ id, label }) => ({ id, label })
);
if (settingsItems.length > 0) {
const byModule = new Map<string, OrderedAdminNavItem[]>();
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(" ");
} }