mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
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:
parent
aa7d8e8ab7
commit
a0731f139e
43 changed files with 1719 additions and 79 deletions
3
examples/user_profile_examples/README.md
Normal file
3
examples/user_profile_examples/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# User profile examples
|
||||
|
||||
Demo of how to implement a suggest user functionality.
|
14
examples/user_profile_examples/kibana.json
Normal file
14
examples/user_profile_examples/kibana.json
Normal 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": []
|
||||
}
|
66
examples/user_profile_examples/public/avatar_demo.tsx
Normal file
66
examples/user_profile_examples/public/avatar_demo.tsx
Normal 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 }}
|
||||
/>
|
||||
 
|
||||
<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} />`;
|
10
examples/user_profile_examples/public/index.ts
Executable file
10
examples/user_profile_examples/public/index.ts
Executable 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();
|
|
@ -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" />
|
||||
</>
|
||||
);
|
68
examples/user_profile_examples/public/plugin.tsx
Executable file
68
examples/user_profile_examples/public/plugin.tsx
Executable 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() {}
|
||||
}
|
106
examples/user_profile_examples/public/popover_demo.tsx
Normal file
106
examples/user_profile_examples/public/popover_demo.tsx
Normal 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
|
||||
}}
|
||||
/>`;
|
80
examples/user_profile_examples/public/selectable_demo.tsx
Normal file
80
examples/user_profile_examples/public/selectable_demo.tsx
Normal 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} />`;
|
10
examples/user_profile_examples/server/index.ts
Executable file
10
examples/user_profile_examples/server/index.ts
Executable 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();
|
83
examples/user_profile_examples/server/plugin.ts
Normal file
83
examples/user_profile_examples/server/plugin.ts
Normal 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 {};
|
||||
}
|
||||
}
|
19
examples/user_profile_examples/tsconfig.json
Normal file
19
examples/user_profile_examples/tsconfig.json
Normal 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" }
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue