feat: organize administration as a settings tree
This commit is contained in:
@@ -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(" ");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user