mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Role picker: Split components into separate files (#60519)
This commit is contained in:
parent
35090c376c
commit
6e2b148745
104
public/app/core/components/RolePicker/RoleMenuGroupOption.tsx
Normal file
104
public/app/core/components/RolePicker/RoleMenuGroupOption.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import React, { FormEvent } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Checkbox, Portal, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles';
|
||||
|
||||
import { getStyles } from './styles';
|
||||
|
||||
interface RoleMenuGroupsOptionProps {
|
||||
data: SelectableValue<string>;
|
||||
onChange: (value: string) => void;
|
||||
onClick?: (value: string) => void;
|
||||
onOpenSubMenu?: (value: string) => void;
|
||||
onCloseSubMenu?: (value: string) => void;
|
||||
isSelected?: boolean;
|
||||
partiallySelected?: boolean;
|
||||
isFocused?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
root?: HTMLElement;
|
||||
}
|
||||
|
||||
export const RoleMenuGroupOption = React.forwardRef<HTMLDivElement, RoleMenuGroupsOptionProps>(
|
||||
(
|
||||
{
|
||||
data,
|
||||
isFocused,
|
||||
isSelected,
|
||||
partiallySelected,
|
||||
disabled,
|
||||
onChange,
|
||||
onClick,
|
||||
onOpenSubMenu,
|
||||
onCloseSubMenu,
|
||||
children,
|
||||
root,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
const customStyles = useStyles2(getStyles);
|
||||
|
||||
const wrapperClassName = cx(
|
||||
styles.option,
|
||||
isFocused && styles.optionFocused,
|
||||
disabled && customStyles.menuOptionDisabled
|
||||
);
|
||||
|
||||
const onChangeInternal = (event: FormEvent<HTMLElement>) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (data.value) {
|
||||
onChange(data.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickInternal = (event: FormEvent<HTMLElement>) => {
|
||||
if (onClick) {
|
||||
onClick(data.value!);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (onOpenSubMenu) {
|
||||
onOpenSubMenu(data.value!);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (onCloseSubMenu) {
|
||||
onCloseSubMenu(data.value!);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<div ref={ref} className={wrapperClassName} aria-label="Role picker option" onClick={onClickInternal}>
|
||||
<Checkbox
|
||||
value={isSelected}
|
||||
className={cx(customStyles.menuOptionCheckbox, {
|
||||
[customStyles.checkboxPartiallyChecked]: partiallySelected,
|
||||
})}
|
||||
onChange={onChangeInternal}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
|
||||
<span>{data.displayName || data.name}</span>
|
||||
<span className={customStyles.menuOptionExpand} />
|
||||
</div>
|
||||
{root && children && (
|
||||
<Portal className={customStyles.subMenuPortal} root={root}>
|
||||
{children}
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RoleMenuGroupOption.displayName = 'RoleMenuGroupOption';
|
113
public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx
Normal file
113
public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Role } from 'app/types';
|
||||
|
||||
import { RoleMenuGroupOption } from './RoleMenuGroupOption';
|
||||
import { RoleMenuOption } from './RoleMenuOption';
|
||||
import { RolePickerSubMenu } from './RolePickerSubMenu';
|
||||
import { isNotDelegatable } from './utils';
|
||||
|
||||
interface RoleMenuGroupsSectionProps {
|
||||
roles: Role[];
|
||||
renderedName: string;
|
||||
menuSectionStyle: string;
|
||||
groupHeaderStyle: string;
|
||||
optionBodyStyle: string;
|
||||
showGroups?: boolean;
|
||||
optionGroups: Array<{
|
||||
name: string;
|
||||
options: Role[];
|
||||
value: string;
|
||||
uid: string;
|
||||
}>;
|
||||
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<HTMLDivElement, RoleMenuGroupsSectionProps>(
|
||||
(
|
||||
{
|
||||
roles,
|
||||
renderedName,
|
||||
menuSectionStyle,
|
||||
groupHeaderStyle,
|
||||
optionBodyStyle,
|
||||
showGroups,
|
||||
optionGroups,
|
||||
onChange,
|
||||
groupSelected,
|
||||
groupPartiallySelected,
|
||||
onOpenSubMenuRMGS,
|
||||
onCloseSubMenu,
|
||||
subMenuNode,
|
||||
showSubMenu,
|
||||
openedMenuGroup,
|
||||
subMenuOptions,
|
||||
selectedOptions,
|
||||
onChangeSubMenu,
|
||||
onClearSubMenu,
|
||||
showOnLeftSubMenu,
|
||||
},
|
||||
_ref
|
||||
) => {
|
||||
return (
|
||||
<div>
|
||||
{roles.length > 0 && (
|
||||
<div className={menuSectionStyle}>
|
||||
<div className={groupHeaderStyle}>{renderedName}</div>
|
||||
<div className={optionBodyStyle}></div>
|
||||
{showGroups && !!optionGroups?.length
|
||||
? optionGroups.map((option) => (
|
||||
<RoleMenuGroupOption
|
||||
data={option}
|
||||
key={option.value}
|
||||
isSelected={groupSelected(option.value) || groupPartiallySelected(option.value)}
|
||||
partiallySelected={groupPartiallySelected(option.value)}
|
||||
disabled={option.options?.every(isNotDelegatable)}
|
||||
onChange={onChange}
|
||||
onOpenSubMenu={onOpenSubMenuRMGS}
|
||||
onCloseSubMenu={onCloseSubMenu}
|
||||
root={subMenuNode}
|
||||
isFocused={showSubMenu && openedMenuGroup === option.value}
|
||||
>
|
||||
{showSubMenu && openedMenuGroup === option.value && (
|
||||
<RolePickerSubMenu
|
||||
options={subMenuOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelect={onChangeSubMenu}
|
||||
onClear={onClearSubMenu}
|
||||
showOnLeft={showOnLeftSubMenu}
|
||||
/>
|
||||
)}
|
||||
</RoleMenuGroupOption>
|
||||
))
|
||||
: roles.map((option) => (
|
||||
<RoleMenuOption
|
||||
data={option}
|
||||
key={option.uid}
|
||||
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
|
||||
disabled={isNotDelegatable(option)}
|
||||
onChange={onChangeSubMenu}
|
||||
hideDescription
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RoleMenuGroupsSection.displayName = 'RoleMenuGroupsSection';
|
62
public/app/core/components/RolePicker/RoleMenuOption.tsx
Normal file
62
public/app/core/components/RolePicker/RoleMenuOption.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import React, { FormEvent } from 'react';
|
||||
|
||||
import { Checkbox, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles';
|
||||
import { Role } from 'app/types';
|
||||
|
||||
import { getStyles } from './styles';
|
||||
|
||||
interface RoleMenuOptionProps {
|
||||
data: Role;
|
||||
onChange: (value: Role) => void;
|
||||
isSelected?: boolean;
|
||||
isFocused?: boolean;
|
||||
disabled?: boolean;
|
||||
hideDescription?: boolean;
|
||||
}
|
||||
|
||||
export const RoleMenuOption = React.forwardRef<HTMLDivElement, React.PropsWithChildren<RoleMenuOptionProps>>(
|
||||
({ data, isFocused, isSelected, disabled, onChange, hideDescription }, ref) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
const customStyles = useStyles2(getStyles);
|
||||
|
||||
const wrapperClassName = cx(
|
||||
styles.option,
|
||||
isFocused && styles.optionFocused,
|
||||
disabled && customStyles.menuOptionDisabled
|
||||
);
|
||||
|
||||
const onChangeInternal = (event: FormEvent<HTMLElement>) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onChange(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className={wrapperClassName} aria-label="Role picker option" onClick={onChangeInternal}>
|
||||
<Checkbox
|
||||
value={isSelected}
|
||||
className={customStyles.menuOptionCheckbox}
|
||||
onChange={onChangeInternal}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
|
||||
<span>{data.displayName || data.name}</span>
|
||||
{!hideDescription && data.description && <div className={styles.optionDescription}>{data.description}</div>}
|
||||
</div>
|
||||
{data.description && (
|
||||
<Tooltip content={data.description}>
|
||||
<Icon name="info-circle" className={customStyles.menuOptionInfoSign} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RoleMenuOption.displayName = 'RoleMenuOption';
|
@ -1,24 +1,15 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { FormEvent, useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CustomScrollbar,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
Portal,
|
||||
RadioButtonGroup,
|
||||
Tooltip,
|
||||
useStyles2,
|
||||
useTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, CustomScrollbar, HorizontalGroup, RadioButtonGroup, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles';
|
||||
import { OrgRole, Role } from 'app/types';
|
||||
|
||||
import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH } from './constants';
|
||||
import { RoleMenuGroupsSection } from './RoleMenuGroupsSection';
|
||||
import { MENU_MAX_HEIGHT } from './constants';
|
||||
import { getStyles } from './styles';
|
||||
|
||||
enum GroupType {
|
||||
fixed = 'fixed',
|
||||
@ -297,443 +288,12 @@ const convertRolesToGroupOptions = (roles: Role[]) => {
|
||||
return groups;
|
||||
};
|
||||
|
||||
interface RolePickerSubMenuProps {
|
||||
options: Role[];
|
||||
selectedOptions: Role[];
|
||||
disabledOptions?: Role[];
|
||||
onSelect: (option: Role) => void;
|
||||
onClear?: () => void;
|
||||
showOnLeft?: boolean;
|
||||
}
|
||||
|
||||
export const RolePickerSubMenu = ({
|
||||
options,
|
||||
selectedOptions,
|
||||
disabledOptions,
|
||||
onSelect,
|
||||
onClear,
|
||||
showOnLeft,
|
||||
}: RolePickerSubMenuProps): JSX.Element => {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
const customStyles = useStyles2(getStyles);
|
||||
|
||||
const onClearInternal = async () => {
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(customStyles.subMenu, { [customStyles.subMenuLeft]: showOnLeft })}
|
||||
aria-label="Role picker submenu"
|
||||
>
|
||||
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack>
|
||||
<div className={styles.optionBody}>
|
||||
{options.map((option, i) => (
|
||||
<RoleMenuOption
|
||||
data={option}
|
||||
key={i}
|
||||
isSelected={
|
||||
!!(
|
||||
option.uid &&
|
||||
(!!selectedOptions.find((opt) => opt.uid === option.uid) ||
|
||||
disabledOptions?.find((opt) => opt.uid === option.uid))
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
!!(option.uid && disabledOptions?.find((opt) => opt.uid === option.uid)) || isNotDelegatable(option)
|
||||
}
|
||||
onChange={onSelect}
|
||||
hideDescription
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
<div className={customStyles.subMenuButtonRow}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button size="sm" fill="text" onClick={onClearInternal}>
|
||||
Clear
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RoleMenuOptionProps {
|
||||
data: Role;
|
||||
onChange: (value: Role) => void;
|
||||
isSelected?: boolean;
|
||||
isFocused?: boolean;
|
||||
disabled?: boolean;
|
||||
hideDescription?: boolean;
|
||||
}
|
||||
|
||||
export const RoleMenuOption = React.forwardRef<HTMLDivElement, React.PropsWithChildren<RoleMenuOptionProps>>(
|
||||
({ data, isFocused, isSelected, disabled, onChange, hideDescription }, ref) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
const customStyles = useStyles2(getStyles);
|
||||
|
||||
const wrapperClassName = cx(
|
||||
styles.option,
|
||||
isFocused && styles.optionFocused,
|
||||
disabled && customStyles.menuOptionDisabled
|
||||
);
|
||||
|
||||
const onChangeInternal = (event: FormEvent<HTMLElement>) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onChange(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className={wrapperClassName} aria-label="Role picker option" onClick={onChangeInternal}>
|
||||
<Checkbox
|
||||
value={isSelected}
|
||||
className={customStyles.menuOptionCheckbox}
|
||||
onChange={onChangeInternal}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
|
||||
<span>{data.displayName || data.name}</span>
|
||||
{!hideDescription && data.description && <div className={styles.optionDescription}>{data.description}</div>}
|
||||
</div>
|
||||
{data.description && (
|
||||
<Tooltip content={data.description}>
|
||||
<Icon name="info-circle" className={customStyles.menuOptionInfoSign} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RoleMenuOption.displayName = 'RoleMenuOption';
|
||||
|
||||
interface RoleMenuGroupsSectionProps {
|
||||
roles: Role[];
|
||||
renderedName: string;
|
||||
menuSectionStyle: string;
|
||||
groupHeaderStyle: string;
|
||||
optionBodyStyle: string;
|
||||
showGroups?: boolean;
|
||||
optionGroups: Array<{
|
||||
name: string;
|
||||
options: Role[];
|
||||
value: string;
|
||||
uid: string;
|
||||
}>;
|
||||
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<HTMLDivElement, RoleMenuGroupsSectionProps>(
|
||||
(
|
||||
{
|
||||
roles,
|
||||
renderedName,
|
||||
menuSectionStyle,
|
||||
groupHeaderStyle,
|
||||
optionBodyStyle,
|
||||
showGroups,
|
||||
optionGroups,
|
||||
onChange,
|
||||
groupSelected,
|
||||
groupPartiallySelected,
|
||||
onOpenSubMenuRMGS,
|
||||
onCloseSubMenu,
|
||||
subMenuNode,
|
||||
showSubMenu,
|
||||
openedMenuGroup,
|
||||
subMenuOptions,
|
||||
selectedOptions,
|
||||
onChangeSubMenu,
|
||||
onClearSubMenu,
|
||||
showOnLeftSubMenu,
|
||||
},
|
||||
_ref
|
||||
) => {
|
||||
return (
|
||||
<div>
|
||||
{roles.length > 0 && (
|
||||
<div className={menuSectionStyle}>
|
||||
<div className={groupHeaderStyle}>{renderedName}</div>
|
||||
<div className={optionBodyStyle}></div>
|
||||
{showGroups && !!optionGroups?.length
|
||||
? optionGroups.map((option) => (
|
||||
<RoleMenuGroupOption
|
||||
data={option}
|
||||
key={option.value}
|
||||
isSelected={groupSelected(option.value) || groupPartiallySelected(option.value)}
|
||||
partiallySelected={groupPartiallySelected(option.value)}
|
||||
disabled={option.options?.every(isNotDelegatable)}
|
||||
onChange={onChange}
|
||||
onOpenSubMenu={onOpenSubMenuRMGS}
|
||||
onCloseSubMenu={onCloseSubMenu}
|
||||
root={subMenuNode}
|
||||
isFocused={showSubMenu && openedMenuGroup === option.value}
|
||||
>
|
||||
{showSubMenu && openedMenuGroup === option.value && (
|
||||
<RolePickerSubMenu
|
||||
options={subMenuOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelect={onChangeSubMenu}
|
||||
onClear={onClearSubMenu}
|
||||
showOnLeft={showOnLeftSubMenu}
|
||||
/>
|
||||
)}
|
||||
</RoleMenuGroupOption>
|
||||
))
|
||||
: roles.map((option) => (
|
||||
<RoleMenuOption
|
||||
data={option}
|
||||
key={option.uid}
|
||||
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
|
||||
disabled={isNotDelegatable(option)}
|
||||
onChange={onChangeSubMenu}
|
||||
hideDescription
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RoleMenuGroupsSection.displayName = 'RoleMenuGroupsSection';
|
||||
|
||||
interface RoleMenuGroupsOptionProps {
|
||||
data: SelectableValue<string>;
|
||||
onChange: (value: string) => void;
|
||||
onClick?: (value: string) => void;
|
||||
onOpenSubMenu?: (value: string) => void;
|
||||
onCloseSubMenu?: (value: string) => void;
|
||||
isSelected?: boolean;
|
||||
partiallySelected?: boolean;
|
||||
isFocused?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
root?: HTMLElement;
|
||||
}
|
||||
|
||||
export const RoleMenuGroupOption = React.forwardRef<HTMLDivElement, RoleMenuGroupsOptionProps>(
|
||||
(
|
||||
{
|
||||
data,
|
||||
isFocused,
|
||||
isSelected,
|
||||
partiallySelected,
|
||||
disabled,
|
||||
onChange,
|
||||
onClick,
|
||||
onOpenSubMenu,
|
||||
onCloseSubMenu,
|
||||
children,
|
||||
root,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
const customStyles = useStyles2(getStyles);
|
||||
|
||||
const wrapperClassName = cx(
|
||||
styles.option,
|
||||
isFocused && styles.optionFocused,
|
||||
disabled && customStyles.menuOptionDisabled
|
||||
);
|
||||
|
||||
const onChangeInternal = (event: FormEvent<HTMLElement>) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (data.value) {
|
||||
onChange(data.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickInternal = (event: FormEvent<HTMLElement>) => {
|
||||
if (onClick) {
|
||||
onClick(data.value!);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (onOpenSubMenu) {
|
||||
onOpenSubMenu(data.value!);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (onCloseSubMenu) {
|
||||
onCloseSubMenu(data.value!);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<div ref={ref} className={wrapperClassName} aria-label="Role picker option" onClick={onClickInternal}>
|
||||
<Checkbox
|
||||
value={isSelected}
|
||||
className={cx(customStyles.menuOptionCheckbox, {
|
||||
[customStyles.checkboxPartiallyChecked]: partiallySelected,
|
||||
})}
|
||||
onChange={onChangeInternal}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
|
||||
<span>{data.displayName || data.name}</span>
|
||||
<span className={customStyles.menuOptionExpand} />
|
||||
</div>
|
||||
{root && children && (
|
||||
<Portal className={customStyles.subMenuPortal} root={root}>
|
||||
{children}
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RoleMenuGroupOption.displayName = 'RoleMenuGroupOption';
|
||||
|
||||
const getRoleGroup = (role: Role) => {
|
||||
return role.group || 'Other';
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const sortRolesByName = (a: Role, b: Role) => a.name.localeCompare(b.name);
|
||||
|
||||
const isNotDelegatable = (role: Role) => {
|
||||
return role.delegatable !== undefined && !role.delegatable;
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
menuWrapper: css`
|
||||
display: flex;
|
||||
max-height: 650px;
|
||||
position: absolute;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
overflow: hidden;
|
||||
min-width: auto;
|
||||
`,
|
||||
menu: css`
|
||||
min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
|
||||
|
||||
& > div {
|
||||
padding-top: ${theme.spacing(1)};
|
||||
}
|
||||
`,
|
||||
menuLeft: css`
|
||||
right: 0;
|
||||
flex-direction: row-reverse;
|
||||
`,
|
||||
subMenu: css`
|
||||
height: 100%;
|
||||
min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid ${theme.components.input.borderColor};
|
||||
|
||||
& > div {
|
||||
padding-top: ${theme.spacing(1)};
|
||||
}
|
||||
`,
|
||||
subMenuLeft: css`
|
||||
border-right: 1px solid ${theme.components.input.borderColor};
|
||||
border-left: unset;
|
||||
`,
|
||||
groupHeader: css`
|
||||
padding: ${theme.spacing(0, 4)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${theme.colors.text.primary};
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
`,
|
||||
container: css`
|
||||
padding: ${theme.spacing(1)};
|
||||
border: 1px ${theme.colors.border.weak} solid;
|
||||
border-radius: ${theme.shape.borderRadius(1)};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
z-index: ${theme.zIndex.modal};
|
||||
`,
|
||||
menuSection: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
menuOptionCheckbox: css`
|
||||
display: flex;
|
||||
margin: ${theme.spacing(0, 1, 0, 0.25)};
|
||||
`,
|
||||
menuButtonRow: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
menuOptionBody: css`
|
||||
font-weight: ${theme.typography.fontWeightRegular};
|
||||
padding: ${theme.spacing(0, 1.5, 0, 0)};
|
||||
`,
|
||||
menuOptionDisabled: css`
|
||||
color: ${theme.colors.text.disabled};
|
||||
cursor: not-allowed;
|
||||
`,
|
||||
menuOptionExpand: css`
|
||||
position: absolute;
|
||||
right: ${theme.spacing(1.25)};
|
||||
color: ${theme.colors.text.disabled};
|
||||
|
||||
&:after {
|
||||
content: '>';
|
||||
}
|
||||
`,
|
||||
menuOptionInfoSign: css`
|
||||
color: ${theme.colors.text.disabled};
|
||||
`,
|
||||
basicRoleSelector: css`
|
||||
margin: ${theme.spacing(1, 1.25, 1, 1)};
|
||||
`,
|
||||
subMenuPortal: css`
|
||||
height: 100%;
|
||||
> div {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
subMenuButtonRow: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
checkboxPartiallyChecked: css`
|
||||
input {
|
||||
&:checked + span {
|
||||
&:after {
|
||||
border-width: 0 3px 0px 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
76
public/app/core/components/RolePicker/RolePickerSubMenu.tsx
Normal file
76
public/app/core/components/RolePicker/RolePickerSubMenu.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { Button, CustomScrollbar, HorizontalGroup, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles';
|
||||
import { Role } from 'app/types';
|
||||
|
||||
import { RoleMenuOption } from './RoleMenuOption';
|
||||
import { MENU_MAX_HEIGHT } from './constants';
|
||||
import { getStyles } from './styles';
|
||||
import { isNotDelegatable } from './utils';
|
||||
|
||||
interface RolePickerSubMenuProps {
|
||||
options: Role[];
|
||||
selectedOptions: Role[];
|
||||
disabledOptions?: Role[];
|
||||
onSelect: (option: Role) => void;
|
||||
onClear?: () => void;
|
||||
showOnLeft?: boolean;
|
||||
}
|
||||
|
||||
export const RolePickerSubMenu = ({
|
||||
options,
|
||||
selectedOptions,
|
||||
disabledOptions,
|
||||
onSelect,
|
||||
onClear,
|
||||
showOnLeft,
|
||||
}: RolePickerSubMenuProps): JSX.Element => {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
const customStyles = useStyles2(getStyles);
|
||||
|
||||
const onClearInternal = async () => {
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(customStyles.subMenu, { [customStyles.subMenuLeft]: showOnLeft })}
|
||||
aria-label="Role picker submenu"
|
||||
>
|
||||
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack>
|
||||
<div className={styles.optionBody}>
|
||||
{options.map((option, i) => (
|
||||
<RoleMenuOption
|
||||
data={option}
|
||||
key={i}
|
||||
isSelected={
|
||||
!!(
|
||||
option.uid &&
|
||||
(!!selectedOptions.find((opt) => opt.uid === option.uid) ||
|
||||
disabledOptions?.find((opt) => opt.uid === option.uid))
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
!!(option.uid && disabledOptions?.find((opt) => opt.uid === option.uid)) || isNotDelegatable(option)
|
||||
}
|
||||
onChange={onSelect}
|
||||
hideDescription
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
<div className={customStyles.subMenuButtonRow}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button size="sm" fill="text" onClick={onClearInternal}>
|
||||
Clear
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
112
public/app/core/components/RolePicker/styles.ts
Normal file
112
public/app/core/components/RolePicker/styles.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { ROLE_PICKER_SUBMENU_MIN_WIDTH } from './constants';
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
menuWrapper: css`
|
||||
display: flex;
|
||||
max-height: 650px;
|
||||
position: absolute;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
overflow: hidden;
|
||||
min-width: auto;
|
||||
`,
|
||||
menu: css`
|
||||
min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
|
||||
|
||||
& > div {
|
||||
padding-top: ${theme.spacing(1)};
|
||||
}
|
||||
`,
|
||||
menuLeft: css`
|
||||
right: 0;
|
||||
flex-direction: row-reverse;
|
||||
`,
|
||||
subMenu: css`
|
||||
height: 100%;
|
||||
min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid ${theme.components.input.borderColor};
|
||||
|
||||
& > div {
|
||||
padding-top: ${theme.spacing(1)};
|
||||
}
|
||||
`,
|
||||
subMenuLeft: css`
|
||||
border-right: 1px solid ${theme.components.input.borderColor};
|
||||
border-left: unset;
|
||||
`,
|
||||
groupHeader: css`
|
||||
padding: ${theme.spacing(0, 4)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${theme.colors.text.primary};
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
`,
|
||||
container: css`
|
||||
padding: ${theme.spacing(1)};
|
||||
border: 1px ${theme.colors.border.weak} solid;
|
||||
border-radius: ${theme.shape.borderRadius(1)};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
z-index: ${theme.zIndex.modal};
|
||||
`,
|
||||
menuSection: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
menuOptionCheckbox: css`
|
||||
display: flex;
|
||||
margin: ${theme.spacing(0, 1, 0, 0.25)};
|
||||
`,
|
||||
menuButtonRow: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
menuOptionBody: css`
|
||||
font-weight: ${theme.typography.fontWeightRegular};
|
||||
padding: ${theme.spacing(0, 1.5, 0, 0)};
|
||||
`,
|
||||
menuOptionDisabled: css`
|
||||
color: ${theme.colors.text.disabled};
|
||||
cursor: not-allowed;
|
||||
`,
|
||||
menuOptionExpand: css`
|
||||
position: absolute;
|
||||
right: ${theme.spacing(1.25)};
|
||||
color: ${theme.colors.text.disabled};
|
||||
|
||||
&:after {
|
||||
content: '>';
|
||||
}
|
||||
`,
|
||||
menuOptionInfoSign: css`
|
||||
color: ${theme.colors.text.disabled};
|
||||
`,
|
||||
basicRoleSelector: css`
|
||||
margin: ${theme.spacing(1, 1.25, 1, 1)};
|
||||
`,
|
||||
subMenuPortal: css`
|
||||
height: 100%;
|
||||
> div {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
subMenuButtonRow: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
checkboxPartiallyChecked: css`
|
||||
input {
|
||||
&:checked + span {
|
||||
&:after {
|
||||
border-width: 0 3px 0px 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
5
public/app/core/components/RolePicker/utils.ts
Normal file
5
public/app/core/components/RolePicker/utils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Role } from 'app/types';
|
||||
|
||||
export const isNotDelegatable = (role: Role) => {
|
||||
return role.delegatable !== undefined && !role.delegatable;
|
||||
};
|
Loading…
Reference in New Issue
Block a user