Add the ability to limit the number of users you can select (#144618)

This commit is contained in:
Thom Heymann 2022-11-07 12:39:30 +00:00 committed by GitHub
parent 2e495074fd
commit a7976e57aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 22 deletions

View file

@ -38,6 +38,18 @@ export const AvatarDemo: FunctionComponent = () => {
   
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} /> <UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} />
<EuiSpacer size="l" /> <EuiSpacer size="l" />
<EuiTitle size="xs">
<h3>Disabled</h3>
</EuiTitle>
<EuiSpacer size="s" />
<UserAvatar
user={userProfile.user}
avatar={{ ...userProfile.data.avatar, imageUrl: undefined }}
isDisabled
/>
&ensp;
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} isDisabled />
<EuiSpacer size="l" />
<EuiTitle size="xs"> <EuiTitle size="xs">
<h3>Unknown</h3> <h3>Unknown</h3>
</EuiTitle> </EuiTitle>

View file

@ -110,6 +110,7 @@ export const PopoverDemo: FunctionComponent = () => {
selectedOptions, selectedOptions,
defaultOptions, defaultOptions,
onChange: setSelectedOptions, onChange: setSelectedOptions,
limit: 2,
height: 32 * 8, height: 32 * 8,
}} }}
panelStyle={{ panelStyle={{
@ -140,6 +141,7 @@ export const PopoverDemo: FunctionComponent = () => {
selectedOptions: selectedOptions2, selectedOptions: selectedOptions2,
options: options2, options: options2,
onChange: setSelectedOptions2, onChange: setSelectedOptions2,
limit: 2,
height: 32 * 8, height: 32 * 8,
nullOptionLabel: 'Unassigned', nullOptionLabel: 'Unassigned',
}} }}

View file

@ -62,6 +62,7 @@ export const SelectableDemo: FunctionComponent = () => {
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
defaultOptions={defaultOptions} defaultOptions={defaultOptions}
onChange={setSelectedOptions} onChange={setSelectedOptions}
limit={2}
/> />
</PanelWithCodeBlock> </PanelWithCodeBlock>
); );

View file

@ -84,6 +84,37 @@ describe('UserProfilesSelectable', () => {
]); ]);
}); });
it('should render warning and disable remaining users when limit has been reached', () => {
const [firstOption, secondOption, thirdOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable
selectedOptions={[firstOption]}
defaultOptions={[secondOption, thirdOption]}
limit={1}
/>
);
expect(wrapper.find('EuiCallOut').prop('color')).toEqual('warning');
expect(wrapper.find('EuiSelectable').prop('options')).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: firstOption.uid,
checked: 'on',
disabled: false,
}),
expect.objectContaining({
key: secondOption.uid,
checked: undefined,
disabled: true,
}),
expect.objectContaining({
key: thirdOption.uid,
checked: undefined,
disabled: true,
}),
])
);
});
it('should hide `selectedOptions` and `defaultOptions` when `options` has been provided', () => { it('should hide `selectedOptions` and `defaultOptions` when `options` has been provided', () => {
const [firstOption, secondOption, thirdOption] = userProfiles; const [firstOption, secondOption, thirdOption] = userProfiles;
const wrapper = mount( const wrapper = mount(

View file

@ -16,6 +16,7 @@ import {
EuiSelectable, EuiSelectable,
EuiSpacer, EuiSpacer,
EuiText, EuiText,
EuiCallOut,
EuiHighlight, EuiHighlight,
EuiTextColor, EuiTextColor,
} from '@elastic/eui'; } from '@elastic/eui';
@ -59,6 +60,13 @@ export interface UserProfilesSelectableProps<Option extends UserProfileWithAvata
*/ */
options?: Option[]; options?: Option[];
/**
* Maximum number of users allowed to be selected.
*
* This limit is not enforced and only used to show a warning message.
*/
limit?: number;
/** /**
* Passes back the current selection. * Passes back the current selection.
* @param options Either the list of selected users or `null` (no users). * @param options Either the list of selected users or `null` (no users).
@ -87,11 +95,17 @@ export interface UserProfilesSelectableProps<Option extends UserProfileWithAvata
searchInputId?: string; searchInputId?: string;
/** /**
* Returns text for selected status. * Returns message for number of selected users.
* @param selectedCount Number of selected users * @param selectedCount Number of selected users
*/ */
selectedStatusMessage?(selectedCount: number): ReactNode; selectedStatusMessage?(selectedCount: number): ReactNode;
/**
* Returns message when maximum number of selected users are reached.
* @param limit Maximum number of users allowed to be selected
*/
limitReachedMessage?(limit: number): ReactNode;
/** /**
* Label for clear button. * Label for clear button.
*/ */
@ -119,6 +133,7 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
onSearchChange, onSearchChange,
isLoading = false, isLoading = false,
singleSelection = false, singleSelection = false,
limit,
height, height,
loadingMessage, loadingMessage,
noMatchesMessage, noMatchesMessage,
@ -127,6 +142,7 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
searchPlaceholder, searchPlaceholder,
searchInputId, searchInputId,
selectedStatusMessage, selectedStatusMessage,
limitReachedMessage,
nullOptionLabel, nullOptionLabel,
defaultOptionsLabel, defaultOptionsLabel,
clearButtonLabel, clearButtonLabel,
@ -134,6 +150,9 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
const [displayedOptions, setDisplayedOptions] = useState<SelectableOption[]>([]); const [displayedOptions, setDisplayedOptions] = useState<SelectableOption[]>([]);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const selectedCount = selectedOptions ? selectedOptions.length : 0;
const limitReached = limit ? selectedCount >= limit : false;
// Resets all displayed options // Resets all displayed options
const resetDisplayedOptions = () => { const resetDisplayedOptions = () => {
if (options) { if (options) {
@ -208,18 +227,30 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
values.map((option) => { values.map((option) => {
if (selectedOptions) { if (selectedOptions) {
const match = selectedOptions.find((profile) => isMatchingOption(option, profile)); const match = selectedOptions.find((profile) => isMatchingOption(option, profile));
return { ...option, checked: match === undefined ? undefined : 'on' }; const checked = match === undefined ? undefined : 'on';
const disabled = checked ? false : limitReached;
return {
...option,
checked,
disabled,
prepend: option.data ? (
<UserAvatar
user={option.data.user}
avatar={option.data.data?.avatar}
size="s"
isDisabled={disabled}
/>
) : undefined,
};
} }
return { ...option, checked: undefined }; return { ...option, checked: undefined, disabled: undefined };
}) })
); );
}; };
useEffect(resetDisplayedOptions, [options]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(resetDisplayedOptions, [options]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(updateDisplayedOptions, [defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(updateDisplayedOptions, [defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(updateCheckedStatus, [options, defaultOptions, selectedOptions]); useEffect(updateCheckedStatus, [options, defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps
const selectedCount = selectedOptions ? selectedOptions.length : 0;
return ( return (
<EuiSelectable <EuiSelectable
@ -298,12 +329,12 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
gutterSize="s" gutterSize="s"
responsive={false} responsive={false}
> >
<EuiFlexItem grow={false}> <EuiFlexItem>
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight> <EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
</EuiFlexItem> </EuiFlexItem>
{option.user.email && option.user.email !== option.label ? ( {option.user.email && option.user.email !== option.label ? (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiTextColor color="subdued"> <EuiTextColor color={option.disabled ? 'disabled' : 'subdued'}>
{searchValue ? ( {searchValue ? (
<EuiHighlight search={searchValue}>{option.user.email}</EuiHighlight> <EuiHighlight search={searchValue}>{option.user.email}</EuiHighlight>
) : ( ) : (
@ -365,6 +396,26 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
</> </>
) : undefined} ) : undefined}
</EuiPanel> </EuiPanel>
{limit && selectedCount >= limit ? (
<>
<EuiHorizontalRule margin="none" />
<EuiCallOut
title={
limitReachedMessage ? (
limitReachedMessage(limit)
) : (
<FormattedMessage
id="userProfileComponents.userProfilesSelectable.limitReachedMessage"
defaultMessage="You've selected the maximum of {count, plural, one {# user} other {# users}}"
values={{ count: limit }}
/>
)
}
color="warning"
size="s"
/>
</>
) : undefined}
<EuiHorizontalRule margin="none" /> <EuiHorizontalRule margin="none" />
{list} {list}
</> </>
@ -382,7 +433,6 @@ function toSelectableOption(
if (userProfile) { if (userProfile) {
return { return {
key: userProfile.uid, key: userProfile.uid,
prepend: <UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} size="s" />,
label: getUserDisplayName(userProfile.user), label: getUserDisplayName(userProfile.user),
data: userProfile, data: userProfile,
}; };

View file

@ -6,13 +6,11 @@ exports[`<SpaceAwarePrivilegeSection> with user profile disabling "manageSpaces"
data-test-subj="userCannotManageSpacesCallout" data-test-subj="userCannotManageSpacesCallout"
iconType="alert" iconType="alert"
title={ title={
<p> <FormattedMessage
<FormattedMessage defaultMessage="Insufficient Privileges"
defaultMessage="Insufficient Privileges" id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription" values={Object {}}
values={Object {}} />
/>
</p>
} }
> >
<p> <p>

View file

@ -82,12 +82,10 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
return ( return (
<EuiCallOut <EuiCallOut
title={ title={
<p> <FormattedMessage
<FormattedMessage id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription" defaultMessage="Insufficient Privileges"
defaultMessage="Insufficient Privileges" />
/>
</p>
} }
iconType="alert" iconType="alert"
color="danger" color="danger"