From 25d7c591cf484e275dac5fe2b17c1113c49f2b77 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Fri, 16 Dec 2022 14:41:09 +0100 Subject: [PATCH] Auth: Add plugin roles to RolePicker (#59667) * Add `RoleMenuGroupsSection` component * Add plugin roles data structures * Use `RoleMenuGroupsSection` for rendering roles Co-authored-by: Alexander Zobnin --- .../components/RolePicker/RolePickerMenu.tsx | 314 ++++++++++-------- 1 file changed, 184 insertions(+), 130 deletions(-) diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index 16b6236c8e0..43cfc5b75a0 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import React, { FormEvent, useEffect, useRef, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { @@ -22,6 +23,7 @@ import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH } from './constants'; enum GroupType { fixed = 'fixed', custom = 'custom', + plugin = 'plugin', } const BasicRoles = Object.values(OrgRole); @@ -87,7 +89,33 @@ export const RolePickerMenu = ({ const customRoles = options.filter(filterCustomRoles).sort(sortRolesByName); const fixedRoles = options.filter(filterFixedRoles).sort(sortRolesByName); - const optionGroups = getOptionGroups(options); + const pluginRoles = options.filter(filterPluginsRoles).sort(sortRolesByName); + const optionGroups = { + fixed: convertRolesToGroupOptions(fixedRoles).sort((a, b) => a.name.localeCompare(b.name)), + custom: convertRolesToGroupOptions(customRoles).sort((a, b) => a.name.localeCompare(b.name)), + plugin: convertRolesToGroupOptions(pluginRoles).sort((a, b) => a.name.localeCompare(b.name)), + }; + + const rolesCollection = { + fixed: { + groupType: GroupType.fixed, + optionGroup: optionGroups.fixed, + renderedName: `Fixed roles`, + roles: fixedRoles, + }, + custom: { + groupType: GroupType.custom, + optionGroup: optionGroups.custom, + renderedName: `Custom roles`, + roles: customRoles, + }, + pluginRoles: { + groupType: GroupType.plugin, + optionGroup: optionGroups.plugin, + renderedName: `Plugin roles`, + roles: pluginRoles, + }, + }; const getSelectedGroupOptions = (group: string) => { const selectedGroupOptions = []; @@ -123,16 +151,17 @@ export const RolePickerMenu = ({ const group = optionGroups[groupType].find((g) => { return g.value === value; }); + + if (!group) { + return; + } + if (groupSelected(groupType, value) || groupPartiallySelected(groupType, value)) { - 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))); } else { - if (group) { - const groupOptions = group.options.filter((role) => role.delegatable); - const restOptions = selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)); - setSelectedOptions([...restOptions, ...groupOptions]); - } + const groupOptions = group.options.filter((role) => role.delegatable); + const restOptions = selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)); + setSelectedOptions([...restOptions, ...groupOptions]); } }; @@ -200,96 +229,33 @@ export const RolePickerMenu = ({ /> )} - {!!fixedRoles.length && ( -
-
Fixed roles
-
- {showGroups && !!optionGroups.fixed.length - ? optionGroups.fixed.map((option, i) => ( - 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 && ( - 0} - /> - )} - - )) - : fixedRoles.map((option, i) => ( - opt.uid === option.uid))} - disabled={isNotDelegatable(option)} - onChange={onChange} - hideDescription - /> - ))} -
-
- )} - {!!customRoles.length && ( -
-
Custom roles
-
- {showGroups && !!optionGroups.custom.length - ? optionGroups.custom.map((option, i) => ( - 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 && ( - 0} - /> - )} - - )) - : customRoles.map((option, i) => ( - opt.uid === option.uid))} - disabled={isNotDelegatable(option)} - onChange={onChange} - hideDescription - /> - ))} -
-
- )} + {Object.entries(rolesCollection).map(([groupId, collection]) => { + return ( + groupSelected(collection.groupType, group)} + groupPartiallySelected={(group: string) => groupPartiallySelected(collection.groupType, group)} + onChange={(group: string) => onGroupChange(collection.groupType, group)} + onOpenSubMenuRMGS={(group: string) => onOpenSubMenu(collection.groupType, group)} + onCloseSubMenu={onCloseSubMenu} + subMenuNode={subMenuNode?.current!} + showSubMenu={showSubMenu} + openedMenuGroup={openedMenuGroup} + subMenuOptions={subMenuOptions} + selectedOptions={selectedOptions} + onChangeSubMenu={onChange} + onClearSubMenu={onClearSubMenu} + showOnLeftSubMenu={offset.horizontal > 0} + > + ); + })}
@@ -307,45 +273,28 @@ export const RolePickerMenu = ({ ); }; -const filterCustomRoles = (option: Role) => !option.name?.startsWith('fixed:'); +const filterCustomRoles = (option: Role) => !option.name?.startsWith('fixed:') && !option.name.startsWith('plugins:'); const filterFixedRoles = (option: Role) => option.name?.startsWith('fixed:'); +const filterPluginsRoles = (option: Role) => option.name?.startsWith('plugins:'); -const getOptionGroups = (options: Role[]) => { +const convertRolesToGroupOptions = (roles: Role[]) => { const groupsMap: { [key: string]: Role[] } = {}; - const customGroupsMap: { [key: string]: Role[] } = {}; - options.forEach((role) => { - const m = role.name.startsWith('fixed:') ? groupsMap : customGroupsMap; + roles.forEach((role) => { const groupName = getRoleGroup(role); - if (!m[groupName]) { - m[groupName] = []; + if (!groupsMap[groupName]) { + groupsMap[groupName] = []; } - m[groupName].push(role); + groupsMap[groupName].push(role); }); - - const groups = []; - for (const groupName of Object.keys(groupsMap)) { - const groupOptions = groupsMap[groupName].sort(sortRolesByName); - groups.push({ + const groups = Object.entries(groupsMap).map(([groupName, roles]) => { + return { name: fixedRoleGroupNames[groupName] || capitalize(groupName), value: groupName, - options: groupOptions, - }); - } - - 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)), - }; + options: roles.sort(sortRolesByName), + uid: uuidv4(), + }; + }); + return groups; }; interface RolePickerSubMenuProps { @@ -467,6 +416,111 @@ export const RoleMenuOption = React.forwardRef; + onChange: (value: string) => void; + onOpenSubMenuRMGS: (value: string) => void; + onCloseSubMenu?: (value: string) => void; + groupSelected: (group: string) => boolean; + groupPartiallySelected: (group: string) => boolean; + disabled?: boolean; + subMenuNode?: HTMLDivElement; + showSubMenu: boolean; + openedMenuGroup: string; + subMenuOptions: Role[]; + selectedOptions: Role[]; + onChangeSubMenu: (option: Role) => void; + onClearSubMenu: () => void; + showOnLeftSubMenu: boolean; +} + +export const RoleMenuGroupsSection = React.forwardRef( + ( + { + roles, + renderedName, + menuSectionStyle, + groupHeaderStyle, + optionBodyStyle, + showGroups, + optionGroups, + onChange, + groupSelected, + groupPartiallySelected, + onOpenSubMenuRMGS, + onCloseSubMenu, + subMenuNode, + showSubMenu, + openedMenuGroup, + subMenuOptions, + selectedOptions, + onChangeSubMenu, + onClearSubMenu, + showOnLeftSubMenu, + }, + _ref + ) => { + return ( +
+ {roles.length > 0 && ( +
+
{renderedName}
+
+ {showGroups && !!optionGroups?.length + ? optionGroups.map((option) => ( + + {showSubMenu && openedMenuGroup === option.value && ( + + )} + + )) + : roles.map((option) => ( + opt.uid === option.uid))} + disabled={isNotDelegatable(option)} + onChange={onChangeSubMenu} + hideDescription + /> + ))} +
+ )} +
+ ); + } +); + +RoleMenuGroupsSection.displayName = 'RoleMenuGroupsSection'; + interface RoleMenuGroupsOptionProps { data: SelectableValue; onChange: (value: string) => void;