From 418cf1648d215de9812975d3b6817bea5cd68e48 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 13 Jun 2026 22:31:23 +0100 Subject: [PATCH] Converge RBAC settings actions on shared Button primitives --- .../subsystems/frontend-primitives.md | 5 +++ .../subsystems/organization-settings.md | 5 +++ .../scripts/shared-template-registry.json | 22 ++++++++++ .../src/components/Settings/RolesPanel.tsx | 30 +++++++------ .../Settings/UserAssignmentsPanel.tsx | 10 +++-- .../__tests__/settingsArchitecture.test.ts | 22 ++++++++++ .../SharedPrimitives.guardrails.test.ts | 43 +++++++++++++++++++ 7 files changed, 119 insertions(+), 18 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index ce3982370..3cddacc5f 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -576,6 +576,11 @@ not a replacement status card, CTA band, or page-local nested card. security/privacy owns auth setup, password-change, credential-rotation, and read-only semantics, while `Button` owns the warning, primary, secondary, and settings-action chrome. + Organization RBAC settings actions follow the same boundary: organization + settings owns role creation, role editing/deletion, user-access assignment, + feature-gate, and row-action semantics, while `Button` / `ActionIconButton` + own primary, ghost, accent, danger, focus, disabled, and settings-action + chrome. If a new surface needs a variant that the shared primitive does not expose, extend the primitive and registry guard rather than adding a page-local class string. diff --git a/docs/release-control/v6/internal/subsystems/organization-settings.md b/docs/release-control/v6/internal/subsystems/organization-settings.md index 39396f942..a706350d9 100644 --- a/docs/release-control/v6/internal/subsystems/organization-settings.md +++ b/docs/release-control/v6/internal/subsystems/organization-settings.md @@ -124,6 +124,11 @@ create, review, and approve cross-organization shares. `LoadingSpinner` primitive. Organization settings owns RBAC loading semantics; frontend-primitives owns spinner size, tone, and accessible status behavior. +9. Keep RBAC role creation, role edit/delete row actions, and user-access + management actions on the shared `Button` / `ActionIconButton` primitives. + Organization settings owns the role and assignment semantics, feature-gate + availability, and row-action behavior; frontend-primitives owns primary, + ghost, accent, danger, focus, disabled, and settings-action chrome. ## Current State diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index aaeef0af0..86dfd3824 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -969,9 +969,11 @@ { "path": "src/components/Settings/ProLicensePlanSection.tsx" }, { "path": "src/components/Settings/ReportingPanel.tsx" }, { "path": "src/components/Settings/ResourcePicker.tsx" }, + { "path": "src/components/Settings/RolesPanel.tsx" }, { "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" }, { "path": "src/components/Settings/SecurityAuthPanel.tsx" }, { "path": "src/components/Settings/SSOProvidersPanel.tsx" }, + { "path": "src/components/Settings/UserAssignmentsPanel.tsx" }, { "path": "src/components/UpdateConfirmationModal.tsx" }, { "path": "src/components/UpdateProgressModal.tsx" }, { "path": "src/components/Workloads/GuestDrawer.tsx" }, @@ -1011,6 +1013,12 @@ "w-full sm:w-auto min-h-10 sm:min-h-9 flex items-center justify-center gap-1.5 px-3 py-2.5 text-sm rounded-md border border-border" ] }, + { + "path": "src/components/Settings/RolesPanel.tsx", + "patterns": [ + "inline-flex w-full sm:w-auto min-h-10 sm:min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700" + ] + }, { "path": "src/components/Settings/ReportingPanel.tsx", "patterns": [ @@ -1060,6 +1068,12 @@ "px-2 py-1 text-xs font-medium text-muted bg-surface-hover rounded hover:bg-surface-hover flex items-center gap-1" ] }, + { + "path": "src/components/Settings/UserAssignmentsPanel.tsx", + "patterns": [ + "inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium text-base-content hover:bg-surface-hover transition-colors" + ] + }, { "path": "src/components/Settings/InfrastructureInstallerSection.tsx", "patterns": [ @@ -1172,6 +1186,7 @@ { "path": "src/components/Alerts/AlertResourceTableMobile.tsx" }, { "path": "src/components/Alerts/AlertResourceTableRow.tsx" }, { "path": "src/components/Alerts/ResourceTable.tsx" }, + { "path": "src/components/Settings/RolesPanel.tsx" }, { "path": "src/components/Settings/SSOProvidersPanel.tsx" } ], "forbiddenPatterns": [ @@ -1211,6 +1226,13 @@ "p-2 text-slate-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900 rounded-md transition-colors", "class=\"text-slate-400 hover:text-base-content\"" ] + }, + { + "path": "src/components/Settings/RolesPanel.tsx", + "patterns": [ + "p-1.5 rounded-md text-slate-500 hover:text-blue-600 hover:bg-surface-hover dark:hover:text-blue-300", + "p-1.5 rounded-md text-slate-500 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900" + ] } ], "proof": [ diff --git a/frontend-modern/src/components/Settings/RolesPanel.tsx b/frontend-modern/src/components/Settings/RolesPanel.tsx index 73ca4ff23..1ac3982c0 100644 --- a/frontend-modern/src/components/Settings/RolesPanel.tsx +++ b/frontend-modern/src/components/Settings/RolesPanel.tsx @@ -1,4 +1,5 @@ import { Component, For, Show } from 'solid-js'; +import { ActionIconButton, Button } from '@/components/shared/Button'; import SettingsPanel from '@/components/shared/SettingsPanel'; import { RBACFeatureGateSection } from './RBACFeatureGateSection'; import { RolesEditorDialog } from './RolesEditorDialog'; @@ -19,15 +20,16 @@ export const RolesPanel: Component = () => { New Role - + } noPadding bodyClass="divide-y divide-border" @@ -87,22 +89,22 @@ export const RolesPanel: Component = () => { render: (role) => (
- - + Read-only diff --git a/frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx b/frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx index 9c59ca9b3..515be6042 100644 --- a/frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx +++ b/frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx @@ -1,4 +1,5 @@ import { Component, For, Show } from 'solid-js'; +import { Button } from '@/components/shared/Button'; import SettingsPanel from '@/components/shared/SettingsPanel'; import { RBACFeatureGateSection } from './RBACFeatureGateSection'; import { UserAssignmentsDialog } from './UserAssignmentsDialog'; @@ -109,14 +110,15 @@ export const UserAssignmentsPanel: Component = () => { label: 'Actions', align: 'right', render: (assignment) => ( - + ), }, ]} diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 17aff7a82..4366c3531 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -66,6 +66,8 @@ import organizationAccessManagementSectionSource from '../OrganizationAccessMana import organizationAccessMembersSectionSource from '../OrganizationAccessMembersSection.tsx?raw'; import organizationSharingCreateSectionSource from '../OrganizationSharingCreateSection.tsx?raw'; import rolesEditorDialogSource from '../RolesEditorDialog.tsx?raw'; +import rolesPanelSource from '../RolesPanel.tsx?raw'; +import userAssignmentsPanelSource from '../UserAssignmentsPanel.tsx?raw'; import diagnosticsResultsPanelSource from '../DiagnosticsResultsPanel.tsx?raw'; import diagnosticsModelSource from '../diagnosticsModel.ts?raw'; import agentProfilesStateSource from '../useAgentProfilesPanelState.ts?raw'; @@ -343,6 +345,26 @@ describe('settings architecture guardrails', () => { expect(securityAuthPanelSource).not.toContain( 'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border', ); + + expect(rolesPanelSource).toContain( + "import { ActionIconButton, Button } from '@/components/shared/Button';", + ); + expect(rolesPanelSource).toContain('variant="primary"'); + expect(rolesPanelSource).toContain('ActionIconButton'); + expect(rolesPanelSource).not.toContain( + 'inline-flex w-full sm:w-auto min-h-10 sm:min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600', + ); + expect(rolesPanelSource).not.toContain('p-1.5 rounded-md text-slate-500 hover:text-blue-600'); + expect(rolesPanelSource).not.toContain('p-1.5 rounded-md text-slate-500 hover:text-red-600'); + + expect(userAssignmentsPanelSource).toContain( + "import { Button } from '@/components/shared/Button';", + ); + expect(userAssignmentsPanelSource).toContain('variant="ghost"'); + expect(userAssignmentsPanelSource).toContain('size="settingsAction"'); + expect(userAssignmentsPanelSource).not.toContain( + 'inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-1.5 rounded-md', + ); }); it('keeps settings callouts on the shared CalloutCard primitive', () => { diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 4427b87d4..8a037adef 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -3097,9 +3097,11 @@ describe('shared primitive guardrails', () => { 'src/components/Settings/ProLicensePlanSection.tsx', 'src/components/Settings/ReportingPanel.tsx', 'src/components/Settings/ResourcePicker.tsx', + 'src/components/Settings/RolesPanel.tsx', 'src/components/Settings/SelfHostedCommercialRecoverySection.tsx', 'src/components/Settings/SecurityAuthPanel.tsx', 'src/components/Settings/SSOProvidersPanel.tsx', + 'src/components/Settings/UserAssignmentsPanel.tsx', 'src/components/UpdateConfirmationModal.tsx', 'src/components/UpdateProgressModal.tsx', 'src/components/Workloads/GuestDrawer.tsx', @@ -3140,6 +3142,12 @@ describe('shared primitive guardrails', () => { 'w-full sm:w-auto min-h-10 sm:min-h-9 flex items-center justify-center gap-1.5 px-3 py-2.5 text-sm rounded-md border border-border', ]), }), + expect.objectContaining({ + path: 'src/components/Settings/RolesPanel.tsx', + patterns: expect.arrayContaining([ + 'inline-flex w-full sm:w-auto min-h-10 sm:min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700', + ]), + }), expect.objectContaining({ path: 'src/components/Settings/ReportingPanel.tsx', patterns: expect.arrayContaining([ @@ -3186,6 +3194,12 @@ describe('shared primitive guardrails', () => { 'px-4 py-2 text-sm font-medium bg-red-600 text-white rounded-md hover:bg-red-700', ]), }), + expect.objectContaining({ + path: 'src/components/Settings/UserAssignmentsPanel.tsx', + patterns: expect.arrayContaining([ + 'inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium text-base-content hover:bg-surface-hover transition-colors', + ]), + }), expect.objectContaining({ path: 'src/components/Settings/InfrastructureInstallerSection.tsx', patterns: expect.arrayContaining([ @@ -3593,6 +3607,7 @@ describe('shared primitive guardrails', () => { 'src/components/Alerts/AlertResourceTableMobile.tsx', 'src/components/Alerts/AlertResourceTableRow.tsx', 'src/components/Alerts/ResourceTable.tsx', + 'src/components/Settings/RolesPanel.tsx', 'src/components/Settings/SSOProvidersPanel.tsx', ]); expect(actionIconRule?.forbiddenPatterns).toEqual( @@ -3638,6 +3653,13 @@ describe('shared primitive guardrails', () => { 'class="text-slate-400 hover:text-base-content"', ]), }), + expect.objectContaining({ + path: 'src/components/Settings/RolesPanel.tsx', + patterns: expect.arrayContaining([ + 'p-1.5 rounded-md text-slate-500 hover:text-blue-600 hover:bg-surface-hover dark:hover:text-blue-300', + 'p-1.5 rounded-md text-slate-500 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900', + ]), + }), ]), ); expect(alertResourceActionGuard?.canonical?.path).toBe('src/components/shared/Button.tsx'); @@ -3911,6 +3933,20 @@ describe('shared primitive guardrails', () => { expect(resourcePickerSource).not.toContain( 'w-full sm:w-auto min-h-10 sm:min-h-9 flex items-center justify-center gap-1.5 px-3 py-2.5 text-sm rounded-md border border-border', ); + expect(rolesPanelSource).toContain('@/components/shared/Button'); + expect(rolesPanelSource).toContain(' { expect(securityAuthPanelSource).not.toContain( 'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors', ); + expect(userAssignmentsPanelSource).toContain('@/components/shared/Button'); + expect(userAssignmentsPanelSource).toContain('