Remove unused sharedux avatar components (#168686)

## Summary

Closes https://github.com/elastic/kibana/issues/168689

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Vadim Kibana 2023-10-25 12:07:55 +02:00 committed by GitHub
parent 6f334cd510
commit 9a9a51b454
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 3 additions and 1164 deletions

1
.github/CODEOWNERS vendored
View file

@ -676,7 +676,6 @@ examples/share_examples @elastic/appex-sharedux
src/plugins/share @elastic/appex-sharedux
packages/kbn-shared-svg @elastic/apm-ui
packages/shared-ux/avatar/solution @elastic/appex-sharedux
packages/shared-ux/avatar/user_profile/impl @elastic/appex-sharedux
packages/shared-ux/button/exit_full_screen/impl @elastic/appex-sharedux
packages/shared-ux/button/exit_full_screen/mocks @elastic/appex-sharedux
packages/shared-ux/button/exit_full_screen/types @elastic/appex-sharedux

View file

@ -679,7 +679,6 @@
"@kbn/share-plugin": "link:src/plugins/share",
"@kbn/shared-svg": "link:packages/kbn-shared-svg",
"@kbn/shared-ux-avatar-solution": "link:packages/shared-ux/avatar/solution",
"@kbn/shared-ux-avatar-user-profile-components": "link:packages/shared-ux/avatar/user_profile/impl",
"@kbn/shared-ux-button-exit-full-screen": "link:packages/shared-ux/button/exit_full_screen/impl",
"@kbn/shared-ux-button-exit-full-screen-mocks": "link:packages/shared-ux/button/exit_full_screen/mocks",
"@kbn/shared-ux-button-exit-full-screen-types": "link:packages/shared-ux/button/exit_full_screen/types",

View file

@ -1,12 +0,0 @@
---
id: sharedUX/Components/UserProfileAvatar
slug: /shared-ux/components/user-profile-avatar
title: User Profile Avatar
description: A wrapper around `EuiAvatar`
tags: ['shared-ux', 'component']
date: 2022-09-01
---
## Description
A wrapper around `EuiAvatar` tailored for user profiles

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { UserAvatarProps, UserProfileWithAvatar } from './user_avatar';
export type { UserProfilesSelectableProps } from './user_profiles_selectable';
export type { UserProfilesPopoverProps } from './user_profiles_popover';
export { UserAvatar } from './user_avatar';
export { UserProfilesSelectable } from './user_profiles_selectable';
export { UserProfilesPopover } from './user_profiles_popover';
export type { UserProfile, UserProfileUserInfo, UserProfileAvatarData } from './user_profile';

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/packages/shared-ux/avatar/user_profile/impl'],
};

View file

@ -1,5 +0,0 @@
{
"type": "shared-common",
"id": "@kbn/shared-ux-avatar-user-profile-components",
"owner": "@elastic/appex-sharedux"
}

View file

@ -1,6 +0,0 @@
{
"name": "@kbn/shared-ux-avatar-user-profile-components",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -1,25 +0,0 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@kbn/ambient-ui-types"
]
},
"include": [
"*ts*",
"*.md*",
"**/*.ts",
"**/*.md*",
],
"kbn_references": [
"@kbn/i18n-react",
"@kbn/i18n",
],
"exclude": [
"target/**/*",
]
}

View file

@ -1,94 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { UserAvatar } from './user_avatar';
describe('UserAvatar', () => {
it('should render `EuiAvatar` correctly with image avatar', () => {
const wrapper = shallow(
<UserAvatar
user={{
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
fullName: 'Delighted Nightingale',
}}
avatar={{
color: '#09e8ca',
initials: 'DN',
imageUrl: 'https://source.unsplash.com/64x64/?cat',
}}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<EuiAvatar
color="plain"
imageUrl="https://source.unsplash.com/64x64/?cat"
name="Delighted Nightingale"
/>
`);
});
it('should render `EuiAvatar` correctly with initials avatar', () => {
const wrapper = shallow(
<UserAvatar
user={{
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
fullName: 'Delighted Nightingale',
}}
avatar={{
color: '#09e8ca',
initials: 'DN',
imageUrl: undefined,
}}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<EuiAvatar
color="#09e8ca"
initials="DN"
initialsLength={2}
name="Delighted Nightingale"
/>
`);
});
it('should render `EuiAvatar` correctly without avatar data', () => {
const wrapper = shallow(
<UserAvatar
user={{
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
fullName: 'Delighted Nightingale',
}}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<EuiAvatar
color="#AA6556"
initials="DN"
initialsLength={2}
name="Delighted Nightingale"
/>
`);
});
it('should render `EuiAvatar` correctly without user data', () => {
const wrapper = shallow(<UserAvatar />);
expect(wrapper).toMatchInlineSnapshot(`
<EuiAvatar
color="#F1F4FA"
initials="?"
name=""
/>
`);
});
});

View file

@ -1,79 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { EuiAvatarProps } from '@elastic/eui';
import { EuiAvatar, useEuiTheme } from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';
import type { UserProfile, UserProfileUserInfo, UserProfileAvatarData } from './user_profile';
import {
getUserAvatarColor,
getUserAvatarInitials,
getUserDisplayName,
USER_AVATAR_MAX_INITIALS,
} from './user_profile';
/**
* Convenience type for a {@link UserProfile} with avatar data
*/
export type UserProfileWithAvatar = UserProfile<{ avatar?: UserProfileAvatarData }>;
/**
* Props of {@link UserAvatar} component
*/
export interface UserAvatarProps
extends Omit<
EuiAvatarProps,
| 'initials'
| 'initialsLength'
| 'imageUrl'
| 'iconType'
| 'iconSize'
| 'iconColor'
| 'name'
| 'color'
| 'type'
> {
/**
* User to be rendered
*/
user?: UserProfileUserInfo;
/**
* Avatar data of user to be rendered
*/
avatar?: UserProfileAvatarData;
}
/**
* Renders an avatar given a user profile
*/
export const UserAvatar: FunctionComponent<UserAvatarProps> = ({ user, avatar, ...rest }) => {
const { euiTheme } = useEuiTheme();
if (!user) {
return <EuiAvatar name="" color={euiTheme.colors.lightestShade} initials="?" {...rest} />;
}
const displayName = getUserDisplayName(user);
if (avatar?.imageUrl) {
return <EuiAvatar name={displayName} imageUrl={avatar.imageUrl} color="plain" {...rest} />;
}
return (
<EuiAvatar
name={displayName}
initials={getUserAvatarInitials(user, avatar)}
initialsLength={USER_AVATAR_MAX_INITIALS}
color={getUserAvatarColor(user, avatar)}
{...rest}
/>
);
};

View file

@ -1,137 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { VISUALIZATION_COLORS } from '@elastic/eui';
/**
* IMPORTANT:
*
* The types in this file have been imported from
* `x-pack/plugins/security/common/model/user_profile.ts`
*
* When making changes please ensure to keep both files in sync.
*/
/**
* Describes basic properties stored in user profile.
*/
export interface UserProfile<D extends UserProfileData = UserProfileData> {
/**
* Unique ID for of the user profile.
*/
uid: string;
/**
* Indicates whether user profile is enabled or not.
*/
enabled: boolean;
/**
* Information about the user that owns profile.
*/
user: UserProfileUserInfo;
/**
* User specific data associated with the profile.
*/
data: Partial<D>;
}
/**
* Basic user information returned in user profile.
*/
export interface UserProfileUserInfo {
/**
* Username of the user.
*/
username: string;
/**
* Optional email of the user.
*/
email?: string;
/**
* Optional full name of the user.
*/
fullName?: string;
/**
* Optional display name of the user.
*/
displayName?: string;
}
/**
* Placeholder for data stored in user profile.
*/
export type UserProfileData = Record<string, unknown>;
/**
* Avatar stored in user profile.
*/
export interface UserProfileAvatarData {
/**
* Optional initials (two letters) of the user to use as avatar if avatar picture isn't specified.
*/
initials?: string;
/**
* Background color of the avatar when initials are used.
*/
color?: string;
/**
* Base64 data URL for the user avatar image.
*/
imageUrl?: string | null;
}
export const USER_AVATAR_FALLBACK_CODE_POINT = 97; // code point for lowercase "a"
export const USER_AVATAR_MAX_INITIALS = 2;
/**
* Determines the color for the provided user profile.
* If a color is present on the user profile itself, then that is used.
* Otherwise, a color is provided from EUI's Visualization Colors based on the display name.
*
* @param {UserProfileUserInfo} user User info
* @param {UserProfileAvatarData} avatar User avatar
*/
export function getUserAvatarColor(
user: Pick<UserProfileUserInfo, 'username' | 'fullName'>,
avatar?: UserProfileAvatarData
) {
const firstCodePoint = getUserDisplayName(user).codePointAt(0) || USER_AVATAR_FALLBACK_CODE_POINT;
return avatar?.color ?? VISUALIZATION_COLORS[firstCodePoint % VISUALIZATION_COLORS.length];
}
/**
* Determines the initials for the provided user profile.
* If initials are present on the user profile itself, then that is used.
* Otherwise, the initials are calculated based off the words in the display name, with a max length of 2 characters.
*
* @param {UserProfileUserInfo} user User info
* @param {UserProfileAvatarData} avatar User avatar
*/
export function getUserAvatarInitials(
user: Pick<UserProfileUserInfo, 'username' | 'fullName'>,
avatar?: UserProfileAvatarData
) {
const words = getUserDisplayName(user).split(' ');
const numInitials = Math.min(USER_AVATAR_MAX_INITIALS, words.length);
words.splice(numInitials, words.length);
return avatar?.initials ?? words.map((word) => word.substring(0, 1)).join('');
}
/**
* Determines the display name for the provided user profile.
*
* @param {UserProfileUserInfo} user User info
*/
export function getUserDisplayName(user: Pick<UserProfileUserInfo, 'username' | 'fullName'>) {
return user.fullName || user.username;
}

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { UserAvatar, UserAvatarProps } from './user_avatar';
import mdx from './README.mdx';
import { UserProfileUserInfo } from './user_profile';
export default {
title: 'Avatar/User Profile',
description: '',
parameters: {
docs: {
page: mdx,
},
},
};
type UserAvatarParams = Pick<UserAvatarProps, 'user'>;
const sampleUsers = [
{
username: 'Peggy',
email: 'test@email.com',
fullName: 'Peggy Simms',
displayName: 'Peggy',
},
{
username: 'Martin',
email: 'test@email.com',
fullName: 'Martin Gatsby',
displayName: 'Martin',
},
{
username: 'Leonardo DiCaprio',
email: 'test@email.com',
fullName: 'Leonardo DiCaprio',
displayName: 'Leonardo DiCaprio',
},
];
export const userAvatar = (
params: Pick<UserProfileUserInfo, 'username'>,
rest: UserAvatarParams
) => {
const username = params;
return <UserAvatar {...{ user: username }} {...rest} />;
};
userAvatar.argTypes = {
username: {
control: { type: 'radio' },
options: sampleUsers.map(({ username }) => username),
defaultValue: sampleUsers.map(({ username }) => username)[0],
},
};

View file

@ -1,125 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { UserProfilesPopover } from './user_profiles_popover';
const userProfiles = [
{
uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0',
enabled: true,
data: {},
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@profiles.elastic.co',
fullName: 'Delighted Nightingale',
},
},
{
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
enabled: true,
data: {},
user: {
username: 'damaged_raccoon',
email: 'damaged_raccoon@profiles.elastic.co',
fullName: 'Damaged Raccoon',
},
},
{
uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0',
enabled: true,
data: {},
user: {
username: 'physical_dinosaur',
email: 'physical_dinosaur@profiles.elastic.co',
fullName: 'Physical Dinosaur',
},
},
{
uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0',
enabled: true,
data: {},
user: {
username: 'wet_dingo',
email: 'wet_dingo@profiles.elastic.co',
fullName: 'Wet Dingo',
},
},
];
describe('UserProfilesPopover', () => {
it('should render `EuiPopover` and `UserProfilesSelectable` correctly', () => {
const [firstOption, secondOption] = userProfiles;
const wrapper = shallow(
<UserProfilesPopover
title="Title"
button={<button>Toggle</button>}
closePopover={jest.fn()}
selectableProps={{
selectedOptions: [firstOption],
defaultOptions: [secondOption],
}}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<EuiPopover
anchorPosition="downCenter"
button={
<button>
Toggle
</button>
}
closePopover={[MockFunction]}
display="inline-block"
hasArrow={true}
isOpen={false}
ownFocus={true}
panelPaddingSize="none"
repositionToCrossAxis={true}
>
<EuiContextMenuPanel
items={Array []}
title="Title"
>
<UserProfilesSelectable
defaultOptions={
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"user": Object {
"email": "damaged_raccoon@profiles.elastic.co",
"fullName": "Damaged Raccoon",
"username": "damaged_raccoon",
},
},
]
}
selectedOptions={
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0",
"user": Object {
"email": "delighted_nightingale@profiles.elastic.co",
"fullName": "Delighted Nightingale",
"username": "delighted_nightingale",
},
},
]
}
/>
</EuiContextMenuPanel>
</EuiPopover>
`);
});
});

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { EuiPopoverProps, EuiContextMenuPanelProps } from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';
import { EuiPopover, EuiContextMenuPanel } from '@elastic/eui';
import { UserProfilesSelectable, UserProfilesSelectableProps } from './user_profiles_selectable';
/**
* Props of {@link UserProfilesPopover} component
*/
export interface UserProfilesPopoverProps extends EuiPopoverProps {
/**
* Title of the popover
* @see EuiContextMenuPanelProps
*/
title?: EuiContextMenuPanelProps['title'];
/**
* Props forwarded to selectable component
* @see UserProfilesSelectableProps
*/
selectableProps: UserProfilesSelectableProps;
}
/**
* Renders a selectable component inside a popover given a list of user profiles
*/
export const UserProfilesPopover: FunctionComponent<UserProfilesPopoverProps> = ({
title,
selectableProps,
...popoverProps
}) => {
return (
<EuiPopover panelPaddingSize="none" {...popoverProps}>
<EuiContextMenuPanel title={title}>
<UserProfilesSelectable {...selectableProps} />
</EuiContextMenuPanel>
</EuiPopover>
);
};

View file

@ -1,203 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { mount } from 'enzyme';
import React from 'react';
import { UserProfilesSelectable } from './user_profiles_selectable';
const userProfiles = [
{
uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0',
enabled: true,
data: {},
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@profiles.elastic.co',
fullName: 'Delighted Nightingale',
},
},
{
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
enabled: true,
data: {},
user: {
username: 'damaged_raccoon',
email: 'damaged_raccoon@profiles.elastic.co',
fullName: 'Damaged Raccoon',
},
},
{
uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0',
enabled: true,
data: {},
user: {
username: 'physical_dinosaur',
email: 'physical_dinosaur@profiles.elastic.co',
fullName: 'Physical Dinosaur',
},
},
{
uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0',
enabled: true,
data: {},
user: {
username: 'wet_dingo',
email: 'wet_dingo@profiles.elastic.co',
fullName: 'Wet Dingo',
},
},
];
describe('UserProfilesSelectable', () => {
it('should render `selectedOptions` before `defaultOptions` separated by a group label', () => {
const [firstOption, secondOption, thirdOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable
selectedOptions={[firstOption]}
defaultOptions={[secondOption, thirdOption]}
/>
);
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
expect.objectContaining({
key: firstOption.uid,
checked: 'on',
}),
expect.objectContaining({
isGroupLabel: true,
label: 'Suggested',
}),
expect.objectContaining({
key: secondOption.uid,
checked: undefined,
}),
expect.objectContaining({
key: thirdOption.uid,
checked: undefined,
}),
]);
});
it('should hide `selectedOptions` and `defaultOptions` when `options` has been provided', () => {
const [firstOption, secondOption, thirdOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable
selectedOptions={[firstOption]}
defaultOptions={[secondOption]}
options={[thirdOption]}
/>
);
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
expect.objectContaining({
key: thirdOption.uid,
checked: undefined,
}),
]);
});
it('should hide `selectedOptions` and `defaultOptions` when `options` gets updated', () => {
const [firstOption, secondOption, thirdOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable selectedOptions={[firstOption]} defaultOptions={[secondOption]} />
);
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
expect.objectContaining({
key: firstOption.uid,
checked: 'on',
}),
expect.objectContaining({
isGroupLabel: true,
label: 'Suggested',
}),
expect.objectContaining({
key: secondOption.uid,
checked: undefined,
}),
]);
wrapper.setProps({ options: [thirdOption] }).update();
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
expect.objectContaining({
key: thirdOption.uid,
checked: undefined,
}),
]);
});
it('should render `options` with correct checked status', () => {
const [firstOption, secondOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable
selectedOptions={[firstOption]}
options={[firstOption, secondOption]}
/>
);
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
expect.objectContaining({
key: firstOption.uid,
checked: 'on',
}),
expect.objectContaining({
key: secondOption.uid,
checked: undefined,
}),
]);
});
it('should trigger `onChange` callback when selection changes', () => {
const onChange = jest.fn();
const [firstOption, secondOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable
selectedOptions={[firstOption]}
defaultOptions={[secondOption]}
onChange={onChange}
/>
);
wrapper.find('EuiSelectableListItem').last().simulate('click');
expect(onChange).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
uid: firstOption.uid,
}),
expect.objectContaining({
uid: secondOption.uid,
}),
])
);
});
it('should continue to display `selectedOptions` when getting unchecked', () => {
const onChange = jest.fn();
const [firstOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable selectedOptions={[firstOption]} onChange={onChange} />
);
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
expect.objectContaining({
key: firstOption.uid,
checked: 'on',
}),
]);
wrapper.setProps({ selectedOptions: [] }).update();
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
expect.objectContaining({
key: firstOption.uid,
checked: undefined,
}),
]);
});
it('should trigger `onSearchChange` callback when search term changes', () => {
const onSearchChange = jest.fn();
const wrapper = mount(<UserProfilesSelectable onSearchChange={onSearchChange} />);
wrapper.find('input[type="search"]').simulate('change', { target: { value: 'search' } });
expect(onSearchChange).toHaveBeenCalledWith('search', []);
});
});

View file

@ -1,319 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { EuiSelectableOption, EuiSelectableProps } from '@elastic/eui';
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiSelectable,
EuiSpacer,
EuiText,
EuiTextColor,
} from '@elastic/eui';
import type { FunctionComponent, ReactNode } from 'react';
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { getUserDisplayName } from './user_profile';
import type { UserProfileWithAvatar } from './user_avatar';
import { UserAvatar } from './user_avatar';
/**
* Props of {@link UserProfilesSelectable} component
*/
export interface UserProfilesSelectableProps
extends Pick<
EuiSelectableProps,
| 'height'
| 'singleSelection'
| 'loadingMessage'
| 'noMatchesMessage'
| 'emptyMessage'
| 'errorMessage'
> {
/**
* List of users to be rendered as suggestions.
*/
defaultOptions?: UserProfileWithAvatar[];
/**
* List of selected users.
*/
selectedOptions?: UserProfileWithAvatar[];
/**
* List of users from search results. Should be updated based on the search term provided by `onSearchChange` callback.
*/
options?: UserProfileWithAvatar[];
/**
* Passes back the list of selected users.
* @param options List of selected users
*/
onChange?(options: UserProfileWithAvatar[]): void;
/**
* Passes back the search term.
* @param searchTerm Search term
*/
onSearchChange?(searchTerm: string): void;
/**
* Loading indicator for asynchronous search operations.
*/
isLoading?: boolean;
/**
* Placeholder text for search box.
*/
searchPlaceholder?: string;
/**
* Returns text for selected status.
* @param selectedCount Number of selected users
*/
selectedStatusMessage?(selectedCount: number): ReactNode;
/**
* Text for label of clear button.
*/
clearButtonLabel?: ReactNode;
}
/**
* Renders a selectable component given a list of user profiles
*/
export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectableProps> = ({
selectedOptions,
defaultOptions,
options,
onChange,
onSearchChange,
isLoading = false,
singleSelection = false,
height,
loadingMessage,
noMatchesMessage,
emptyMessage,
errorMessage,
searchPlaceholder,
selectedStatusMessage,
clearButtonLabel,
}) => {
const [displayedOptions, setDisplayedOptions] = useState<SelectableOption[]>([]);
// Resets all displayed options
const resetDisplayedOptions = () => {
if (options) {
setDisplayedOptions(options.map(toSelectableOption));
return;
}
setDisplayedOptions([]);
updateDisplayedOptions();
};
const ensureSeparator = (values: SelectableOption[]) => {
let index = values.findIndex((option) => option.isGroupLabel);
if (index === -1) {
const length = values.push({
label: i18n.translate(
'sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel',
{
defaultMessage: 'Suggested',
}
),
isGroupLabel: true,
} as SelectableOption);
index = length - 1;
}
return index;
};
// Updates displayed options without removing or resorting exiting options
const updateDisplayedOptions = () => {
if (options) {
return;
}
setDisplayedOptions((values) => {
// Copy all displayed options
const nextOptions: SelectableOption[] = [...values];
// Get any newly added selected options
const selectedOptionsToAdd: SelectableOption[] = selectedOptions
? selectedOptions
.filter((profile) => !nextOptions.find((option) => option.key === profile.uid))
.map(toSelectableOption)
: [];
// Get any newly added default options
const defaultOptionsToAdd: SelectableOption[] = defaultOptions
? defaultOptions
.filter(
(profile) =>
!nextOptions.find((option) => option.key === profile.uid) &&
!selectedOptionsToAdd.find((option) => option.key === profile.uid)
)
.map(toSelectableOption)
: [];
// Merge in any new options and add group separator if necessary
if (defaultOptionsToAdd.length) {
const separatorIndex = ensureSeparator(nextOptions);
nextOptions.splice(separatorIndex, 0, ...selectedOptionsToAdd);
nextOptions.push(...defaultOptionsToAdd);
} else {
nextOptions.push(...selectedOptionsToAdd);
}
return nextOptions;
});
};
// Marks displayed options as checked or unchecked depending on `props.selectedOptions`
const updateCheckedStatus = () => {
setDisplayedOptions((values) =>
values.map((option) => {
if (selectedOptions) {
const match = selectedOptions.find((p) => p.uid === option.key);
return { ...option, checked: match ? 'on' : undefined };
}
return { ...option, checked: 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;
const placeholder =
searchPlaceholder ??
i18n.translate(
'sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder',
{
defaultMessage: 'Search',
}
);
return (
<EuiSelectable
options={displayedOptions}
// @ts-expect-error: Type of `nextOptions` in EuiSelectable does not match what's actually being passed back so need to manually override it
onChange={(nextOptions: Array<EuiSelectableOption<{ data: UserProfileWithAvatar }>>) => {
if (!onChange) {
return;
}
// Take all selected options from `nextOptions` unless already in `props.selectedOptions`
const values: UserProfileWithAvatar[] = nextOptions
.filter((option) => {
if (option.isGroupLabel || option.checked !== 'on') {
return false;
}
if (selectedOptions && selectedOptions.find((p) => p.uid === option.key)) {
return false;
}
return true;
})
.map((option) => option.data);
// Add all options from `props.selectedOptions` unless they have been deselected in `nextOptions`
if (selectedOptions && !singleSelection) {
selectedOptions.forEach((profile) => {
const match = nextOptions.find((o) => o.key === profile.uid);
if (!match || match.checked === 'on') {
values.push(profile);
}
});
}
onChange(values);
}}
style={{ maxHeight: height }}
singleSelection={singleSelection}
searchable
searchProps={{
placeholder,
onChange: onSearchChange,
isLoading,
isClearable: !isLoading,
}}
isPreFiltered
listProps={{ onFocusBadge: false }}
loadingMessage={loadingMessage}
noMatchesMessage={noMatchesMessage}
emptyMessage={emptyMessage}
errorMessage={errorMessage}
>
{(list, search) => (
<>
<EuiPanel hasShadow={false} paddingSize="s">
{search}
<EuiSpacer size="s" />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{selectedStatusMessage ? (
selectedStatusMessage(selectedCount)
) : (
<FormattedMessage
id="sharedUXPackages.userProfileComponents.userProfilesSelectable.selectedStatusMessage"
defaultMessage="{count, plural, one {# user selected} other {# users selected}}"
values={{ count: selectedCount }}
/>
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{selectedCount ? (
<EuiButtonEmpty
size="xs"
flush="right"
onClick={() => onChange?.([])}
style={{ height: '1rem' }}
>
{clearButtonLabel ?? (
<FormattedMessage
id="sharedUXPackages.userProfileComponents.userProfilesSelectable.clearButtonLabel"
defaultMessage="Remove all users"
/>
)}
</EuiButtonEmpty>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiHorizontalRule margin="none" />
{list}
</>
)}
</EuiSelectable>
);
};
type SelectableOption = EuiSelectableOption<UserProfileWithAvatar>;
function toSelectableOption(userProfile: UserProfileWithAvatar): SelectableOption {
// @ts-ignore: `isGroupLabel` is not required here but TS complains
return {
key: userProfile.uid,
prepend: <UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} size="s" />,
label: getUserDisplayName(userProfile.user),
append: <EuiTextColor color="subdued">{userProfile.user.email}</EuiTextColor>,
data: userProfile,
};
}

View file

@ -14,13 +14,13 @@
"**/*.tsx",
],
"kbn_references": [
"@kbn/shared-ux-avatar-solution",
"@kbn/shared-ux-card-no-data",
"@kbn/shared-ux-page-no-data-types",
"@kbn/test-jest-helpers",
"@kbn/shared-ux-page-no-data-mocks",
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/shared-ux-avatar-solution",
],
"exclude": [
"target/**/*",

View file

@ -23,9 +23,9 @@
"@kbn/test-jest-helpers",
"@kbn/shared-ux-page-kibana-template",
"@kbn/shared-ux-page-analytics-no-data",
"@kbn/shared-ux-avatar-solution",
"@kbn/shared-ux-link-redirect-app",
"@kbn/shared-ux-router",
"@kbn/shared-ux-avatar-solution",
],
"exclude": [
"target/**/*",

View file

@ -1346,8 +1346,6 @@
"@kbn/shared-svg/*": ["packages/kbn-shared-svg/*"],
"@kbn/shared-ux-avatar-solution": ["packages/shared-ux/avatar/solution"],
"@kbn/shared-ux-avatar-solution/*": ["packages/shared-ux/avatar/solution/*"],
"@kbn/shared-ux-avatar-user-profile-components": ["packages/shared-ux/avatar/user_profile/impl"],
"@kbn/shared-ux-avatar-user-profile-components/*": ["packages/shared-ux/avatar/user_profile/impl/*"],
"@kbn/shared-ux-button-exit-full-screen": ["packages/shared-ux/button/exit_full_screen/impl"],
"@kbn/shared-ux-button-exit-full-screen/*": ["packages/shared-ux/button/exit_full_screen/impl/*"],
"@kbn/shared-ux-button-exit-full-screen-mocks": ["packages/shared-ux/button/exit_full_screen/mocks"],

View file

@ -16,7 +16,6 @@
"@kbn/core",
"@kbn/test-jest-helpers",
"@kbn/i18n-react",
"@kbn/shared-ux-avatar-solution",
"@kbn/core-saved-objects-api-server",
"@kbn/config-schema",
"@kbn/utility-types",
@ -32,6 +31,7 @@
"@kbn/core-custom-branding-common",
"@kbn/shared-ux-link-redirect-app",
"@kbn/config",
"@kbn/shared-ux-avatar-solution",
],
"exclude": [
"target/**/*",

View file

@ -5214,7 +5214,6 @@
"sharedUXPackages.noDataPage.intro": "Ajoutez vos données pour commencer, ou {link} sur {solution}.",
"sharedUXPackages.noDataPage.welcomeTitle": "Bienvenue dans Elastic {solution} !",
"sharedUXPackages.solutionNav.mobileTitleText": "{solutionName} {menuText}",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.selectedStatusMessage": "{count, plural, one {# utilisateur sélectionné} many {# utilisateurs sélectionnés} other {# utilisateurs sélectionnés}}",
"sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "Ajouter depuis la bibliothèque",
"sharedUXPackages.buttonToolbar.toolbar.errorToolbarText": "Il y a plus de 120 boutons supplémentaires. Nous vous invitons à limiter le nombre de boutons.",
"sharedUXPackages.card.noData.description": "Utilisez Elastic Agent pour collecter de manière simple et unifiée les données de vos machines.",
@ -5268,9 +5267,6 @@
"sharedUXPackages.solutionNav.collapsibleLabel": "Réduire la navigation latérale",
"sharedUXPackages.solutionNav.menuText": "menu",
"sharedUXPackages.solutionNav.openLabel": "Ouvrir la navigation latérale",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.clearButtonLabel": "Retirer tous les utilisateurs",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder": "Recherche",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "Suggérée",
"telemetry.callout.appliesSettingTitle": "Les modifications apportées à ce paramètre s'appliquent dans {allOfKibanaText} et sont enregistrées automatiquement.",
"telemetry.dataManagementDisclaimerPrivacy": "{optInStatus} Ceci nous permet de savoir ce qui intéresse le plus nos utilisateurs, afin daméliorer nos produits et services. Consultez notre {privacyStatementLink}.",
"telemetry.seeExampleOfClusterDataAndEndpointSecuity": "Découvrez des exemples des {clusterData} et {securityData} que nous collectons.",

View file

@ -5230,7 +5230,6 @@
"sharedUXPackages.noDataPage.intro": "データを追加して開始するか、{solution}については{link}をご覧ください。",
"sharedUXPackages.noDataPage.welcomeTitle": "Elastic {solution}へようこそ!",
"sharedUXPackages.solutionNav.mobileTitleText": "{solutionName} {menuText}",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.selectedStatusMessage": "{count, plural, other {#人のユーザーが選択されました}}",
"sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "ライブラリから追加",
"sharedUXPackages.buttonToolbar.toolbar.errorToolbarText": "120以上のボタンがあります。ボタンの数を制限することを検討してください。",
"sharedUXPackages.card.noData.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。",
@ -5284,9 +5283,6 @@
"sharedUXPackages.solutionNav.collapsibleLabel": "サイドナビゲーションを折りたたむ",
"sharedUXPackages.solutionNav.menuText": "メニュー",
"sharedUXPackages.solutionNav.openLabel": "サイドナビゲーションを開く",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.clearButtonLabel": "すべてのユーザーを削除",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder": "検索",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "候補",
"telemetry.callout.appliesSettingTitle": "この設定に加えた変更は{allOfKibanaText}に適用され、自動的に保存されます。",
"telemetry.dataManagementDisclaimerPrivacy": "{optInStatus} これにより、ユーザーが最も関心を持っている項目を把握できるため、製品とサービスを改善できます。{privacyStatementLink}を参照してください。",
"telemetry.seeExampleOfClusterDataAndEndpointSecuity": "収集される{clusterData}および{securityData}の例を参照してください。",

View file

@ -5229,7 +5229,6 @@
"sharedUXPackages.noDataPage.intro": "添加您的数据以开始,或{link}{solution}。",
"sharedUXPackages.noDataPage.welcomeTitle": "欢迎使用 Elastic {solution}",
"sharedUXPackages.solutionNav.mobileTitleText": "{solutionName} {menuText}",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.selectedStatusMessage": "{count, plural, other {# 个用户已选择}}",
"sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "从库中添加",
"sharedUXPackages.buttonToolbar.toolbar.errorToolbarText": "有 120 多个附加按钮。请考虑限制按钮数量。",
"sharedUXPackages.card.noData.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。",
@ -5283,9 +5282,6 @@
"sharedUXPackages.solutionNav.collapsibleLabel": "折叠侧边导航",
"sharedUXPackages.solutionNav.menuText": "菜单",
"sharedUXPackages.solutionNav.openLabel": "打开侧边导航",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.clearButtonLabel": "移除所有用户",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder": "搜索",
"sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "已建议",
"telemetry.callout.appliesSettingTitle": "对此设置的更改将应用到{allOfKibanaText} 且会自动保存。",
"telemetry.dataManagementDisclaimerPrivacy": "{optInStatus} 这便于我们了解用户最感兴趣的内容,以便我们改善产品和服务。请参阅我们的{privacyStatementLink}。",
"telemetry.seeExampleOfClusterDataAndEndpointSecuity": "查看我们收集的{clusterData}和{securityData}示例。",

View file

@ -5596,10 +5596,6 @@
version "0.0.0"
uid ""
"@kbn/shared-ux-avatar-user-profile-components@link:packages/shared-ux/avatar/user_profile/impl":
version "0.0.0"
uid ""
"@kbn/shared-ux-button-exit-full-screen-mocks@link:packages/shared-ux/button/exit_full_screen/mocks":
version "0.0.0"
uid ""