mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: disable custom roles as possible default org roles (#26461)
This commit is contained in:
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,6 +19,9 @@ type RoleSelectorProps = {
|
||||
additionalImpliedRoles?: AssignableRoles[];
|
||||
selectedRoles: Set<string>;
|
||||
onChange: (roles: Set<string>) => 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<RoleSelectorProps> = ({
|
||||
@@ -29,6 +32,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
additionalImpliedRoles = [],
|
||||
selectedRoles,
|
||||
onChange,
|
||||
disabledReason,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -82,6 +86,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
advancedRoles={advancedRoles}
|
||||
selectedRoles={selectedRoles}
|
||||
handleToggle={handleToggle}
|
||||
disabledReason={disabledReason}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -95,6 +100,7 @@ type RoleSelectorListProps = {
|
||||
advancedRoles: AssignableRoles[];
|
||||
selectedRoles: Set<string>;
|
||||
handleToggle: (roleName: string) => void;
|
||||
disabledReason?: (role: AssignableRoles) => string | undefined;
|
||||
};
|
||||
|
||||
const RoleSelectorList: React.FC<RoleSelectorListProps> = ({
|
||||
@@ -102,6 +108,7 @@ const RoleSelectorList: React.FC<RoleSelectorListProps> = ({
|
||||
advancedRoles,
|
||||
selectedRoles,
|
||||
handleToggle,
|
||||
disabledReason,
|
||||
}) => {
|
||||
return (
|
||||
<div className="border border-border border-solid rounded-md overflow-y-auto max-h-72 p-3 flex flex-col gap-2">
|
||||
@@ -111,6 +118,7 @@ const RoleSelectorList: React.FC<RoleSelectorListProps> = ({
|
||||
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<RoleSelectorListProps> = ({
|
||||
role={role}
|
||||
selected={selectedRoles.has(role.name)}
|
||||
onToggle={() => handleToggle(role.name)}
|
||||
disabledReason={disabledReason?.(role)}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSummary>
|
||||
@@ -133,29 +142,32 @@ type RoleCheckboxProps = {
|
||||
role: AssignableRoles;
|
||||
selected: boolean;
|
||||
onToggle: () => void;
|
||||
disabledReason?: string;
|
||||
};
|
||||
|
||||
const RoleCheckbox: React.FC<RoleCheckboxProps> = ({
|
||||
role,
|
||||
selected,
|
||||
onToggle,
|
||||
disabledReason,
|
||||
}) => {
|
||||
const checkboxId = useId();
|
||||
const isDisabled = !role.assignable || !!disabledReason;
|
||||
|
||||
return (
|
||||
const label = (
|
||||
<label
|
||||
key={role.name}
|
||||
htmlFor={checkboxId}
|
||||
className={cn(
|
||||
"flex items-start gap-2",
|
||||
role.assignable ? "cursor-pointer" : "cursor-not-allowed opacity-50",
|
||||
isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
checked={selected}
|
||||
onCheckedChange={onToggle}
|
||||
disabled={!role.assignable}
|
||||
disabled={isDisabled}
|
||||
className="mt-1 shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
@@ -168,6 +180,13 @@ const RoleCheckbox: React.FC<RoleCheckboxProps> = ({
|
||||
</div>
|
||||
</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 <span title={disabledReason}>{label}</span>;
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
type RoleSelectorLayoutProps = {
|
||||
|
||||
@@ -85,6 +85,11 @@ const ActiveDefaultRolesDialog: FC<ActiveProps> = ({
|
||||
availableRoles={availableRoles}
|
||||
selectedRoles={selected}
|
||||
onChange={setSelected}
|
||||
disabledReason={(role) =>
|
||||
role.built_in
|
||||
? undefined
|
||||
: "Only built-in roles are supported as organization default roles"
|
||||
}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogActions
|
||||
|
||||
Reference in New Issue
Block a user