Add user profile selectable (#137424)

* Add reusable user profile selector component

* Move to package and add examples

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Add server side example

* CI Fixes

* fix tests

* Addd tests

* Addressed suggestions from code review

* Fix types

* Updated user avatar component

* Tweak styling and copy

* Add missing jsdoc comments

* .

* .

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Thom Heymann 2022-08-04 22:09:01 +01:00 committed by GitHub
parent aa7d8e8ab7
commit a0731f139e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1719 additions and 79 deletions

View file

@ -0,0 +1,3 @@
# User profile examples
Demo of how to implement a suggest user functionality.

View file

@ -0,0 +1,14 @@
{
"id": "userProfileExamples",
"kibanaVersion": "kibana",
"version": "0.0.1",
"server": true,
"ui": true,
"owner": {
"name": "Kibana Platform Security",
"githubTeam": "kibana-security"
},
"description": "Demo of how to implement a suggest user functionality",
"requiredPlugins": ["developerExamples", "security", "spaces"],
"optionalPlugins": []
}

View file

@ -0,0 +1,66 @@
/*
* 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, { FunctionComponent } from 'react';
import { EuiTitle, EuiSpacer } from '@elastic/eui';
import { UserAvatar } from '@kbn/user-profile-components';
import { PanelWithCodeBlock } from './panel_with_code_block';
export const AvatarDemo: FunctionComponent = () => {
const userProfile = {
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
full_name: 'Delighted Nightingale',
},
data: {
avatar: {
color: '#09e8ca',
initials: 'DN',
imageUrl: 'https://source.unsplash.com/64x64/?cat',
},
},
};
return (
<PanelWithCodeBlock title="Avatar" code={code}>
<UserAvatar
user={userProfile.user}
avatar={{ ...userProfile.data.avatar, imageUrl: undefined }}
/>
&ensp;
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} />
<EuiSpacer size="l" />
<EuiTitle size="xs">
<h3>Unknown</h3>
</EuiTitle>
<EuiSpacer size="s" />
<UserAvatar />
</PanelWithCodeBlock>
);
};
const code = `import { UserAvatar } from '@kbn/user-profile-components';
const userProfile = {
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
full_name: 'Delighted Nightingale',
},
data: {
avatar: {
color: '#09e8ca',
initials: 'DN',
imageUrl: 'https://source.unsplash.com/64x64/?cat'
}
},
};
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} />`;

View file

@ -0,0 +1,10 @@
/*
* 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 { UserProfilesPlugin } from './plugin';
export const plugin = () => new UserProfilesPlugin();

View file

@ -0,0 +1,36 @@
/*
* 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 { EuiTitle, EuiSpacer, EuiSplitPanel, EuiCodeBlock } from '@elastic/eui';
export interface PanelWithCodeBlockProps {
title: string;
code: string;
}
export const PanelWithCodeBlock: React.FunctionComponent<PanelWithCodeBlockProps> = ({
title,
code,
children,
}) => (
<>
<EuiTitle>
<h2>{title}</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiSplitPanel.Outer hasBorder>
<EuiSplitPanel.Inner>{children}</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner color="subdued">
<EuiCodeBlock language="jsx" paddingSize="none" transparentBackground>
{code}
</EuiCodeBlock>
</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
<EuiSpacer size="l" />
</>
);

View file

@ -0,0 +1,68 @@
/*
* 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 ReactDOM from 'react-dom';
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
import { EuiPageTemplate } from '@elastic/eui';
import { AvatarDemo } from './avatar_demo';
import { PopoverDemo } from './popover_demo';
import { SelectableDemo } from './selectable_demo';
interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
security: SecurityPluginSetup;
}
interface StartDeps {
security: SecurityPluginStart;
}
export class UserProfilesPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
public setup(core: CoreSetup<StartDeps>, deps: SetupDeps) {
// Register an application into the side navigation menu
core.application.register({
id: 'userProfileExamples',
title: 'User profile components',
async mount({ element }: AppMountParameters) {
// Fetch user suggestions
// const [, depsStart] = await core.getStartServices();
// depsStart.security.userProfiles.suggest('/internal/user_profiles_examples/_suggest', {
// name: 'a',
// });
ReactDOM.render(
<EuiPageTemplate
pageHeader={{
pageTitle: 'User profile components',
}}
>
<AvatarDemo />
<SelectableDemo />
<PopoverDemo />
</EuiPageTemplate>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
},
});
deps.developerExamples.register({
appId: 'userProfileExamples',
title: 'User Profile',
description: 'Demo of how to implement a suggest user functionality',
});
}
public start(core: CoreStart) {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,106 @@
/*
* 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, { FunctionComponent, useState } from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { UserProfilesPopover, UserProfileWithAvatar } from '@kbn/user-profile-components';
import { PanelWithCodeBlock } from './panel_with_code_block';
export const PopoverDemo: FunctionComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOptions, setSelectedOptions] = useState<UserProfileWithAvatar[]>([
{
uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0',
data: {},
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
full_name: 'Delighted Nightingale',
},
},
]);
const defaultOptions: UserProfileWithAvatar[] = [
{
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
data: {},
user: {
username: 'damaged_raccoon',
email: 'damaged_raccoon@elastic.co',
full_name: 'Damaged Raccoon',
},
},
{
uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0',
data: {},
user: {
username: 'physical_dinosaur',
email: 'physical_dinosaur@elastic.co',
full_name: 'Physical Dinosaur',
},
},
{
uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0',
data: {},
user: {
username: 'wet_dingo',
email: 'wet_dingo@elastic.co',
full_name: 'Wet Dingo',
},
},
];
return (
<PanelWithCodeBlock title="Popover" code={code}>
<UserProfilesPopover
title="Edit assignees"
button={
<EuiButtonEmpty iconType="pencil" onClick={() => setIsOpen((value) => !value)}>
Edit assignees
</EuiButtonEmpty>
}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
selectableProps={{
selectedOptions,
defaultOptions,
onChange: setSelectedOptions,
height: 32 * 8,
}}
panelStyle={{
width: 32 * 16,
}}
/>
</PanelWithCodeBlock>
);
};
const code = `import { UserProfilesPopover } from '@kbn/user-profile-components';
const [selectedOptions, setSelectedOptions] = useState([
{
uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0',
data: {},
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
full_name: 'Delighted Nightingale',
},
},
]);
<UserProfilesPopover
title="Edit assignees"
button={
<EuiButton>
Edit assignees
</EuiButton>
}
selectableProps={{
selectedOptions,
onChange: setSelectedOptions
}}
/>`;

View file

@ -0,0 +1,80 @@
/*
* 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, { FunctionComponent, useState } from 'react';
import { UserProfilesSelectable, UserProfileWithAvatar } from '@kbn/user-profile-components';
import { PanelWithCodeBlock } from './panel_with_code_block';
export const SelectableDemo: FunctionComponent = () => {
const [selectedOptions, setSelectedOptions] = useState<UserProfileWithAvatar[]>([
{
uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0',
data: {},
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
full_name: 'Delighted Nightingale',
},
},
]);
const defaultOptions: UserProfileWithAvatar[] = [
{
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
data: {},
user: {
username: 'damaged_raccoon',
email: 'damaged_raccoon@elastic.co',
full_name: 'Damaged Raccoon',
},
},
{
uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0',
data: {},
user: {
username: 'physical_dinosaur',
email: 'physical_dinosaur@elastic.co',
full_name: 'Physical Dinosaur',
},
},
{
uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0',
data: {},
user: {
username: 'wet_dingo',
email: 'wet_dingo@elastic.co',
full_name: 'Wet Dingo',
},
},
];
return (
<PanelWithCodeBlock title="Selectable" code={code}>
<UserProfilesSelectable
selectedOptions={selectedOptions}
defaultOptions={defaultOptions}
onChange={setSelectedOptions}
/>
</PanelWithCodeBlock>
);
};
const code = `import { UserProfilesSelectable } from '@kbn/user-profile-components';
const [selectedOptions, setSelectedOptions] = useState([
{
uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0',
data: {},
user: {
username: 'delighted_nightingale',
email: 'delighted_nightingale@elastic.co',
full_name: 'Delighted Nightingale',
},
},
]);
<UserProfilesSelectable selectedOptions={selectedOptions} onChange={setSelectedOptions} />`;

View file

@ -0,0 +1,10 @@
/*
* 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 { UserProfilesPlugin } from './plugin';
export const plugin = () => new UserProfilesPlugin();

View file

@ -0,0 +1,83 @@
/*
* 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 { Plugin, CoreSetup } from '@kbn/core/server';
import {
PluginSetupContract as FeaturesPluginSetup,
PluginStartContract as FeaturesPluginStart,
} from '@kbn/features-plugin/server';
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
import { schema } from '@kbn/config-schema';
export interface SetupDeps {
features: FeaturesPluginSetup;
security: SecurityPluginSetup;
spaces: SpacesPluginSetup;
}
export interface StartDeps {
features: FeaturesPluginStart;
security: SecurityPluginStart;
spaces: SpacesPluginStart;
}
export class UserProfilesPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
setup(core: CoreSetup<StartDeps>) {
const router = core.http.createRouter();
router.post(
{
path: '/internal/user_profiles_examples/_suggest',
validate: {
body: schema.object({
name: schema.string(),
dataPath: schema.maybe(schema.string()),
}),
},
/**
* Important: You must restrict access to this endpoint using access `tags`.
*/
options: { tags: ['access:suggestUserProfiles'] },
},
async (context, request, response) => {
const [, pluginDeps] = await core.getStartServices();
/**
* Important: `requiredPrivileges` must be hard-coded server-side and cannot be exposed as a
* param client-side.
*
* If your app requires suggestions based on different privileges you must expose separate
* endpoints for each use-case.
*
* In this example we ensure that suggested users have access to the current space and are
* able to login but in your app you will want to change that to something more relevant.
*/
const profiles = await pluginDeps.security.userProfiles.suggest({
name: request.body.name,
dataPath: request.body.dataPath,
requiredPrivileges: {
spaceId: pluginDeps.spaces.spacesService.getSpaceId(request),
privileges: {
kibana: [pluginDeps.security.authz.actions.login],
},
},
});
return response.ok({ body: profiles });
}
);
}
start() {
return {};
}
stop() {
return {};
}
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types"
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../src/core/tsconfig.json" },
{ "path": "../../x-pack/plugins/security/tsconfig.json" },
{ "path": "../developer_examples/tsconfig.json" }
]
}