From 44516a03aab7018f217281be3668219e0943bd65 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 24 Jun 2026 10:18:47 +0200 Subject: [PATCH] WebUI module split; docs --- README.md | 62 +++-- docs/RBAC_MANIFEST.md | 291 ++++++++++++++++++++++++ docs/SYSTEM_GOVERNANCE_MANIFEST.md | 178 +++++++++++++++ webui/src/App.tsx | 67 ++---- webui/src/components/AccessBoundary.tsx | 11 +- webui/src/components/table/DataGrid.tsx | 15 +- webui/src/platform/modules.ts | 6 +- webui/src/types.ts | 2 + 8 files changed, 558 insertions(+), 74 deletions(-) create mode 100644 docs/RBAC_MANIFEST.md create mode 100644 docs/SYSTEM_GOVERNANCE_MANIFEST.md diff --git a/README.md b/README.md index 2c3b5c8..2cb7e70 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,68 @@ # govoplan-core -Reusable core components for the Government Operational Platform GovOPlaN. +GovOPlaN core is the platform runner and shared foundation. It owns the server entry point, database/session primitives, tenant and RBAC infrastructure, governance policy, audit/auth helpers, module discovery, migration registration, and the shared WebUI shell. Feature code is supplied by installed modules. -This repository owns platform-level contracts and infrastructure that should not be coupled to a consuming product module: +## Repository ownership -- backend module manifests and registry validation -- server runner integration for routes, metadata and module availability -- database session/base primitives and module migration registration -- tenant/access/RBAC models and permission evaluation -- stable principal DTOs, token helpers and tenant datastore abstractions -- shared WebUI package `@govoplan/core-webui` for API/CSRF/auth helpers, core types, generic UI components, login UI and shell layout primitives +Core owns: -Modules register backend routes, permissions, nav items, frontend package metadata, SQLAlchemy metadata and migration locations through their `govoplan.modules` manifest. The core runner aggregates those contributions and exposes the combined platform. +- `govoplan_core.server.app:app`, the FastAPI entry point used by uvicorn +- `GovoplanServerConfig`, module discovery, registry validation, and route aggregation +- SQLAlchemy base/session helpers and module migration registration +- tenant/account/session/RBAC/governance/audit models and services +- core API routes for auth, admin, platform metadata, audit, and system health +- `@govoplan/core-webui`, including login, CSRF/API helpers, shell layout, generic UI components, IconRail, DataGrid, access boundaries, and module route/nav contracts -## Development +Feature modules own their backend routers, models, migrations, permissions, frontend packages, nav items, and route contributions. Core should not import feature pages directly; it imports module manifests and renders their route contributions. -Install the backend core and module checkouts through the core development environment: +## Governance docs + +Canonical policy documents live in `docs/`: + +- [RBAC_MANIFEST.md](docs/RBAC_MANIFEST.md) +- [SYSTEM_GOVERNANCE_MANIFEST.md](docs/SYSTEM_GOVERNANCE_MANIFEST.md) + +Modules may define module-specific permissions and policy behavior, but the platform-level permission model and governance hierarchy belong here. + +## Backend development + +Create or activate the core virtual environment, then install core and sibling modules from this repository: ```bash cd /mnt/DATA/git/govoplan-core ./.venv/bin/python -m pip install -r requirements-dev.txt ``` -Install WebUI dependencies from the core WebUI runner, which links the module WebUI packages from the neighboring module repositories: +Run the platform server from core. The default config loads the access/core module only; product configs can enable installed modules. + +```bash +cd /mnt/DATA/git/govoplan-core +GOVOPLAN_SERVER_CONFIG=app.govoplan_config:get_server_config \ + ./.venv/bin/python -m uvicorn govoplan_core.server.app:app --reload --host 127.0.0.1 --port 8000 +``` + +`requirements-dev.txt` currently links the local `govoplan-files`, `govoplan-mail`, and `govoplan-campaign` checkouts for development. Production deployments should use tagged git dependencies or published packages. + +## WebUI development + +Install and run from the core WebUI host: ```bash cd /mnt/DATA/git/govoplan-core/webui PATH=/home/zemion/.nvm/versions/node/v22.22.3/bin:$PATH /home/zemion/.nvm/versions/node/v22.22.3/bin/npm install +PATH=/home/zemion/.nvm/versions/node/v22.22.3/bin:$PATH /home/zemion/.nvm/versions/node/v22.22.3/bin/npm run dev ``` -Production deployments should use tagged git dependencies or published package versions, not runtime file copying. +The host links sibling module WebUI packages through local file dependencies and Vite filesystem allowances. + +## Module contract + +Backend modules register through the `govoplan.modules` entry point and return a `ModuleManifest`. A manifest can contribute: + +- permissions and role templates +- API routers +- SQLAlchemy metadata and migration locations +- nav metadata and frontend package metadata +- resource ACL providers and tenant summary/delete-veto providers + +WebUI modules export a `PlatformWebModule` with nav items and route contributions. Core renders those routes with `settings` and `auth` context. diff --git a/docs/RBAC_MANIFEST.md b/docs/RBAC_MANIFEST.md new file mode 100644 index 0000000..5ae51cb --- /dev/null +++ b/docs/RBAC_MANIFEST.md @@ -0,0 +1,291 @@ +# Multi Seal Mail - Current RBAC and Resource-Access Model + +**Updated:** 2026-06-16 +**Current migration head:** `f5a6b7c8d9e0` + +## Authorization Equation + +An operation is permitted only when every applicable layer allows it: + +```text +effective role/API-key capability +AND resource ownership/share access +AND workflow state +AND active governance/policy constraints +``` + +RBAC answers what an actor may do. ACLs answer which resource the actor may do it to. Workflow state and policy decide whether the operation is currently valid. + +## Identity and Scope + +```text +Account global login identity ++- User membership tenant-local identity + +- direct tenant roles + +- active group memberships + | +- inherited tenant roles + +- tenant-local API keys + +Account ++- direct system-role assignments +``` + +A browser session has one active tenant membership. System privileges do not silently grant tenant data access. API keys remain tenant-local and receive the intersection of their configured scopes and their owner's live tenant scopes on every request. + +## Wildcards + +```text +tenant:* every canonical tenant permission +system:* every canonical system permission +* legacy alias interpreted as tenant:* only +``` + +Tenant wildcards never grant system permissions. + +## Canonical Tenant Permissions - 53 + +### Campaigns + +```text +campaign:read +campaign:create +campaign:update +campaign:copy +campaign:archive +campaign:delete +campaign:share +campaign:validate +campaign:build +campaign:review +campaign:send_test +campaign:queue +campaign:control +campaign:send +campaign:retry +campaign:reconcile +``` + +### Recipients + +```text +recipients:read +recipients:write +recipients:import +recipients:export +``` + +### Files + +```text +files:read +files:download +files:upload +files:organize +files:share +files:delete +files:admin +``` + +### Reports and Audit + +```text +reports:read +reports:export +reports:send +audit:read +``` + +### Mail Servers + +```text +mail_servers:read +mail_servers:use +mail_servers:test +mail_servers:write +mail_servers:manage_credentials +``` + +### Tenant Administration + +```text +admin:users:read +admin:users:create +admin:users:update +admin:users:suspend + +admin:groups:read +admin:groups:write +admin:groups:manage_members + +admin:roles:read +admin:roles:write +admin:roles:assign + +admin:api_keys:read +admin:api_keys:create +admin:api_keys:revoke + +admin:settings:read +admin:settings:write +admin:policies:read +admin:policies:write +``` + +## Canonical System Permissions - 18 + +```text +system:tenants:read +system:tenants:create +system:tenants:update +system:tenants:suspend + +system:accounts:read +system:accounts:create +system:accounts:update +system:accounts:suspend + +system:roles:read +system:roles:write +system:roles:assign + +system:access:read +system:access:assign + +system:audit:read +system:settings:read +system:settings:write +system:governance:read +system:governance:write +``` + +`system:access:*` remains as a compatibility/read and assignment boundary for cross-tenant/system access handling. It is not a separate primary UI area. + +## Default Tenant Roles + +- **Owner:** `tenant:*`. At least one active operational owner must remain. +- **Tenant administrator:** settings, policies, users, groups, roles and API keys plus read access to campaigns/files/reports/audit. Real delivery remains separately delegable. +- **Administrator (legacy):** all tenant permissions for upgraded installations. +- **Access administrator:** membership and assignment management within delegation limits. +- **Campaign manager:** prepare, validate and build campaigns; no review approval or real delivery by default. +- **Reviewer:** inspect and approve prepared campaign messages. +- **Sender:** mock-test, queue, control, send, retry and reconcile prepared campaigns; can use/test approved mail profiles. +- **File manager:** managed file operations without campaign delivery rights. +- **Viewer:** read campaigns, recipients, files and reports. +- **Auditor:** read campaigns, recipient evidence, reports and audit records; export detailed evidence. + +## Default System Roles + +- **System owner:** `system:*`, protected. At least one active account must retain it. +- **System administrator:** all specific system permissions, editable and not protected. +- **System auditor:** read-only system registry/settings/governance/audit role, editable. + +## Delegation Ceiling + +For role definition, assignment and API-key creation: + +```text +requested scopes subset of actor delegateable scopes +``` + +Rules: + +1. Tenant roles may contain tenant scopes only. +2. System roles may contain system scopes only. +3. Definition rights and assignment rights are separate. +4. Group definition and group membership management are separate. +5. API-key scopes are intersected with the owner's current effective scopes on every request. +6. Suspended accounts, users, tenants or groups stop contributing access immediately. +7. Administrative updates are field-sensitive; a user with only status authority cannot change role assignments. + +## Campaign Ownership and ACLs + +A campaign has exactly one owner: + +```text +owner user OR owner group +``` + +Additional active shares may target users or groups with `read` or `write`. + +Resolution: + +- owner user: read and write; +- member of owner group: read and write; +- explicit read share: read; +- explicit write share: read and write; +- `tenant:*`: tenant-wide ACL bypass; +- ordinary campaign permission without ownership/share: no object access. + +ACLs do not add capabilities. A write share still needs the specific permission for update, validation, review, send, report, retry or reconciliation. + +## Sensitive Recipient Boundary + +Recipient-complete campaign JSON, message data and job detail require `recipients:read`. Recipient edits require `recipients:write`; exports require `recipients:export`; import is reserved for the dedicated recipient import/list workflow. + +## Files + +| Permission | Operations | +|---|---| +| `files:read` | list, search, inspect, resolve metadata | +| `files:download` | download file bytes and generated ZIP archives | +| `files:upload` | upload files and ZIP contents | +| `files:organize` | create folders, rename, move, copy and bulk rename | +| `files:share` | create/revoke file shares | +| `files:delete` | delete/hide files and folders subject to retention | +| `files:admin` | tenant-wide administration of user/group file spaces | + +## Mail Servers + +| Permission | Boundary | +|---|---| +| `mail_servers:read` | profile metadata and effective policy visibility | +| `mail_servers:use` | select an approved profile without reading secrets | +| `mail_servers:test` | run server-side connection tests | +| `mail_servers:write` | define/edit profiles in allowed scopes | +| `mail_servers:manage_credentials` | create/replace SMTP/IMAP secrets or campaign-level credentials where policy allows | + +Reusable encrypted profiles now exist. Effective usability is also constrained by hierarchical mail-profile policy, ownership, allowed/forced profile sets, credential inheritance/override rules and allow/deny patterns. + +## Sessions, API Keys and CSRF + +- Browser login creates an HttpOnly session cookie and a separate readable CSRF cookie. +- Unsafe cookie-authenticated requests require matching CSRF cookie/header and stored CSRF hash. +- API keys remain supported for CLI/automation and do not use browser CSRF. +- Login responses still expose a compatibility session token in the response body; the WebUI does not persist it. + +## Legacy Compatibility + +Runtime aliases remain only for names that are no longer canonical, including: + +```text +campaign:write +attachments:read +attachments:write +admin:users +admin:users:write +admin:api_keys:write +admin:settings +system:tenants:write +system:access:write +``` + +Canonical scopes are not widened by runtime alias expansion after migration. + +## Deferred Permission Families + +Add these only with their corresponding implemented features: + +```text +templates:* +address_books:* +recipient_lists:* +connectors:* +dsar:* +system:monitoring:read +system:backups:run +system:backups:restore +system:updates:apply +system:updates:rollback +``` + +A separate `retention:*` family is not currently canonical because retention is managed through system settings and tenant policy scopes. Add it only if retention operation duties need separation from general policy/settings administration. diff --git a/docs/SYSTEM_GOVERNANCE_MANIFEST.md b/docs/SYSTEM_GOVERNANCE_MANIFEST.md new file mode 100644 index 0000000..925b3c7 --- /dev/null +++ b/docs/SYSTEM_GOVERNANCE_MANIFEST.md @@ -0,0 +1,178 @@ +# Multi Seal Mail - Current System and Tenant Governance Model + +**Updated:** 2026-06-16 +**Current migration head:** `f5a6b7c8d9e0` + +## Governance Rule + +System policy is authoritative for tenants and all lower levels. Each lower level may only narrow what it inherits: + +```text +system +-> tenant +-> user or group owner +-> campaign +``` + +Lower levels do not widen privileges, allowed profiles, retention durations or credential rights granted by a higher level. + +## Administration Structure + +```text +SYSTEM +- Settings +- Retention +- Mail servers +- Tenants +- Users +- Groups +- System roles +- Tenant roles +- Audit + +TENANT +- Settings boundary +- Users +- Groups +- Roles +- API keys +- Mail servers +- Retention +- Audit + +USER +- User mail +- User retention + +GROUP +- Group mail +- Group retention +``` + +There is no separate System access page. Compatibility access scopes remain in the backend for assignment/read boundaries. + +## Tenant Governance + +System settings define tenant defaults and whether tenants may narrow selected options. Tenant overrides can only restrict: + +- custom groups; +- custom roles; +- tenant API keys. + +The backend enforces that tenant governance cannot widen system-denied privileges. + +## Mail-Profile Governance + +Mail server profiles may exist at these scopes: + +```text +system +tenant +user +group +campaign +``` + +Effective campaign profile availability follows campaign ownership. A campaign owned by a user resolves through system, tenant, that user and campaign policy. A group-owned campaign resolves through system, tenant, that group and campaign policy. + +Policy semantics: + +- higher levels define the maximum available profile set; +- lower levels can further restrict the set; +- forced profiles mean the lower level must choose from the forced set; +- a forced set with one profile effectively enforces that profile; +- campaign-level profile creation is allowed only if the effective policy permits it; +- credentials may be inherited from the authoritative profile or supplied by a lower level only when override is allowed; +- deny patterns always win over allow patterns; +- empty or `*` allowlist means allow all except denied; +- non-empty allowlist means at least one allow rule must match and no deny rule may match. + +Pattern targets: + +```text +SMTP hostname +IMAP hostname +envelope sender +From header +recipient domains +``` + +Ownership transfer is intentionally deferred as a two-step workflow: original owner initiates, new owner accepts and reselects/repairs the mail profile if their effective policy requires it. + +## Retention Governance + +Retention policy is hierarchical: + +```text +system -> tenant -> user/group -> campaign +``` + +Managed fields: + +- raw campaign JSON retention days; +- generated EML retention days; +- stored report detail retention days; +- mock mailbox retention days; +- audit detail retention days. + +Rules: + +- system may set concrete defaults or unlimited retention; +- system exposes allow-limiting toggles per field; +- tenants, users/groups and campaigns may only shorten inherited retention where the parent allows limiting; +- blank lower-level values inherit; +- mock mailbox retention is currently system-level because mock mailbox records do not yet carry tenant/campaign ownership metadata; +- dry-run/apply retention actions report affected classes before destructive cleanup. + +## Role Definitions and Assignments + +### System roles + +System roles define instance-wide permissions. `system:*` is stored as one wildcard and displayed as granting the full system catalogue. System owner is protected. + +### Tenant roles + +Tenant roles can be system-governed templates or tenant-local definitions, subject to system tenant-governance settings and actor delegation ceilings. Wildcard counts are expanded against the canonical tenant catalogue. + +## Audit Access + +Audit access remains scope-separated: + +```text +system audit -> system:audit:read +tenant audit -> active tenant + audit:read +``` + +Audit pages use server pagination, filtering and bounded grids. + +## Tenant Switching + +Tenant switching preserves the current URL when possible and falls back when a route/resource is not accessible in the new tenant context. + +The tenant selector is hidden for ordinary single-tenant accounts and visible for multi-tenant or system tenant-management contexts. + +## DataGrid Contract in Administration + +Admin lists use bounded container grids: + +- one flexible fill column; +- fixed total table width; +- compact action/status/count columns; +- resizable text/date columns; +- no intrinsic content growth; +- sticky headers where needed; +- server pagination for audit. + +## Still Deferred + +- real SMTP/IMAP test-bed verification and operator runbook; +- recipient import with column mapping; +- Seafile/external connector governance; +- system/tenant/group/user file-space hierarchy and external storage hierarchy; +- session/device revocation UI; +- backup/restore, monitoring and update procedures; +- DSAR workflows and evidence bundle verifier; +- campaign ownership transfer workflow; +- policy impact analysis before delete/disable/unshare/change; +- LDAP/OIDC/SAML provisioning; +- destructive tenant erasure orchestration. diff --git a/webui/src/App.tsx b/webui/src/App.tsx index f463dbc..a2b1b4a 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,26 +1,18 @@ -import { Navigate, Route, Routes, useParams } from "react-router-dom"; -import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import { Navigate, Route, Routes } from "react-router-dom"; +import { lazy, Suspense, useEffect, useMemo, useState } from "react"; import { fetchMe } from "./api/auth"; import { fetchPlatformModules } from "./api/platform"; import { loadApiSettings, saveApiSettings } from "./api/client"; import type { ApiSettings, AuthInfo, LoginResponse, PlatformModuleInfo } from "./types"; import AppShell from "./layout/AppShell"; import PublicLandingPage from "./features/auth/PublicLandingPage"; -import { PermissionBoundary, ResourceAccessBoundary } from "./components/AccessBoundary"; +import { PermissionBoundary } from "./components/AccessBoundary"; import { adminReadScopes } from "./utils/permissions"; -import { firstAccessibleRoute, navItemsForModules, resolveInstalledWebModules } from "./platform/modules"; -import PlaceholderPage from "./features/PlaceholderPage"; -import { getCampaign } from "@govoplan/campaign-webui"; +import { firstAccessibleRoute, navItemsForModules, resolveInstalledWebModules, routeContributionsForModules } from "./platform/modules"; const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage")); -const CampaignListPage = lazy(() => import("@govoplan/campaign-webui").then((module) => ({ default: module.CampaignListPage }))); -const CampaignWorkspace = lazy(() => import("@govoplan/campaign-webui").then((module) => ({ default: module.CampaignWorkspace }))); const SettingsPage = lazy(() => import("./features/settings/SettingsPage")); const AdminPage = lazy(() => import("./features/admin/AdminPage")); -const TemplatesPage = lazy(() => import("@govoplan/campaign-webui").then((module) => ({ default: module.TemplatesPage }))); -const FilesPage = lazy(() => import("@govoplan/files-webui").then((module) => ({ default: module.FilesPage }))); -const OperatorQueuePage = lazy(() => import("@govoplan/campaign-webui").then((module) => ({ default: module.OperatorQueuePage }))); -const AddressBookPage = lazy(() => import("@govoplan/campaign-webui").then((module) => ({ default: module.AddressBookPage }))); export default function App() { const [settings, setSettings] = useState(() => loadApiSettings()); @@ -30,6 +22,7 @@ export default function App() { const webModules = useMemo(() => resolveInstalledWebModules(platformModules), [platformModules]); const navItems = useMemo(() => navItemsForModules(webModules), [webModules]); + const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]); function updateSettings(next: ApiSettings) { setSettings(next); @@ -152,33 +145,17 @@ export default function App() { } /> } /> - - - - } /> - - - - } /> - } /> - - - - } /> - } /> - - - - } /> - - - - } /> + {moduleRoutes.map((route) => ( + + {route.render({ settings, auth })} + + } + /> + ))} } /> @@ -191,15 +168,3 @@ export default function App() { ); } - -function CampaignResourceRoute({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) { - const { campaignId = "" } = useParams(); - const tenantId = (auth.active_tenant ?? auth.tenant).id; - const probe = useCallback(() => getCampaign(settings, campaignId), [settings.apiBaseUrl, settings.apiKey, campaignId, tenantId]); - - return ( - - - - ); -} diff --git a/webui/src/components/AccessBoundary.tsx b/webui/src/components/AccessBoundary.tsx index 35bccc6..7c7ff8b 100644 --- a/webui/src/components/AccessBoundary.tsx +++ b/webui/src/components/AccessBoundary.tsx @@ -3,17 +3,20 @@ import { Navigate } from "react-router-dom"; import type { AuthInfo } from "../types"; import { isApiError } from "../api/client"; import DismissibleAlert from "./DismissibleAlert"; -import { hasAnyScope } from "../utils/permissions"; +import { hasAnyScope, hasScope } from "../utils/permissions"; type PermissionBoundaryProps = { auth: AuthInfo; - anyOf: string[]; + anyOf?: string[]; + allOf?: string[]; fallback: string; children: ReactNode; }; -export function PermissionBoundary({ auth, anyOf, fallback, children }: PermissionBoundaryProps) { - return hasAnyScope(auth, anyOf) ? <>{children} : ; +export function PermissionBoundary({ auth, anyOf = [], allOf = [], fallback, children }: PermissionBoundaryProps) { + if (allOf.length && !allOf.every((scope) => hasScope(auth, scope))) return ; + if (anyOf.length && !hasAnyScope(auth, anyOf)) return ; + return <>{children}; } type ResourceAccessBoundaryProps = { diff --git a/webui/src/components/table/DataGrid.tsx b/webui/src/components/table/DataGrid.tsx index 7051f2a..636bb0c 100644 --- a/webui/src/components/table/DataGrid.tsx +++ b/webui/src/components/table/DataGrid.tsx @@ -670,7 +670,7 @@ export default function DataGrid({ startX: event.clientX, startWidth: currentWidth, minWidth: effectiveColumnMinWidth(column), - maxWidth: Math.max(effectiveColumnMinWidth(column), column.maxWidth ?? 2000), + maxWidth: effectiveColumnMaxWidth(column), baseWidths, rightResizeTargets, bufferTarget: canUseBuffer ? { @@ -1223,7 +1223,7 @@ function sanitizePersistedColumnState( const column = columnMap.get(columnId); if (!column || !Number.isFinite(width)) return []; const minimum = effectiveColumnMinWidth(column); - const maximum = Math.max(minimum, column.maxWidth ?? 2000); + const maximum = effectiveColumnMaxWidth(column, minimum); return [[columnId, Math.min(maximum, Math.max(minimum, width))]]; }) ); @@ -1246,7 +1246,7 @@ function sanitizePersistedColumnState( function widthForColumn(column: DataGridColumn, savedWidth?: number): string { const minimum = effectiveColumnMinWidth(column); - const maximum = Math.max(minimum, column.maxWidth ?? 2000); + const maximum = effectiveColumnMaxWidth(column, minimum); if (savedWidth !== undefined) return `${Math.min(maximum, Math.max(minimum, savedWidth))}px`; if (typeof column.width === "number") return `${Math.min(maximum, Math.max(minimum, column.width))}px`; if (column.width) return columnTrackWithMinimum(column.width, minimum); @@ -1291,7 +1291,7 @@ function resizeTargetForColumn(column: DataGridColumn, baseWidths: Record< columnId: column.id, startWidth: baseWidths[column.id] ?? columnPixelWidth(column), minWidth: minimum, - maxWidth: Math.max(minimum, column.maxWidth ?? 2000) + maxWidth: effectiveColumnMaxWidth(column, minimum) }; } @@ -1303,6 +1303,11 @@ function effectiveColumnMinWidth(column: DataGridColumn): number { return Math.max(column.minWidth ?? 0, affordanceWidth); } +function effectiveColumnMaxWidth(column: DataGridColumn, minimum = effectiveColumnMinWidth(column)): number { + if (column.maxWidth !== undefined) return Math.max(minimum, column.maxWidth); + return Math.max(minimum, isFlexibleWidth(column.width) ? BUFFER_MAX_WIDTH : 2000); +} + function totalColumnWidth(columns: DataGridColumn[], widths: Record): number { return columns.reduce((total, column) => total + (widths[column.id] ?? columnPixelWidth(column)), 0); } @@ -1450,7 +1455,7 @@ function isFlexibleWidth(width?: string | number): boolean { function columnPixelWidth(column: DataGridColumn, savedWidth?: number, measuredWidth?: number): number { const minimum = effectiveColumnMinWidth(column); - const maximum = Math.max(minimum, column.maxWidth ?? 2000); + const maximum = effectiveColumnMaxWidth(column, minimum); const configured = measuredWidth ?? savedWidth ?? (typeof column.width === "number" ? column.width : parsePixelWidth(column.width)) diff --git a/webui/src/platform/modules.ts b/webui/src/platform/modules.ts index 5d41802..a17062e 100644 --- a/webui/src/platform/modules.ts +++ b/webui/src/platform/modules.ts @@ -2,7 +2,7 @@ import { Activity, FileText, Folder, Form, LayoutDashboard, MailCheck, Users, ty import campaignModule from "@govoplan/campaign-webui"; import filesModule from "@govoplan/files-webui"; import mailModule from "@govoplan/mail-webui"; -import type { AuthInfo, PlatformModuleInfo, PlatformNavItem, PlatformWebModule } from "../types"; +import type { AuthInfo, PlatformModuleInfo, PlatformNavItem, PlatformRouteContribution, PlatformWebModule } from "../types"; import { adminReadScopes, hasAnyScope, hasScope } from "../utils/permissions"; export const shellNavItems: PlatformNavItem[] = [ @@ -73,6 +73,10 @@ export function navItemsForModules(modules: PlatformWebModule[]): PlatformNavIte return [...shellNavItems, ...modules.flatMap((module) => module.navItems ?? [])].sort((left, right) => (left.order ?? 100) - (right.order ?? 100)); } +export function routeContributionsForModules(modules: PlatformWebModule[]): PlatformRouteContribution[] { + return modules.flatMap((module) => module.routes ?? []).sort((left, right) => (left.order ?? 100) - (right.order ?? 100)); +} + export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = legacyModules): PlatformNavItem[] { return navItemsForModules(modules).filter((item) => { if (item.allOf?.length && !item.allOf.every((scope) => hasScope(auth, scope))) return false; diff --git a/webui/src/types.ts b/webui/src/types.ts index 0bd124d..cbfd4d8 100644 --- a/webui/src/types.ts +++ b/webui/src/types.ts @@ -122,6 +122,7 @@ export type PlatformNavItem = { }; export type PlatformRouteContext = { + settings: ApiSettings; auth: AuthInfo; }; @@ -129,6 +130,7 @@ export type PlatformRouteContribution = { path: string; anyOf?: string[]; allOf?: string[]; + order?: number; render: (context: PlatformRouteContext) => ReactNode; };