D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
usr
/
share
/
grafana
/
public
/
app
/
core
/
components
/
RolePicker
/
Filename :
RolePickerMenu.tsx
back
Copy
import { css, cx } from '@emotion/css'; import React, { useEffect, useRef, useState } from 'react'; import { Button, CustomScrollbar, HorizontalGroup, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles'; 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<string, string> = { ldap: 'LDAP', current: 'Current org', }; interface RolePickerMenuProps { basicRole?: OrgRole; options: Role[]; 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 }; } export const RolePickerMenu = ({ basicRole, options, appliedRoles, showGroups, basicRoleDisabled, disabledMessage, showBasicRole, onSelect, onBasicRoleSelect, onUpdate, updateDisabled, offset, apply, }: RolePickerMenuProps): JSX.Element => { const [selectedOptions, setSelectedOptions] = useState<Role[]>(appliedRoles); const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(basicRole); const [rolesCollection, setRolesCollection] = useState<{ [key: string]: RolesCollectionEntry }>({}); const subMenuNode = useRef<HTMLDivElement | null>(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 <RoleMenuGroupsSection /> 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 onChange = (option: Role) => { if (selectedOptions.find((role) => role.uid === option.uid)) { 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) || groupPartiallySelected(groupType, value)) { setSelectedOptions(selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid))); } else { 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 onSelectedBuiltinRoleChange = (newRole: OrgRole) => { setSelectedBuiltInRole(newRole); }; const onClearInternal = async () => { setSelectedOptions([]); }; const onClearSubMenu = (group: string) => { const options = selectedOptions.filter((role) => { const roleGroup = getRoleGroup(role); return roleGroup !== group; }); setSelectedOptions(options); }; const onUpdateInternal = () => { onUpdate(selectedOptions, selectedBuiltInRole); }; return ( <div className={cx( styles.menu, customStyles.menuWrapper, { [customStyles.menuLeft]: offset.horizontal > 0 }, css` bottom: ${offset.vertical > 0 ? `${offset.vertical}px` : 'unset'}; top: ${offset.vertical < 0 ? `${Math.abs(offset.vertical)}px` : 'unset'}; ` )} > <div className={customStyles.menu} aria-label="Role picker menu"> <CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack hideVerticalTrack> {showBasicRole && ( <div className={customStyles.menuSection}> <BuiltinRoleSelector value={selectedBuiltInRole} onChange={onSelectedBuiltinRoleChange} disabled={basicRoleDisabled} disabledMesssage={disabledMessage} /> </div> )} {Object.entries(rolesCollection).map(([groupId, collection]) => ( <RoleMenuGroupsSection key={groupId} roles={collection.roles} renderedName={collection.renderedName} showGroups={showGroups} optionGroups={collection.optionGroup} groupSelected={(group: string) => 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={offset.horizontal > 0} /> ))} </CustomScrollbar> <div className={customStyles.menuButtonRow}> <HorizontalGroup justify="flex-end"> <Button size="sm" fill="text" onClick={onClearInternal} disabled={updateDisabled}> Clear all </Button> <Button size="sm" onClick={onUpdateInternal} disabled={updateDisabled}> {apply ? `Apply` : `Update`} </Button> </HorizontalGroup> </div> </div> <div ref={subMenuNode} /> </div> ); }; 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); };