RBAC: Display groups for custom roles (#54020)

* RolePicker: Default to "Other" for roles without group

* RolePicker: Add GroupType enum and calculate group options based on
group type

* RolePicker: Display groups for custom roles

* RolePicker: Remove unused code

* RolePicker: Restructure

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Karl Persson 2022-08-22 14:21:12 +02:00 committed by GitHub
parent f91f05f32c
commit ef25d297d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -19,6 +19,11 @@ import { OrgRole, Role } from 'app/types';
import { MENU_MAX_HEIGHT } from './constants';
enum GroupType {
fixed = 'fixed',
custom = 'custom',
}
const BasicRoles = Object.values(OrgRole);
const BasicRoleOption: Array<SelectableValue<OrgRole>> = BasicRoles.map((r) => ({
label: r,
@ -94,15 +99,15 @@ export const RolePickerMenu = ({
return selectedGroupOptions;
};
const groupSelected = (group: string) => {
const groupSelected = (groupType: GroupType, group: string) => {
const selectedGroupOptions = getSelectedGroupOptions(group);
const groupOptions = optionGroups.find((g) => g.value === group);
const groupOptions = optionGroups[groupType].find((g) => g.value === group);
return selectedGroupOptions.length > 0 && selectedGroupOptions.length >= groupOptions!.options.length;
};
const groupPartiallySelected = (group: string) => {
const groupPartiallySelected = (groupType: GroupType, group: string) => {
const selectedGroupOptions = getSelectedGroupOptions(group);
const groupOptions = optionGroups.find((g) => g.value === group);
const groupOptions = optionGroups[groupType].find((g) => g.value === group);
return selectedGroupOptions.length > 0 && selectedGroupOptions.length < groupOptions!.options.length;
};
@ -114,11 +119,11 @@ export const RolePickerMenu = ({
}
};
const onGroupChange = (value: string) => {
const group = optionGroups.find((g) => {
const onGroupChange = (groupType: GroupType, value: string) => {
const group = optionGroups[groupType].find((g) => {
return g.value === value;
});
if (groupSelected(value) || groupPartiallySelected(value)) {
if (groupSelected(groupType, value) || groupPartiallySelected(groupType, value)) {
if (group) {
setSelectedOptions(selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)));
}
@ -131,10 +136,10 @@ export const RolePickerMenu = ({
}
};
const onOpenSubMenu = (value: string) => {
const onOpenSubMenu = (groupType: GroupType, value: string) => {
setOpenedMenuGroup(value);
setShowSubMenu(true);
const group = optionGroups.find((g) => {
const group = optionGroups[groupType].find((g) => {
return g.value === value;
});
if (group) {
@ -165,12 +170,6 @@ export const RolePickerMenu = ({
};
const onUpdateInternal = () => {
const selectedCustomRoles: string[] = [];
// TODO: needed?
for (const key in selectedOptions) {
const roleUID = selectedOptions[key]?.uid;
selectedCustomRoles.push(roleUID);
}
onUpdate(selectedOptions, selectedBuiltInRole);
};
@ -201,68 +200,93 @@ export const RolePickerMenu = ({
/>
</div>
)}
{!!fixedRoles.length &&
(showGroups && !!optionGroups.length ? (
<div className={customStyles.menuSection}>
<div className={customStyles.groupHeader}>Fixed roles</div>
<div className={styles.optionBody}>
{optionGroups.map((option, i) => (
<RoleMenuGroupOption
data={option}
key={i}
isSelected={groupSelected(option.value) || groupPartiallySelected(option.value)}
partiallySelected={groupPartiallySelected(option.value)}
disabled={option.options?.every(isNotDelegatable)}
onChange={onGroupChange}
onOpenSubMenu={onOpenSubMenu}
onCloseSubMenu={onCloseSubMenu}
root={subMenuNode?.current!}
isFocused={showSubMenu && openedMenuGroup === option.value}
>
{showSubMenu && openedMenuGroup === option.value && (
<RolePickerSubMenu
options={subMenuOptions}
selectedOptions={selectedOptions}
onSelect={onChange}
onClear={onClearSubMenu}
showOnLeft={offset.horizontal > 0}
/>
)}
</RoleMenuGroupOption>
))}
</div>
{!!fixedRoles.length && (
<div className={customStyles.menuSection}>
<div className={customStyles.groupHeader}>Fixed roles</div>
<div className={styles.optionBody}>
{showGroups && !!optionGroups.fixed.length
? optionGroups.fixed.map((option, i) => (
<RoleMenuGroupOption
data={option}
key={i}
isSelected={
groupSelected(GroupType.fixed, option.value) ||
groupPartiallySelected(GroupType.fixed, option.value)
}
partiallySelected={groupPartiallySelected(GroupType.fixed, option.value)}
disabled={option.options?.every(isNotDelegatable)}
onChange={(group: string) => onGroupChange(GroupType.fixed, group)}
onOpenSubMenu={(group: string) => onOpenSubMenu(GroupType.fixed, group)}
onCloseSubMenu={onCloseSubMenu}
root={subMenuNode?.current!}
isFocused={showSubMenu && openedMenuGroup === option.value}
>
{showSubMenu && openedMenuGroup === option.value && (
<RolePickerSubMenu
options={subMenuOptions}
selectedOptions={selectedOptions}
onSelect={onChange}
onClear={onClearSubMenu}
showOnLeft={offset.horizontal > 0}
/>
)}
</RoleMenuGroupOption>
))
: fixedRoles.map((option, i) => (
<RoleMenuOption
data={option}
key={i}
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
disabled={isNotDelegatable(option)}
onChange={onChange}
hideDescription
/>
))}
</div>
) : (
<div className={customStyles.menuSection}>
<div className={customStyles.groupHeader}>Fixed roles</div>
<div className={styles.optionBody}>
{fixedRoles.map((option, i) => (
<RoleMenuOption
data={option}
key={i}
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
disabled={isNotDelegatable(option)}
onChange={onChange}
hideDescription
/>
))}
</div>
</div>
))}
</div>
)}
{!!customRoles.length && (
<div>
<div className={customStyles.menuSection}>
<div className={customStyles.groupHeader}>Custom roles</div>
<div className={styles.optionBody}>
{customRoles.map((option, i) => (
<RoleMenuOption
data={option}
key={i}
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
disabled={isNotDelegatable(option)}
onChange={onChange}
hideDescription
/>
))}
{showGroups && !!optionGroups.custom.length
? optionGroups.custom.map((option, i) => (
<RoleMenuGroupOption
data={option}
key={i}
isSelected={
groupSelected(GroupType.custom, option.value) ||
groupPartiallySelected(GroupType.custom, option.value)
}
partiallySelected={groupPartiallySelected(GroupType.custom, option.value)}
disabled={option.options?.every(isNotDelegatable)}
onChange={(group: string) => onGroupChange(GroupType.custom, group)}
onOpenSubMenu={(group: string) => onOpenSubMenu(GroupType.custom, group)}
onCloseSubMenu={onCloseSubMenu}
root={subMenuNode?.current!}
isFocused={showSubMenu && openedMenuGroup === option.value}
>
{showSubMenu && openedMenuGroup === option.value && (
<RolePickerSubMenu
options={subMenuOptions}
selectedOptions={selectedOptions}
onSelect={onChange}
onClear={onClearSubMenu}
showOnLeft={offset.horizontal > 0}
/>
)}
</RoleMenuGroupOption>
))
: customRoles.map((option, i) => (
<RoleMenuOption
data={option}
key={i}
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
disabled={isNotDelegatable(option)}
onChange={onChange}
hideDescription
/>
))}
</div>
</div>
)}
@ -288,15 +312,14 @@ const filterFixedRoles = (option: Role) => option.name?.startsWith('fixed:');
const getOptionGroups = (options: Role[]) => {
const groupsMap: { [key: string]: Role[] } = {};
const customGroupsMap: { [key: string]: Role[] } = {};
options.forEach((role) => {
if (role.name.startsWith('fixed:')) {
const groupName = getRoleGroup(role);
if (groupsMap[groupName]) {
groupsMap[groupName].push(role);
} else {
groupsMap[groupName] = [role];
}
const m = role.name.startsWith('fixed:') ? groupsMap : customGroupsMap;
const groupName = getRoleGroup(role);
if (!m[groupName]) {
m[groupName] = [];
}
m[groupName].push(role);
});
const groups = [];
@ -308,7 +331,21 @@ const getOptionGroups = (options: Role[]) => {
options: groupOptions,
});
}
return groups.sort((a, b) => a.name.localeCompare(b.name));
const customGroups = [];
for (const groupName of Object.keys(customGroupsMap)) {
const groupOptions = customGroupsMap[groupName].sort(sortRolesByName);
customGroups.push({
name: capitalize(groupName),
value: groupName,
options: groupOptions,
});
}
return {
fixed: groups.sort((a, b) => a.name.localeCompare(b.name)),
custom: customGroups.sort((a, b) => a.name.localeCompare(b.name)),
};
};
interface RolePickerSubMenuProps {
@ -527,7 +564,7 @@ export const RoleMenuGroupOption = React.forwardRef<HTMLDivElement, RoleMenuGrou
RoleMenuGroupOption.displayName = 'RoleMenuGroupOption';
const getRoleGroup = (role: Role) => {
return role.group ?? 'Other';
return role.group || 'Other';
};
const capitalize = (s: string): string => {