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

View File

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