import { css, cx } from '@emotion/css'; import { useEffect, useRef, useState } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, ScrollContainer, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/internal'; import { OrgRole, Role } from 'app/types'; import { BuiltinRoleSelector } from './BuiltinRoleSelector'; import { RoleMenuGroupsSection } from './RoleMenuGroupsSection'; import { MENU_MAX_HEIGHT } from './constants'; import { getStyles } from './styles'; enum GroupType { fixed = 'fixed', custom = 'custom', plugin = 'plugin', } interface RoleGroupOption { name: string; value: string; options: Role[]; } interface RolesCollectionEntry { groupType: GroupType; optionGroup: RoleGroupOption[]; renderedName: string; roles: Role[]; } const fixedRoleGroupNames: Record = { ldap: 'LDAP', current: 'Current org', }; const tooltipMessage = ( You can now select the "No basic role" option and add permissions to your custom needs. You can find more information in  our documentation . ); interface RolePickerMenuProps { basicRole?: OrgRole; options: Role[]; isFiltered?: boolean; appliedRoles: Role[]; showGroups?: boolean; basicRoleDisabled?: boolean; disabledMessage?: string; showBasicRole?: boolean; onSelect: (roles: Role[]) => void; onBasicRoleSelect?: (role: OrgRole) => void; onUpdate: (newRoles: Role[], newBuiltInRole?: OrgRole) => void; updateDisabled?: boolean; apply?: boolean; offset: { vertical: number; horizontal: number }; menuLeft?: boolean; } export const RolePickerMenu = ({ basicRole, options, isFiltered, appliedRoles, showGroups, basicRoleDisabled, disabledMessage, showBasicRole, onSelect, onBasicRoleSelect, onUpdate, updateDisabled, offset, menuLeft, apply, }: RolePickerMenuProps): JSX.Element => { const [selectedOptions, setSelectedOptions] = useState(appliedRoles); const [selectedBuiltInRole, setSelectedBuiltInRole] = useState(basicRole); const [rolesCollection, setRolesCollection] = useState<{ [key: string]: RolesCollectionEntry }>({}); const subMenuNode = useRef(null); const theme = useTheme2(); const styles = getSelectStyles(theme); const customStyles = useStyles2(getStyles); // Call onSelect() on every selectedOptions change useEffect(() => { onSelect(selectedOptions); }, [selectedOptions, onSelect]); useEffect(() => { if (onBasicRoleSelect && selectedBuiltInRole) { onBasicRoleSelect(selectedBuiltInRole); } }, [selectedBuiltInRole, onBasicRoleSelect]); // Evaluate rolesCollection only if options changed, otherwise // it triggers unnecessary re-rendering of component useEffect(() => { const customRoles = options.filter(filterCustomRoles).sort(sortRolesByName); const fixedRoles = options.filter(filterFixedRoles).sort(sortRolesByName); 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)), }; setRolesCollection({ fixed: { groupType: GroupType.fixed, optionGroup: optionGroups.fixed, renderedName: `Fixed roles`, roles: fixedRoles, }, custom: { groupType: GroupType.custom, optionGroup: optionGroups.custom, renderedName: `Custom roles`, roles: customRoles, }, plugin: { groupType: GroupType.plugin, optionGroup: optionGroups.plugin, renderedName: `Plugin roles`, roles: pluginRoles, }, }); }, [options]); const getSelectedGroupOptions = (group: string) => { const selectedGroupOptions = []; for (const role of selectedOptions) { if (getRoleGroup(role) === group) { selectedGroupOptions.push(role); } } return selectedGroupOptions; }; const groupSelected = (groupType: GroupType, group: string) => { const selectedGroupOptions = getSelectedGroupOptions(group); const groupOptions = rolesCollection[groupType]?.optionGroup.find((g) => g.value === group); return selectedGroupOptions.length > 0 && selectedGroupOptions.length >= groupOptions!.options.length; }; const groupPartiallySelected = (groupType: GroupType, group: string) => { const selectedGroupOptions = getSelectedGroupOptions(group); const groupOptions = rolesCollection[groupType]?.optionGroup.find((g) => g.value === group); return selectedGroupOptions.length > 0 && selectedGroupOptions.length < groupOptions!.options.length; }; const changeableGroupRolesSelected = (groupType: GroupType, group: string) => { const selectedGroupOptions = getSelectedGroupOptions(group); const changeableGroupOptions = selectedGroupOptions.filter((role) => role.delegatable && !role.mapped); const groupOptions = rolesCollection[groupType]?.optionGroup.find((g) => g.value === group); return changeableGroupOptions.length > 0 && changeableGroupOptions.length < groupOptions!.options.length; }; const onChange = (option: Role) => { if (selectedOptions.find((role) => role.uid === option.uid && !role.mapped)) { setSelectedOptions(selectedOptions.filter((role) => role.uid !== option.uid)); } else { setSelectedOptions([...selectedOptions, option]); } }; const onGroupChange = (groupType: GroupType, value: string) => { const group = rolesCollection[groupType]?.optionGroup.find((g) => { return g.value === value; }); if (!group) { return; } if (groupSelected(groupType, value) || changeableGroupRolesSelected(groupType, value)) { const mappedGroupOptions = selectedOptions.filter((option) => group.options.find((role) => role.uid === option.uid && option.mapped) ); const restOptions = selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)); setSelectedOptions([...restOptions, ...mappedGroupOptions]); } else { const mappedGroupOptions = selectedOptions.filter((option) => group.options.find((role) => role.uid === option.uid && role.delegatable) ); const groupOptions = group.options.filter( (role) => role.delegatable && !selectedOptions.find((option) => role.uid === option.uid && option.mapped) ); const restOptions = selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)); setSelectedOptions([...restOptions, ...groupOptions, ...mappedGroupOptions]); } }; const onSelectedBuiltinRoleChange = (newRole: OrgRole) => { setSelectedBuiltInRole(newRole); }; const onClearInternal = async () => { const mappedRoles = selectedOptions.filter((role) => role.mapped); const nonDelegatableRoles = options.filter((role) => selectedOptions.find((option) => role.uid === option.uid && !role.delegatable) ); setSelectedOptions([...mappedRoles, ...nonDelegatableRoles]); }; const onClearSubMenu = (group: string) => { const options = selectedOptions.filter((role) => { const roleGroup = getRoleGroup(role); return roleGroup !== group || role.mapped; }); setSelectedOptions(options); }; const onUpdateInternal = () => { onUpdate(selectedOptions, selectedBuiltInRole); }; return (
{showBasicRole && (
)} {Object.entries(rolesCollection).map(([groupId, collection]) => ( groupSelected(collection.groupType, group)} groupPartiallySelected={(group: string) => groupPartiallySelected(collection.groupType, group)} onGroupChange={(group: string) => onGroupChange(collection.groupType, group)} subMenuNode={subMenuNode?.current!} selectedOptions={selectedOptions} onRoleChange={onChange} onClearSubMenu={onClearSubMenu} showOnLeftSubMenu={menuLeft} /> ))}
); }; 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:'); interface GroupsMap { [key: string]: { roles: Role[]; name: string }; } const convertRolesToGroupOptions = (roles: Role[]) => { const groupsMap: GroupsMap = {}; roles.forEach((role) => { const groupId = getRoleGroup(role); const groupName = getRoleGroupName(role); if (!groupsMap[groupId]) { groupsMap[groupId] = { name: groupName, roles: [] }; } groupsMap[groupId].roles.push(role); }); const groups = Object.entries(groupsMap).map(([groupId, groupEntry]) => { return { name: fixedRoleGroupNames[groupId] || capitalize(groupEntry.name), value: groupId, options: groupEntry.roles.sort(sortRolesByName), }; }); return groups; }; const getRoleGroup = (role: Role) => { const prefix = getRolePrefix(role); const name = getRoleGroupName(role); return `${prefix}:${name}`; }; const getRoleGroupName = (role: Role) => { return role.group || 'Other'; }; const getRolePrefix = (role: Role) => { const prefixEnd = role.name.indexOf(':'); if (prefixEnd < 0) { return 'unknown'; } return role.name.substring(0, prefixEnd); }; const sortRolesByName = (a: Role, b: Role) => a.name.localeCompare(b.name); const capitalize = (s: string): string => { return s.slice(0, 1).toUpperCase() + s.slice(1); };