mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -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} />
|
<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
|
||||||
|
/>
|
||||||
|
 
|
||||||
|
<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>
|
||||||
|
|
|
@ -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',
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -62,6 +62,7 @@ export const SelectableDemo: FunctionComponent = () => {
|
||||||
selectedOptions={selectedOptions}
|
selectedOptions={selectedOptions}
|
||||||
defaultOptions={defaultOptions}
|
defaultOptions={defaultOptions}
|
||||||
onChange={setSelectedOptions}
|
onChange={setSelectedOptions}
|
||||||
|
limit={2}
|
||||||
/>
|
/>
|
||||||
</PanelWithCodeBlock>
|
</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', () => {
|
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(
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue