mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Add the ability to limit the number of users you can select (#144618)
This commit is contained in:
parent
2e495074fd
commit
a7976e57aa
7 changed files with 114 additions and 22 deletions
|
@ -38,6 +38,18 @@ export const AvatarDemo: FunctionComponent = () => {
|
|||
 
|
||||
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xs">
|
||||
<h3>Disabled</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<UserAvatar
|
||||
user={userProfile.user}
|
||||
avatar={{ ...userProfile.data.avatar, imageUrl: undefined }}
|
||||
isDisabled
|
||||
/>
|
||||
 
|
||||
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} isDisabled />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xs">
|
||||
<h3>Unknown</h3>
|
||||
</EuiTitle>
|
||||
|
|
|
@ -110,6 +110,7 @@ export const PopoverDemo: FunctionComponent = () => {
|
|||
selectedOptions,
|
||||
defaultOptions,
|
||||
onChange: setSelectedOptions,
|
||||
limit: 2,
|
||||
height: 32 * 8,
|
||||
}}
|
||||
panelStyle={{
|
||||
|
@ -140,6 +141,7 @@ export const PopoverDemo: FunctionComponent = () => {
|
|||
selectedOptions: selectedOptions2,
|
||||
options: options2,
|
||||
onChange: setSelectedOptions2,
|
||||
limit: 2,
|
||||
height: 32 * 8,
|
||||
nullOptionLabel: 'Unassigned',
|
||||
}}
|
||||
|
|
|
@ -62,6 +62,7 @@ export const SelectableDemo: FunctionComponent = () => {
|
|||
selectedOptions={selectedOptions}
|
||||
defaultOptions={defaultOptions}
|
||||
onChange={setSelectedOptions}
|
||||
limit={2}
|
||||
/>
|
||||
</PanelWithCodeBlock>
|
||||
);
|
||||
|
|
|
@ -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', () => {
|
||||
const [firstOption, secondOption, thirdOption] = userProfiles;
|
||||
const wrapper = mount(
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiSelectable,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiCallOut,
|
||||
EuiHighlight,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
|
@ -59,6 +60,13 @@ export interface UserProfilesSelectableProps<Option extends UserProfileWithAvata
|
|||
*/
|
||||
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.
|
||||
* @param options Either the list of selected users or `null` (no users).
|
||||
|
@ -87,11 +95,17 @@ export interface UserProfilesSelectableProps<Option extends UserProfileWithAvata
|
|||
searchInputId?: string;
|
||||
|
||||
/**
|
||||
* Returns text for selected status.
|
||||
* Returns message for number of selected users.
|
||||
* @param selectedCount Number of selected users
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -119,6 +133,7 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
|
|||
onSearchChange,
|
||||
isLoading = false,
|
||||
singleSelection = false,
|
||||
limit,
|
||||
height,
|
||||
loadingMessage,
|
||||
noMatchesMessage,
|
||||
|
@ -127,6 +142,7 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
|
|||
searchPlaceholder,
|
||||
searchInputId,
|
||||
selectedStatusMessage,
|
||||
limitReachedMessage,
|
||||
nullOptionLabel,
|
||||
defaultOptionsLabel,
|
||||
clearButtonLabel,
|
||||
|
@ -134,6 +150,9 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
|
|||
const [displayedOptions, setDisplayedOptions] = useState<SelectableOption[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const selectedCount = selectedOptions ? selectedOptions.length : 0;
|
||||
const limitReached = limit ? selectedCount >= limit : false;
|
||||
|
||||
// Resets all displayed options
|
||||
const resetDisplayedOptions = () => {
|
||||
if (options) {
|
||||
|
@ -208,18 +227,30 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
|
|||
values.map((option) => {
|
||||
if (selectedOptions) {
|
||||
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(updateDisplayedOptions, [defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
useEffect(updateCheckedStatus, [options, defaultOptions, selectedOptions]);
|
||||
|
||||
const selectedCount = selectedOptions ? selectedOptions.length : 0;
|
||||
useEffect(updateCheckedStatus, [options, defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<EuiSelectable
|
||||
|
@ -298,12 +329,12 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
|
|||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
|
||||
</EuiFlexItem>
|
||||
{option.user.email && option.user.email !== option.label ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTextColor color="subdued">
|
||||
<EuiTextColor color={option.disabled ? 'disabled' : 'subdued'}>
|
||||
{searchValue ? (
|
||||
<EuiHighlight search={searchValue}>{option.user.email}</EuiHighlight>
|
||||
) : (
|
||||
|
@ -365,6 +396,26 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
|
|||
</>
|
||||
) : undefined}
|
||||
</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" />
|
||||
{list}
|
||||
</>
|
||||
|
@ -382,7 +433,6 @@ function toSelectableOption(
|
|||
if (userProfile) {
|
||||
return {
|
||||
key: userProfile.uid,
|
||||
prepend: <UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} size="s" />,
|
||||
label: getUserDisplayName(userProfile.user),
|
||||
data: userProfile,
|
||||
};
|
||||
|
|
|
@ -6,13 +6,11 @@ exports[`<SpaceAwarePrivilegeSection> with user profile disabling "manageSpaces"
|
|||
data-test-subj="userCannotManageSpacesCallout"
|
||||
iconType="alert"
|
||||
title={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Insufficient Privileges"
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Insufficient Privileges"
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
|
|
|
@ -82,12 +82,10 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
|
|||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
|
||||
defaultMessage="Insufficient Privileges"
|
||||
/>
|
||||
</p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
|
||||
defaultMessage="Insufficient Privileges"
|
||||
/>
|
||||
}
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue