mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Converge RBAC settings actions on shared Button primitives
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 = () => {
|
||||
<SettingsPanel
|
||||
title="Roles"
|
||||
action={
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="primary"
|
||||
size="settingsAction"
|
||||
class="w-full gap-2 sm:w-auto"
|
||||
onClick={state.openCreateRole}
|
||||
disabled={!state.featureGate.rbacEnabled()}
|
||||
class="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"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
New Role
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
noPadding
|
||||
bodyClass="divide-y divide-border"
|
||||
@@ -87,22 +89,22 @@ export const RolesPanel: Component = () => {
|
||||
render: (role) => (
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<Show when={!role.isBuiltIn}>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
label="Edit role"
|
||||
tone="accent"
|
||||
size="sm"
|
||||
onClick={() => state.openEditRole(role)}
|
||||
class="p-1.5 rounded-md text-slate-500 hover:text-blue-600 hover:bg-surface-hover dark:hover:text-blue-300"
|
||||
title="Edit role"
|
||||
>
|
||||
<Pencil class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</ActionIconButton>
|
||||
<ActionIconButton
|
||||
label="Delete role"
|
||||
tone="danger"
|
||||
size="sm"
|
||||
onClick={() => state.handleDeleteRole(role)}
|
||||
class="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"
|
||||
title="Delete role"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</Show>
|
||||
<Show when={role.isBuiltIn}>
|
||||
<span class="text-xs text-slate-400 italic">Read-only</span>
|
||||
|
||||
@@ -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) => (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="settingsAction"
|
||||
class="gap-2"
|
||||
onClick={() => state.openManageAccess(assignment)}
|
||||
class="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"
|
||||
>
|
||||
<Pencil class="w-4 h-4" />
|
||||
Manage Access
|
||||
</button>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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('<Button');
|
||||
expect(rolesPanelSource).toContain('ActionIconButton');
|
||||
expect(rolesPanelSource).toContain('variant="primary"');
|
||||
expect(rolesPanelSource).toContain('size="settingsAction"');
|
||||
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 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700',
|
||||
);
|
||||
expect(rolesPanelSource).not.toContain(
|
||||
'p-1.5 rounded-md text-slate-500 hover:text-blue-600 hover:bg-surface-hover dark:hover:text-blue-300',
|
||||
);
|
||||
expect(rolesPanelSource).not.toContain(
|
||||
'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(reportingPanelSource).toContain('@/components/shared/Button');
|
||||
expect(reportingPanelSource).toContain('<Button');
|
||||
expect(reportingPanelSource).toContain('variant="success"');
|
||||
@@ -3979,6 +4015,13 @@ describe('shared primitive 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 text-base-content rounded-md hover:bg-surface-hover transition-colors',
|
||||
);
|
||||
expect(userAssignmentsPanelSource).toContain('@/components/shared/Button');
|
||||
expect(userAssignmentsPanelSource).toContain('<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 text-sm font-medium text-base-content hover:bg-surface-hover transition-colors',
|
||||
);
|
||||
expect(discoveryTabSource).toContain('@/components/shared/Button');
|
||||
expect(discoveryTabSource).toContain('@/components/shared/CopyableCodeRow');
|
||||
expect(discoveryTabSource).toContain('CopyValueButton');
|
||||
|
||||
Reference in New Issue
Block a user