diff --git a/site/src/modules/roles/RoleSelector.stories.tsx b/site/src/modules/roles/RoleSelector.stories.tsx index c31f75444d..3f14e7fb0a 100644 --- a/site/src/modules/roles/RoleSelector.stories.tsx +++ b/site/src/modules/roles/RoleSelector.stories.tsx @@ -99,3 +99,21 @@ export const WithAdditionalImpliedRoles: Story = { ], }, }; + +const mixedBuiltInAndCustomRoles = [ + assignableRole(MockOrganizationAdminRole, true), + assignableRole(MockOrganizationUserAdminRole, true), + assignableRole(MockOrganizationTemplateAdminRole, true), + { ...assignableRole(MockOrganizationAuditorRole, true), built_in: false }, + { ...assignableRole(MockAgentsAccessRole, true), built_in: false }, +]; + +export const WithDisabledReason: Story = { + args: { + availableRoles: mixedBuiltInAndCustomRoles, + disabledReason: (role) => + role.built_in + ? undefined + : "Only built-in roles are supported as organization default roles", + }, +}; diff --git a/site/src/modules/roles/RoleSelector.tsx b/site/src/modules/roles/RoleSelector.tsx index 82ecb7420d..c80cd31cf7 100644 --- a/site/src/modules/roles/RoleSelector.tsx +++ b/site/src/modules/roles/RoleSelector.tsx @@ -19,6 +19,9 @@ type RoleSelectorProps = { additionalImpliedRoles?: AssignableRoles[]; selectedRoles: Set; onChange: (roles: Set) => void; + /** When provided, called for each role. Return a tooltip string to + * force-disable the role, or undefined to leave it enabled. */ + disabledReason?: (role: AssignableRoles) => string | undefined; }; export const RoleSelector: FC = ({ @@ -29,6 +32,7 @@ export const RoleSelector: FC = ({ additionalImpliedRoles = [], selectedRoles, onChange, + disabledReason, }) => { if (loading) { return ( @@ -82,6 +86,7 @@ export const RoleSelector: FC = ({ advancedRoles={advancedRoles} selectedRoles={selectedRoles} handleToggle={handleToggle} + disabledReason={disabledReason} /> )} @@ -95,6 +100,7 @@ type RoleSelectorListProps = { advancedRoles: AssignableRoles[]; selectedRoles: Set; handleToggle: (roleName: string) => void; + disabledReason?: (role: AssignableRoles) => string | undefined; }; const RoleSelectorList: React.FC = ({ @@ -102,6 +108,7 @@ const RoleSelectorList: React.FC = ({ advancedRoles, selectedRoles, handleToggle, + disabledReason, }) => { return (
@@ -111,6 +118,7 @@ const RoleSelectorList: React.FC = ({ role={role} selected={selectedRoles.has(role.name)} onToggle={() => handleToggle(role.name)} + disabledReason={disabledReason?.(role)} /> ))} {advancedRoles.length > 0 && ( @@ -121,6 +129,7 @@ const RoleSelectorList: React.FC = ({ role={role} selected={selectedRoles.has(role.name)} onToggle={() => handleToggle(role.name)} + disabledReason={disabledReason?.(role)} /> ))} @@ -133,29 +142,32 @@ type RoleCheckboxProps = { role: AssignableRoles; selected: boolean; onToggle: () => void; + disabledReason?: string; }; const RoleCheckbox: React.FC = ({ role, selected, onToggle, + disabledReason, }) => { const checkboxId = useId(); + const isDisabled = !role.assignable || !!disabledReason; - return ( + const label = ( ); + + // Disabled checkboxes apply pointer-events:none which suppresses native + // title tooltips. Wrapping in a span keeps the tooltip reachable on hover. + if (disabledReason) { + return {label}; + } + return label; }; type RoleSelectorLayoutProps = { diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/DefaultRolesDialog.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/DefaultRolesDialog.tsx index 86f89ba295..d4c3be5df7 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/DefaultRolesDialog.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/DefaultRolesDialog.tsx @@ -85,6 +85,11 @@ const ActiveDefaultRolesDialog: FC = ({ availableRoles={availableRoles} selectedRoles={selected} onChange={setSelected} + disabledReason={(role) => + role.built_in + ? undefined + : "Only built-in roles are supported as organization default roles" + } />