mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user