Role picker: Split components into separate files (#60519)

This commit is contained in:
Alexander Zobnin 2022-12-20 13:05:37 +03:00 committed by GitHub
parent 35090c376c
commit 6e2b148745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 480 additions and 448 deletions

View 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';

View 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';

View 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';

View File

@ -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);
}
}
}
`,
};
};

View 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>
);
};

View 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);
}
}
}
`,
};
};

View File

@ -0,0 +1,5 @@
import { Role } from 'app/types';
export const isNotDelegatable = (role: Role) => {
return role.delegatable !== undefined && !role.delegatable;
};