Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:22 +02:00
parent 9ba69286f7
commit 3966c7f33c
5 changed files with 40 additions and 24 deletions
+23 -12
View File
@@ -12,6 +12,7 @@ import type {
} from "@govoplan/core-webui";
import { fetchShellAuth } from "@govoplan/core-webui";
import { ActionBlockerHint } from "@govoplan/core-webui";
import { PageLayout, WorkspaceLayout } from "@govoplan/core-webui";
import { PageScrollViewport } from "@govoplan/core-webui";
import {
TreeSubnav,
@@ -322,15 +323,26 @@ export default function AdminPage({
const contributionContext = { settings, auth, onAuthChange, refreshAuth, availableSections: available, selectSection };
return (
<div className="workspace module-workspace">
<TreeSubnav
active={active}
nodes={adminTree}
onSelect={selectSection}
ariaLabel="i18n:govoplan-access.admin.4e7afebc"
/>
<section className="workspace-content">
<div className="content-pad workspace-data-page">
<WorkspaceLayout
className="module-workspace"
primary={(
<TreeSubnav
active={active}
nodes={adminTree}
onSelect={selectSection}
ariaLabel="i18n:govoplan-access.admin.4e7afebc"
/>
)}
primaryLabel="i18n:govoplan-access.admin.4e7afebc"
contentLabel="i18n:govoplan-access.admin.4e7afebc"
documentationType="admin"
>
<PageLayout
title="i18n:govoplan-access.admin.4e7afebc"
mode="workspace"
showHeader={false}
documentationType="admin"
>
{contributedSection && contributedSection.render(contributionContext)}
{!contributedSection && active === "system-mail-servers" && (
<MailProfilesPanel settings={settings} scopeType="system" canWriteProfiles={hasScope(auth, "system:settings:write")} canManageCredentials={hasScope(auth, "system:settings:write")} canWritePolicy={hasScope(auth, "system:settings:write")} />
@@ -368,9 +380,8 @@ export default function AdminPage({
{!contributedSection && active === "tenant-group-credentials" && <CredentialEnvelopesPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["admin:settings:write", "access:credential:write"])} />}
{!contributedSection && active === "tenant-user-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="user" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
{!contributedSection && active === "tenant-group-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
</div>
</section>
</div>
</PageLayout>
</WorkspaceLayout>
);
}
+3 -3
View File
@@ -1,4 +1,4 @@
import { DescriptionItem, DescriptionList } from "@govoplan/core-webui";
import { ContentGrid, DescriptionItem, DescriptionList } from "@govoplan/core-webui";
import { useEffect, useMemo, useRef, useState } from "react";
import { Pencil, Plus, Search, Trash2 } from "lucide-react";
import type { FormGrid, ApiSettings, AuthInfo } from "@govoplan/core-webui";
@@ -158,10 +158,10 @@ export default function GroupsPanel({ settings, auth, canDefine, canManageMember
<FormField label="i18n:govoplan-access.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} disabled={!canDefine || editing !== "new" && Boolean(editing?.system_required)} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-access.active.a733b809</option><option value="inactive">i18n:govoplan-access.inactive.09af574c</option></select></FormField>
<FormField label="i18n:govoplan-access.description.55f8ebc8"><textarea rows={3} value={draft.description} disabled={!canDefine || editing !== "new" && Boolean(editing?.system_template_id)} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
</FormGrid>
<div className="admin-assignment-grid">
<ContentGrid columns={2} spacing="block" collapseAt="wide">
<div><span className="form-label">i18n:govoplan-access.members.1cb449c1</span><AdminSelectionList options={users.map((user) => ({ id: user.id, label: user.display_name || user.email, description: user.email, disabled: !canManageMembers || !user.is_active || !user.account_is_active }))} selected={draft.memberIds} onChange={(memberIds) => setDraft({ ...draft, memberIds })} emptyText="i18n:govoplan-access.no_tenant_users_exist.96b4a88a" /></div>
<div><span className="form-label">i18n:govoplan-access.inherited_roles.8def9f05</span><AdminSelectionList options={roles.map((role) => ({ id: role.id, label: role.name, description: role.description, disabled: !canAssignRoles }))} selected={draft.roleIds} onChange={(roleIds) => setDraft({ ...draft, roleIds })} emptyText="i18n:govoplan-access.no_assignable_roles_exist.a4c268c2" /></div>
</div>
</ContentGrid>
<p className="muted small-note">i18n:govoplan-access.the_backend_evaluates_the_resulting_access_graph.f318a7dc</p>
</Dialog>
@@ -1,4 +1,4 @@
import { DescriptionItem, DescriptionList } from "@govoplan/core-webui";
import { ContentGrid, DescriptionItem, DescriptionList } from "@govoplan/core-webui";
import { useEffect, useMemo, useRef, useState } from "react";
import { Search, Pencil, Plus, Trash2 } from "lucide-react";
import type { FormGrid, ApiSettings } from "@govoplan/core-webui";
@@ -250,10 +250,10 @@ export default function SystemUsersPanel({
}
<FormField label="i18n:govoplan-access.account_status.8dd86c6d"><select value={draft.isActive ? "active" : "inactive"} disabled={Boolean(editing && editing !== "new" && (!canSuspend || editing.memberships.some((membership) => membership.is_last_active_owner)))} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-access.active.a733b809</option><option value="inactive">i18n:govoplan-access.inactive.09af574c</option></select></FormField>
</FormGrid>
<div className="admin-assignment-grid">
<ContentGrid columns={2} spacing="block" collapseAt="wide">
<div><span className="form-label">i18n:govoplan-access.system_roles.a9461aa6</span><AdminSelectionList options={roles.map((role) => ({ id: role.id, label: role.name, description: role.description, disabled: !canAssignRoles }))} selected={draft.roleIds} onChange={(roleIds) => setDraft({ ...draft, roleIds })} /></div>
<div><span className="form-label">i18n:govoplan-access.tenant_memberships.451de736</span><AdminSelectionList options={tenants.map((tenant) => ({ id: tenant.id, label: tenant.name, description: tenant.slug, disabled: !canManageMemberships || Boolean(draft.memberships.find((item) => item.tenant_id === tenant.id)?.is_last_active_owner) }))} selected={draft.memberships.map((item) => item.tenant_id)} onChange={setMembershipSelection} /></div>
</div>
</ContentGrid>
{editing && editing !== "new" && editing.memberships.some((membership) => membership.is_last_active_owner) && <p className="admin-protection-note">i18n:govoplan-access.this_account_is_the_last_active_operational_owne.5087839f</p>}
<p className="muted small-note">i18n:govoplan-access.removing_a_tenant_checkbox_suspends_that_members.7c6df77d</p>
</Dialog>
+5 -5
View File
@@ -1,4 +1,4 @@
import { DescriptionItem, DescriptionList } from "@govoplan/core-webui";
import { ContentGrid, DescriptionItem, DescriptionList } from "@govoplan/core-webui";
import { useEffect, useMemo, useRef, useState } from "react";
import { KeyRound, Pencil, Plus, Search, Trash2 } from "lucide-react";
import type { FormGrid, ApiSettings, AuthInfo } from "@govoplan/core-webui";
@@ -225,10 +225,10 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
</FormGrid>
{editing === "new" && <ToggleSwitch label="i18n:govoplan-access.require_password_change_when_account_settings_ar.69bce7a3" checked={draft.passwordResetRequired} onChange={(passwordResetRequired) => setDraft({ ...draft, passwordResetRequired })} />}
{editing && editing !== "new" && editing.is_last_active_owner && <p className="admin-protection-note">i18n:govoplan-access.this_membership_is_the_tenant_s_last_active_oper.072b247f</p>}
<div className="admin-assignment-grid">
<ContentGrid columns={2} spacing="block" collapseAt="wide">
<div><span className="form-label">i18n:govoplan-access.groups.ae9629f4</span><AdminSelectionList options={groups.filter((group) => group.is_active).map((group) => ({ id: group.id, label: group.name, description: group.description, disabled: !canManageGroups }))} selected={draft.groupIds} onChange={(groupIds) => setDraft({ ...draft, groupIds })} emptyText="i18n:govoplan-access.no_groups_exist_yet.9cd029f6" /></div>
<div><span className="form-label">i18n:govoplan-access.direct_roles.c4db7156</span><AdminSelectionList options={roles.map((role) => ({ id: role.id, label: role.name, description: role.description, disabled: !canAssignRoles }))} selected={draft.roleIds} onChange={(roleIds) => setDraft({ ...draft, roleIds })} emptyText="i18n:govoplan-access.no_assignable_roles_exist.a4c268c2" /></div>
</div>
</ContentGrid>
<p className="muted small-note">i18n:govoplan-access.group_roles_and_direct_roles_are_combined_the_ba.a43c2f16</p>
</Dialog>
@@ -251,7 +251,7 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
<DescriptionItem term={<>i18n:govoplan-access.function_facts.848b32cc</>}>{accessExplanation.function_facts.length}</DescriptionItem>
</DescriptionList>
<h3>i18n:govoplan-access.role_sources.6f42a672</h3>
{accessExplanation.role_sources.length ? <div className="admin-assignment-grid">
{accessExplanation.role_sources.length ? <ContentGrid columns={2} spacing="block" collapseAt="wide">
{accessExplanation.role_sources.map((source) => <div key={sourceKey(source)}>
<strong>{source.role_name}</strong>
<div className="muted small-note">
@@ -263,7 +263,7 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
<AccessExplanationLinks organizationHref={organizationHrefForSource(source)} idmHref={idmHrefForSource(source)} />
<div className="admin-scope-list">{source.permissions.map((scope) => <code key={scope}>{scope}</code>)}</div>
</div>)}
</div> : <p className="muted small-note">i18n:govoplan-access.no_role_sources_found.91013aa5</p>}
</ContentGrid> : <p className="muted small-note">i18n:govoplan-access.no_role_sources_found.91013aa5</p>}
<h3>i18n:govoplan-access.function_facts.848b32cc</h3>
{accessExplanation.function_facts.length ? <DescriptionList>
{accessExplanation.function_facts.map((fact) => <DescriptionItem key={fact.assignment_id} term={fact.function_name || fact.function_id}>