Skip to content

Commit

Permalink
RBAC: Display groups for custom roles (grafana#54020)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
kalleep and Clarity-89 authored Aug 22, 2022
1 parent f91f05f commit ef25d29
Showing 1 changed file with 119 additions and 82 deletions.
201 changes: 119 additions & 82 deletions public/app/core/components/RolePicker/RolePickerMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
};

Expand All @@ -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)));
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
};

Expand Down Expand Up @@ -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>
</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>
{!!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>
)}
{!!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>
)}
Expand All @@ -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 = [];
Expand All @@ -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 {
Expand Down Expand Up @@ -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 => {
Expand Down

0 comments on commit ef25d29

Please sign in to comment.