diff --git a/package.json b/package.json
index 38cf150094677b..77e466cc8bca4e 100644
--- a/package.json
+++ b/package.json
@@ -283,7 +283,7 @@
"react-resizable": "^1.7.5",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
- "react-use": "^13.27.0",
+ "react-use": "^15.3.4",
"recompose": "^0.26.0",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
diff --git a/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx b/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx
index 4f64a2b95f512f..23500e8480ebb8 100644
--- a/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx
+++ b/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx
@@ -54,16 +54,12 @@ test('can display string element as title', () => {
expect(notifications.toasts.add).toHaveBeenCalledTimes(1);
expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
- "color": undefined,
- "iconType": undefined,
- "onClose": undefined,
"text": MountPoint {
"reactNode": ,
},
"title": MountPoint {
"reactNode": "foo",
},
- "toastLifeTimeMs": undefined,
}
`);
});
@@ -120,7 +116,6 @@ test('can set toast properties', () => {
Object {
"color": "danger",
"iconType": "foo",
- "onClose": undefined,
"text": MountPoint {
"reactNode":
1
@@ -147,42 +142,36 @@ test('can display success, warning and danger toasts', () => {
Object {
"color": "success",
"iconType": "check",
- "onClose": undefined,
"text": MountPoint {
"reactNode": ,
},
"title": MountPoint {
"reactNode": "1",
},
- "toastLifeTimeMs": undefined,
}
`);
expect(notifications.toasts.add.mock.calls[1][0]).toMatchInlineSnapshot(`
Object {
"color": "warning",
"iconType": "help",
- "onClose": undefined,
"text": MountPoint {
"reactNode": ,
},
"title": MountPoint {
"reactNode": "2",
},
- "toastLifeTimeMs": undefined,
}
`);
expect(notifications.toasts.add.mock.calls[2][0]).toMatchInlineSnapshot(`
Object {
"color": "danger",
"iconType": "alert",
- "onClose": undefined,
"text": MountPoint {
"reactNode": ,
},
"title": MountPoint {
"reactNode": "3",
},
- "toastLifeTimeMs": undefined,
}
`);
});
diff --git a/src/plugins/kibana_react/public/notifications/create_notifications.tsx b/src/plugins/kibana_react/public/notifications/create_notifications.tsx
index 826435ec5b587a..dc6430e2f18a95 100644
--- a/src/plugins/kibana_react/public/notifications/create_notifications.tsx
+++ b/src/plugins/kibana_react/public/notifications/create_notifications.tsx
@@ -23,24 +23,14 @@ import { KibanaReactNotifications } from './types';
import { toMountPoint } from '../util';
export const createNotifications = (services: KibanaServices): KibanaReactNotifications => {
- const show: KibanaReactNotifications['toasts']['show'] = ({
- title,
- body,
- color,
- iconType,
- toastLifeTimeMs,
- onClose,
- }) => {
+ const show: KibanaReactNotifications['toasts']['show'] = ({ title, body, ...rest }) => {
if (!services.notifications) {
throw new TypeError('Could not show notification as notifications service is not available.');
}
services.notifications!.toasts.add({
title: toMountPoint(title),
text: toMountPoint(<>{body || null}>),
- color,
- iconType,
- toastLifeTimeMs,
- onClose,
+ ...rest,
});
};
diff --git a/x-pack/plugins/security/public/account_management/account_details_page/account_details_page.tsx b/x-pack/plugins/security/public/account_management/account_details_page/account_details_page.tsx
new file mode 100644
index 00000000000000..0d446a631a37df
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/account_details_page/account_details_page.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { ChangePassword } from './change_password';
+import { PersonalInfo } from './personal_info';
+import { AuthenticatedUser } from '../../../common/model';
+import { UserAPIClient } from '../../management';
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { NotificationsSetup } from '../../../../../../src/core/public';
+
+export interface AccountDetailsPageProps {
+ user: AuthenticatedUser;
+ notifications: NotificationsSetup;
+}
+
+export const AccountDetailsPage: FunctionComponent = ({
+ user,
+ notifications,
+}) => {
+ const { services } = useKibana();
+ const userAPIClient = new UserAPIClient(services.http!);
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/account_details_page/change_password.tsx
similarity index 100%
rename from x-pack/plugins/security/public/account_management/change_password/change_password.tsx
rename to x-pack/plugins/security/public/account_management/account_details_page/change_password.tsx
diff --git a/x-pack/plugins/security/public/account_management/change_password/index.ts b/x-pack/plugins/security/public/account_management/account_details_page/index.ts
similarity index 63%
rename from x-pack/plugins/security/public/account_management/change_password/index.ts
rename to x-pack/plugins/security/public/account_management/account_details_page/index.ts
index ccd810bb814c00..a1b31254e87cb2 100644
--- a/x-pack/plugins/security/public/account_management/change_password/index.ts
+++ b/x-pack/plugins/security/public/account_management/account_details_page/index.ts
@@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export { AccountDetailsPage as default } from './account_details_page'; // eslint-disable-line import/no-default-export
export { ChangePassword } from './change_password';
+export { PersonalInfo } from './personal_info';
diff --git a/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx b/x-pack/plugins/security/public/account_management/account_details_page/personal_info.tsx
similarity index 59%
rename from x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx
rename to x-pack/plugins/security/public/account_management/account_details_page/personal_info.tsx
index 9cbbc242e8400e..a46b10ea997b8f 100644
--- a/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx
+++ b/x-pack/plugins/security/public/account_management/account_details_page/personal_info.tsx
@@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
-import { EuiDescribedFormGroup, EuiFormRow, EuiText } from '@elastic/eui';
+import {
+ EuiDescribedFormGroup,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AuthenticatedUser } from '../../../common/model';
@@ -31,23 +36,19 @@ export const PersonalInfo = (props: Props) => {
/>
}
>
-
-
-
- -
- {props.user.username}
-
- -
- {props.user.email || (
-
- )}
-
-
-
-
+
+
+ {props.user.username}
+
+
+ {props.user.email || (
+
+ )}
+
+
);
};
diff --git a/x-pack/plugins/security/public/account_management/account_management_app.test.ts b/x-pack/plugins/security/public/account_management/account_management_app.test.ts
deleted file mode 100644
index c41bd43872beed..00000000000000
--- a/x-pack/plugins/security/public/account_management/account_management_app.test.ts
+++ /dev/null
@@ -1,74 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-jest.mock('./account_management_page');
-
-import { AppMount, AppNavLinkStatus } from 'src/core/public';
-import { UserAPIClient } from '../management';
-import { accountManagementApp } from './account_management_app';
-
-import { coreMock, scopedHistoryMock } from '../../../../../src/core/public/mocks';
-import { securityMock } from '../mocks';
-
-describe('accountManagementApp', () => {
- it('properly registers application', () => {
- const coreSetupMock = coreMock.createSetup();
-
- accountManagementApp.create({
- application: coreSetupMock.application,
- getStartServices: coreSetupMock.getStartServices,
- authc: securityMock.createSetup().authc,
- });
-
- expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1);
-
- const [[appRegistration]] = coreSetupMock.application.register.mock.calls;
- expect(appRegistration).toEqual({
- id: 'security_account',
- appRoute: '/security/account',
- navLinkStatus: AppNavLinkStatus.hidden,
- title: 'Account Management',
- mount: expect.any(Function),
- });
- });
-
- it('properly sets breadcrumbs and renders application', async () => {
- const coreSetupMock = coreMock.createSetup();
- const coreStartMock = coreMock.createStart();
- coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
-
- const authcMock = securityMock.createSetup().authc;
- const containerMock = document.createElement('div');
-
- accountManagementApp.create({
- application: coreSetupMock.application,
- getStartServices: coreSetupMock.getStartServices,
- authc: authcMock,
- });
-
- const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
- await (mount as AppMount)({
- element: containerMock,
- appBasePath: '',
- onAppLeave: jest.fn(),
- setHeaderActionMenu: jest.fn(),
- history: scopedHistoryMock.create(),
- });
-
- expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenCalledTimes(1);
- expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([
- { text: 'Account Management' },
- ]);
-
- const mockRenderApp = jest.requireMock('./account_management_page').renderAccountManagementPage;
- expect(mockRenderApp).toHaveBeenCalledTimes(1);
- expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
- userAPIClient: expect.any(UserAPIClient),
- authc: authcMock,
- notifications: coreStartMock.notifications,
- });
- });
-});
diff --git a/x-pack/plugins/security/public/account_management/account_management_app.test.tsx b/x-pack/plugins/security/public/account_management/account_management_app.test.tsx
new file mode 100644
index 00000000000000..c5e7784e46fa43
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/account_management_app.test.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import ReactDOM from 'react-dom';
+import { act, screen, fireEvent } from '@testing-library/react';
+import { AppMount, AppNavLinkStatus, ScopedHistory } from '../../../../../src/core/public';
+import { coreMock } from '../../../../../src/core/public/mocks';
+import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
+import { securityMock } from '../mocks';
+import { accountManagementApp } from './account_management_app';
+import { createMemoryHistory } from 'history';
+
+describe('accountManagementApp', () => {
+ it('registers application', () => {
+ const { application, getStartServices } = coreMock.createSetup();
+ const { authc } = securityMock.createSetup();
+ accountManagementApp.create({ application, getStartServices, authc });
+ expect(application.register).toHaveBeenLastCalledWith({
+ id: 'security_account',
+ appRoute: '/security/account',
+ navLinkStatus: AppNavLinkStatus.hidden,
+ title: 'Account Management',
+ mount: expect.any(Function),
+ });
+ });
+
+ it('renders application and sets breadcrumbs', async () => {
+ const { application, getStartServices } = coreMock.createSetup();
+ const coreStartMock = coreMock.createStart();
+ getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
+ const { authc } = securityMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(
+ mockAuthenticatedUser({ username: 'some-user', full_name: undefined })
+ );
+ accountManagementApp.create({ application, getStartServices, authc });
+ const [[{ mount }]] = application.register.mock.calls;
+ const element = document.body.appendChild(document.createElement('div'));
+
+ await act(async () => {
+ await (mount as AppMount)({
+ element,
+ appBasePath: '',
+ onAppLeave: jest.fn(),
+ setHeaderActionMenu: jest.fn(),
+ history: (createMemoryHistory() as unknown) as ScopedHistory,
+ });
+ });
+ expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenLastCalledWith([
+ expect.objectContaining({ text: 'Account Management' }),
+ ]);
+
+ fireEvent.click(screen.getByRole('tab', { name: 'API Keys' }));
+ expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenLastCalledWith([
+ expect.objectContaining({ text: 'Account Management' }),
+ expect.objectContaining({ text: 'API Keys' }),
+ ]);
+
+ // Need to cleanup manually since `mount` renders the app straight to the DOM
+ ReactDOM.unmountComponentAtNode(element);
+ document.body.removeChild(element);
+ });
+});
diff --git a/x-pack/plugins/security/public/account_management/account_management_app.ts b/x-pack/plugins/security/public/account_management/account_management_app.ts
deleted file mode 100644
index 0bb7785259c0ea..00000000000000
--- a/x-pack/plugins/security/public/account_management/account_management_app.ts
+++ /dev/null
@@ -1,54 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-import {
- ApplicationSetup,
- AppMountParameters,
- AppNavLinkStatus,
- StartServicesAccessor,
-} from '../../../../../src/core/public';
-import { AuthenticationServiceSetup } from '../authentication';
-
-interface CreateDeps {
- application: ApplicationSetup;
- authc: AuthenticationServiceSetup;
- getStartServices: StartServicesAccessor;
-}
-
-export const accountManagementApp = Object.freeze({
- id: 'security_account',
- create({ application, authc, getStartServices }: CreateDeps) {
- const title = i18n.translate('xpack.security.account.breadcrumb', {
- defaultMessage: 'Account Management',
- });
- application.register({
- id: this.id,
- title,
- navLinkStatus: AppNavLinkStatus.hidden,
- appRoute: '/security/account',
- async mount({ element }: AppMountParameters) {
- const [
- [coreStart],
- { renderAccountManagementPage },
- { UserAPIClient },
- ] = await Promise.all([
- getStartServices(),
- import('./account_management_page'),
- import('../management'),
- ]);
-
- coreStart.chrome.setBreadcrumbs([{ text: title }]);
-
- return renderAccountManagementPage(coreStart.i18n, element, {
- authc,
- notifications: coreStart.notifications,
- userAPIClient: new UserAPIClient(coreStart.http),
- });
- },
- });
- },
-});
diff --git a/x-pack/plugins/security/public/account_management/account_management_app.tsx b/x-pack/plugins/security/public/account_management/account_management_app.tsx
new file mode 100644
index 00000000000000..e4bab6bddf3224
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/account_management_app.tsx
@@ -0,0 +1,176 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { lazy, Suspense, FunctionComponent } from 'react';
+import ReactDOM from 'react-dom';
+import { Router, useHistory } from 'react-router-dom';
+import { History } from 'history';
+import useAsync from 'react-use/lib/useAsync';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiAvatar,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiPageContentHeader,
+ EuiPageContentHeaderSection,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import {
+ ApplicationSetup,
+ AppMountParameters,
+ AppNavLinkStatus,
+ StartServicesAccessor,
+ NotificationsSetup,
+ CoreStart,
+} from '../../../../../src/core/public';
+import {
+ KibanaContextProvider,
+ reactRouterNavigate,
+} from '../../../../../src/plugins/kibana_react/public';
+import { getUserDisplayName } from '../../common/model';
+import { AuthenticationServiceSetup } from '../authentication';
+import { TabbedRoutes } from './components/tabbed_routes';
+import { Breadcrumb } from './components/breadcrumb';
+
+interface CreateDeps {
+ application: ApplicationSetup;
+ authc: AuthenticationServiceSetup;
+ getStartServices: StartServicesAccessor;
+}
+
+export const accountManagementApp = Object.freeze({
+ id: 'security_account',
+ create({ application, authc, getStartServices }: CreateDeps) {
+ const title = i18n.translate('xpack.security.account.breadcrumb', {
+ defaultMessage: 'Account Management',
+ });
+
+ application.register({
+ id: this.id,
+ title,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ appRoute: '/security/account',
+ async mount({ element, history }: AppMountParameters) {
+ const [coreStart] = await getStartServices();
+
+ ReactDOM.render(
+
+
+
+
+ ,
+ element
+ );
+
+ return () => ReactDOM.unmountComponentAtNode(element);
+ },
+ });
+ },
+});
+
+export interface ProvidersProps {
+ services: CoreStart;
+ history: History;
+}
+
+export const Providers: FunctionComponent = ({ services, history, children }) => (
+
+
+ {children}
+
+
+);
+
+export interface AccountManagementProps {
+ authc: AuthenticationServiceSetup;
+ notifications: NotificationsSetup;
+}
+
+export const AccountManagement: FunctionComponent = ({
+ authc,
+ notifications,
+}) => {
+ const history = useHistory();
+ const userState = useAsync(authc.getCurrentUser, [authc]);
+ const AccountDetailsPage = lazy(() => import('./account_details_page'));
+ const ApiKeysPage = lazy(() => import('./api_keys_page'));
+
+ if (!userState.value) {
+ return null;
+ }
+
+ const displayName = getUserDisplayName(userState.value);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {displayName}
+
+ {userState.value.email}
+
+
+
+
+
+ }>
+
+
+ ),
+ },
+ {
+ id: 'api-keys',
+ path: '/api-keys',
+ to: 'api-keys',
+ name: i18n.translate('xpack.security.accountManagement.ApiKeysTab', {
+ defaultMessage: 'API Keys',
+ }),
+ content: (
+
+ }>
+
+
+
+ ),
+ },
+ ]}
+ />
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx
deleted file mode 100644
index b677f6a1fe8bba..00000000000000
--- a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx
+++ /dev/null
@@ -1,140 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { act } from '@testing-library/react';
-import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
-import { AuthenticatedUser } from '../../common/model';
-import { AccountManagementPage } from './account_management_page';
-import { coreMock } from 'src/core/public/mocks';
-import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
-import { securityMock } from '../mocks';
-import { userAPIClientMock } from '../management/users/index.mock';
-
-interface Options {
- withFullName?: boolean;
- withEmail?: boolean;
- realm?: string;
-}
-const createUser = ({ withFullName = true, withEmail = true, realm = 'native' }: Options = {}) => {
- return mockAuthenticatedUser({
- full_name: withFullName ? 'Casey Smith' : '',
- username: 'csmith',
- email: withEmail ? 'csmith@domain.com' : '',
- roles: [],
- authentication_realm: {
- type: realm,
- name: realm,
- },
- lookup_realm: {
- type: realm,
- name: realm,
- },
- });
-};
-
-function getSecuritySetupMock({ currentUser }: { currentUser: AuthenticatedUser }) {
- const securitySetupMock = securityMock.createSetup();
- securitySetupMock.authc.getCurrentUser.mockResolvedValue(currentUser);
- return securitySetupMock;
-}
-
-describe('', () => {
- it(`displays users full name, username, and email address`, async () => {
- const user = createUser();
- const wrapper = mountWithIntl(
-
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
-
- expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(
- user.full_name
- );
- expect(wrapper.find('[data-test-subj="username"]').text()).toEqual(user.username);
- expect(wrapper.find('[data-test-subj="email"]').text()).toEqual(user.email);
- });
-
- it(`displays username when full_name is not provided`, async () => {
- const user = createUser({ withFullName: false });
- const wrapper = mountWithIntl(
-
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
-
- expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(user.username);
- });
-
- it(`displays a placeholder when no email address is provided`, async () => {
- const user = createUser({ withEmail: false });
- const wrapper = mountWithIntl(
-
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
-
- expect(wrapper.find('[data-test-subj="email"]').text()).toEqual('no email address');
- });
-
- it(`displays change password form for users in the native realm`, async () => {
- const user = createUser();
- const wrapper = mountWithIntl(
-
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
-
- expect(wrapper.find('EuiFieldPassword[data-test-subj="currentPassword"]')).toHaveLength(1);
- expect(wrapper.find('EuiFieldPassword[data-test-subj="newPassword"]')).toHaveLength(1);
- });
-
- it(`does not display change password form for users in the saml realm`, async () => {
- const user = createUser({ realm: 'saml' });
- const wrapper = mountWithIntl(
-
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
-
- expect(wrapper.find('EuiFieldText[data-test-subj="currentPassword"]')).toHaveLength(0);
- expect(wrapper.find('EuiFieldText[data-test-subj="newPassword"]')).toHaveLength(0);
- });
-});
diff --git a/x-pack/plugins/security/public/account_management/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx
deleted file mode 100644
index 2c870bf788ceb7..00000000000000
--- a/x-pack/plugins/security/public/account_management/account_management_page.tsx
+++ /dev/null
@@ -1,69 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { useEffect, useState } from 'react';
-import ReactDOM from 'react-dom';
-import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
-import type { PublicMethodsOf } from '@kbn/utility-types';
-import { CoreStart, NotificationsStart } from 'src/core/public';
-import { getUserDisplayName, AuthenticatedUser } from '../../common/model';
-import { AuthenticationServiceSetup } from '../authentication';
-import { UserAPIClient } from '../management';
-import { ChangePassword } from './change_password';
-import { PersonalInfo } from './personal_info';
-
-interface Props {
- authc: AuthenticationServiceSetup;
- userAPIClient: PublicMethodsOf;
- notifications: NotificationsStart;
-}
-
-export const AccountManagementPage = ({ userAPIClient, authc, notifications }: Props) => {
- const [currentUser, setCurrentUser] = useState(null);
- useEffect(() => {
- authc.getCurrentUser().then(setCurrentUser);
- }, [authc]);
-
- if (!currentUser) {
- return null;
- }
-
- return (
-
-
-
-
- {getUserDisplayName(currentUser)}
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export function renderAccountManagementPage(
- i18nStart: CoreStart['i18n'],
- element: Element,
- props: Props
-) {
- ReactDOM.render(
-
-
- ,
- element
- );
-
- return () => ReactDOM.unmountComponentAtNode(element);
-}
diff --git a/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_empty_prompt.tsx b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_empty_prompt.tsx
new file mode 100644
index 00000000000000..395768811f8c3c
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_empty_prompt.tsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { EuiEmptyPrompt } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { DocLink } from '../components/doc_link';
+
+export interface ApiKeysEmptyPromptProps {
+ error?: Error;
+}
+
+export const ApiKeysEmptyPrompt: FunctionComponent = ({
+ error,
+ children,
+}) => {
+ if (error) {
+ if (doesErrorIndicateAPIKeysAreDisabled(error)) {
+ return (
+
+
+
+
+
+
+
+
+
+ >
+ }
+ />
+ );
+ }
+
+ if (doesErrorIndicateUserHasNoPermissionsToManageAPIKeys(error)) {
+ return (
+
+
+
+ }
+ />
+ );
+ }
+
+ return (
+
+
+
+ }
+ actions={children}
+ />
+ );
+ }
+
+ return (
+
+
+
+ }
+ body={
+
+
+
+ }
+ actions={children}
+ />
+ );
+};
+
+function doesErrorIndicateAPIKeysAreDisabled(error: Record) {
+ const message = error.body?.message || '';
+ return message.indexOf('disabled.feature="api_keys"') !== -1;
+}
+
+function doesErrorIndicateUserHasNoPermissionsToManageAPIKeys(error: Record) {
+ return error.body?.statusCode === 403;
+}
diff --git a/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_page.test.tsx b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_page.test.tsx
new file mode 100644
index 00000000000000..44728f4d674977
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_page.test.tsx
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { render, fireEvent, within, waitForElementToBeRemoved } from '@testing-library/react';
+import { createMemoryHistory } from 'history';
+import { coreMock } from '../../../../../../src/core/public/mocks';
+import { Providers } from '../account_management_app';
+import { ApiKeysPage } from './api_keys_page';
+
+const getApiKeysResponse = {
+ apiKeys: [
+ {
+ creation: 1571322182082,
+ expiration: 1571408582082,
+ id: '0QQZ2m0BO2XZwgJFuWTT',
+ invalidated: false,
+ name: 'my-api-key',
+ realm: 'reserved',
+ username: 'elastic',
+ },
+ ],
+};
+
+describe('ApiKeysPage', () => {
+ it('fetches API keys on mount', async () => {
+ const coreStart = coreMock.createStart();
+ const history = createMemoryHistory();
+ coreStart.http.get.mockResolvedValue(getApiKeysResponse);
+
+ const { queryByText } = render(
+
+
+
+ );
+
+ expect(coreStart.http.get).toHaveBeenLastCalledWith('/internal/security/api_key', {
+ query: { isAdmin: false },
+ });
+
+ await waitForElementToBeRemoved(queryByText(/Loading API keys/i));
+ });
+
+ it('creates API key on form submit', async () => {
+ const coreStart = coreMock.createStart();
+ const history = createMemoryHistory({ initialEntries: ['/api-keys/create'] });
+ coreStart.http.get.mockResolvedValue(getApiKeysResponse);
+
+ const { findByRole } = render(
+
+
+
+ );
+
+ const flyout = await findByRole('dialog');
+ fireEvent.change(within(flyout).getByLabelText(/Name/i), { target: { value: 'Test Key' } });
+ fireEvent.click(within(flyout).getByRole('button', { name: /Create API key/i }));
+
+ expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', {
+ body: '{"name":"Test Key"}',
+ });
+
+ await waitForElementToBeRemoved(() => within(flyout).queryByText(/Creating API key/i));
+ });
+
+ it('invalidates API key after confirmation', async () => {
+ const coreStart = coreMock.createStart();
+ const history = createMemoryHistory();
+ coreStart.http.get.mockResolvedValue(getApiKeysResponse);
+
+ const { findByRole, getByRole, queryByText } = render(
+
+
+
+ );
+
+ const table = await findByRole('table');
+ fireEvent.click(within(table).getByRole('button', { name: /Invalidate/i }));
+ fireEvent.click(getByRole('button', { name: /Invalidate this key/i }));
+
+ expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key/invalidate', {
+ body: '{"apiKeys":[{"id":"0QQZ2m0BO2XZwgJFuWTT","name":"my-api-key"}],"isAdmin":false}',
+ });
+
+ await waitForElementToBeRemoved(() => queryByText(/Invalidating API key/i));
+ });
+});
diff --git a/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_page.tsx b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_page.tsx
new file mode 100644
index 00000000000000..81ebf21ba97db8
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_page.tsx
@@ -0,0 +1,156 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent, useState } from 'react';
+import { Route, useHistory } from 'react-router-dom';
+import useAsyncFn from 'react-use/lib/useAsyncFn';
+import useMount from 'react-use/lib/useMount';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { reactRouterNavigate, useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { SectionLoading } from '../../../../../../src/plugins/es_ui_shared/public';
+import { ApiKey } from '../../../common/model';
+import {
+ APIKeysAPIClient,
+ CreateApiKeyResponse,
+} from '../../management/api_keys/api_keys_api_client';
+import { FieldTextWithCopyButton } from '../components/field_text_with_copy_button';
+import { Breadcrumb } from '../components/breadcrumb';
+import { CreateApiKeyFlyout } from './create_api_key_flyout';
+import { InvalidateApiKeyModal } from './invalidate_api_key_modal';
+import { ApiKeysTable } from './api_keys_table';
+import { ApiKeysEmptyPrompt } from './api_keys_empty_prompt';
+
+export const ApiKeysPage: FunctionComponent = () => {
+ const history = useHistory();
+ const { services } = useKibana();
+ const [state, getApiKeys] = useAsyncFn(() => new APIKeysAPIClient(services.http!).getApiKeys(), [
+ services.http,
+ ]);
+ const [createdApiKey, setCreatedApiKey] = useState();
+ const [apiKeyToInvalidate, setApiKeyToInvalidate] = useState();
+
+ useMount(getApiKeys);
+
+ if (!state.value) {
+ if (state.error && !state.loading) {
+ return (
+
+
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ history.push({ pathname: '/api-keys' })}
+ onSuccess={(apiKey) => {
+ history.push({ pathname: '/api-keys' });
+ setCreatedApiKey(apiKey);
+ getApiKeys();
+ }}
+ />
+
+
+
+ {apiKeyToInvalidate && (
+ setApiKeyToInvalidate(undefined)}
+ onSuccess={() => {
+ setApiKeyToInvalidate(undefined);
+ setCreatedApiKey(undefined);
+ getApiKeys();
+ }}
+ />
+ )}
+
+ {!state.loading && state.value.apiKeys.length === 0 ? (
+
+
+
+
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {createdApiKey && !state.loading && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ >
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_table.tsx b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_table.tsx
new file mode 100644
index 00000000000000..324f5d1e4d241a
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/api_keys_page/api_keys_table.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+// @ts-ignore
+import { formatDate } from '@elastic/eui/lib/services/format';
+import {
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiHideFor,
+ EuiInMemoryTable,
+ EuiInMemoryTableProps,
+ EuiShowFor,
+ EuiText,
+ EuiButtonIcon,
+} from '@elastic/eui';
+import { ApiKey } from '../../../common/model';
+
+export type ApiKeysTableProps = Omit, 'columns'> & {
+ createdItemId?: ApiKey['id'];
+ onInvalidateItem(item: ApiKey): void;
+};
+
+export const ApiKeysTable: FunctionComponent = ({
+ createdItemId,
+ onInvalidateItem,
+ ...props
+}) => {
+ const actions = [
+ {
+ render: (item: ApiKey) => (
+ <>
+
+ onInvalidateItem(item)}
+ />
+
+
+ onInvalidateItem(item)}
+ >
+
+
+
+ >
+ ),
+ },
+ ];
+
+ return (
+
+ !expiration ? (
+
+
+
+ ) : (
+ formatDate(expiration)
+ ),
+ width: '25%',
+ mobileOptions: { show: false },
+ },
+ {
+ field: 'creation',
+ name: i18n.translate('xpack.security.accountManagement.apiKeys.createdHeader', {
+ defaultMessage: 'Created',
+ }),
+ render: (creation: number, item: ApiKey) =>
+ item.id === createdItemId ? (
+
+
+
+ ) : (
+ formatDate(creation)
+ ),
+ width: '25%',
+ },
+ {
+ actions,
+ width: '25%',
+ },
+ ]}
+ sorting={{
+ sort: {
+ field: 'creation',
+ direction: 'desc',
+ },
+ }}
+ pagination={true}
+ />
+ );
+};
diff --git a/x-pack/plugins/security/public/account_management/api_keys_page/create_api_key_flyout.tsx b/x-pack/plugins/security/public/account_management/api_keys_page/create_api_key_flyout.tsx
new file mode 100644
index 00000000000000..d876ea75c4282a
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/api_keys_page/create_api_key_flyout.tsx
@@ -0,0 +1,314 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useRef, FunctionComponent } from 'react';
+import {
+ EuiCallOut,
+ EuiFieldNumber,
+ EuiFieldText,
+ EuiForm,
+ EuiPanel,
+ EuiFormRow,
+ EuiSpacer,
+ EuiSwitch,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { useKibana, CodeEditor } from '../../../../../../src/plugins/kibana_react/public';
+import {
+ APIKeysAPIClient,
+ CreateApiKeyRequest,
+ CreateApiKeyResponse,
+} from '../../management/api_keys/api_keys_api_client';
+import { useForm, ValidationErrors } from '../components/use_form';
+import { FormFlyout, FormFlyoutProps } from '../components/form_flyout';
+import { DocLink } from '../components/doc_link';
+
+export interface FormValues {
+ name: string;
+ expiration: string;
+ customExpiration: boolean;
+ customPrivileges: boolean;
+ role_descriptors: string;
+}
+
+export interface CreateApiKeyFlyoutProps {
+ defaultValues?: Partial;
+ onSuccess?: (apiKey: CreateApiKeyResponse) => void;
+ onError?: (error: Error) => void;
+ onClose: FormFlyoutProps['onClose'];
+}
+
+export const CreateApiKeyFlyout: FunctionComponent = ({
+ onSuccess,
+ onError,
+ onClose,
+ defaultValues,
+}) => {
+ const { services } = useKibana();
+ const [form, eventHandlers] = useForm(
+ {
+ onSubmit: (values) => new APIKeysAPIClient(services.http!).createApiKey(mapValues(values)),
+ onSubmitSuccess: onSuccess,
+ onSubmitError: onError,
+ validate,
+ defaultValues,
+ },
+ [services.http]
+ );
+ const nameInput = useRef(null);
+
+ return (
+
+ {form.submitError && (
+ <>
+
+ {(form.submitError as any).body?.message || form.submitError.message}
+
+
+ >
+ )}
+
+
+
+
+
+
+ form.setValue('customPrivileges', e.target.checked)}
+ />
+ {form.values.customPrivileges && (
+ <>
+
+
+
+
+ }
+ error={form.errors.role_descriptors}
+ isInvalid={!!form.errors.role_descriptors}
+ >
+
+ form.setValue('role_descriptors', value)}
+ width="100%"
+ height="300px"
+ languageId="xjson"
+ options={{
+ fixedOverflowWidgets: true,
+ folding: false,
+ lineNumbers: 'off',
+ scrollBeyondLastLine: false,
+ minimap: {
+ enabled: false,
+ },
+ scrollbar: {
+ useShadows: false,
+ },
+ wordBasedSuggestions: false,
+ wordWrap: 'on',
+ wrappingIndent: 'indent',
+ }}
+ />
+
+
+ >
+ )}
+
+
+ form.setValue('customExpiration', e.target.checked)}
+ />
+ {form.values.customExpiration && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+ );
+};
+
+CreateApiKeyFlyout.defaultProps = {
+ defaultValues: {
+ customExpiration: false,
+ customPrivileges: false,
+ role_descriptors: JSON.stringify(
+ {
+ 'role-a': {
+ cluster: ['all'],
+ indices: [
+ {
+ names: ['index-a*'],
+ privileges: ['read'],
+ },
+ ],
+ },
+ 'role-b': {
+ cluster: ['all'],
+ indices: [
+ {
+ names: ['index-b*'],
+ privileges: ['all'],
+ },
+ ],
+ },
+ },
+ null,
+ 2
+ ),
+ },
+};
+
+export function validate(v: Partial) {
+ const errors: ValidationErrors = {};
+
+ if (!v.name) {
+ errors.name = i18n.translate('xpack.security.management.apiKeys.createApiKey.nameRequired', {
+ defaultMessage: 'Enter a name.',
+ });
+ }
+
+ if (v.customExpiration && !v.expiration) {
+ errors.expiration = i18n.translate(
+ 'xpack.security.management.apiKeys.createApiKey.expirationRequired',
+ {
+ defaultMessage: 'Enter a duration or disable the option.',
+ }
+ );
+ }
+
+ if (v.customPrivileges) {
+ if (!v.role_descriptors) {
+ errors.role_descriptors = i18n.translate(
+ 'xpack.security.management.apiKeys.createApiKey.roleDescriptorsRequired',
+ {
+ defaultMessage: 'Enter role descriptors or disable the option.',
+ }
+ );
+ } else {
+ try {
+ JSON.parse(v.role_descriptors);
+ } catch (e) {
+ errors.role_descriptors = i18n.translate(
+ 'xpack.security.management.apiKeys.createApiKey.invalidJsonError',
+ {
+ defaultMessage: 'Enter valid JSON.',
+ }
+ );
+ }
+ }
+ }
+
+ return errors;
+}
+
+export function mapValues(values: FormValues): CreateApiKeyRequest {
+ return {
+ name: values.name,
+ expiration: values.customExpiration && values.expiration ? `${values.expiration}d` : undefined,
+ role_descriptors:
+ values.customPrivileges && values.role_descriptors
+ ? JSON.parse(values.role_descriptors)
+ : undefined,
+ };
+}
diff --git a/x-pack/plugins/security/public/account_management/api_keys_page/index.ts b/x-pack/plugins/security/public/account_management/api_keys_page/index.ts
new file mode 100644
index 00000000000000..d4821de33b201d
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/api_keys_page/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { ApiKeysEmptyPrompt } from './api_keys_empty_prompt';
+export { ApiKeysPage as default } from './api_keys_page'; // eslint-disable-line import/no-default-export
+export { ApiKeysTable } from './api_keys_table';
+export { CreateApiKeyFlyout } from './create_api_key_flyout';
+export { InvalidateApiKeyModal } from './invalidate_api_key_modal';
diff --git a/x-pack/plugins/security/public/account_management/api_keys_page/invalidate_api_key_modal.tsx b/x-pack/plugins/security/public/account_management/api_keys_page/invalidate_api_key_modal.tsx
new file mode 100644
index 00000000000000..2150826c53d724
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/api_keys_page/invalidate_api_key_modal.tsx
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiText } from '@elastic/eui';
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { ApiKeyToInvalidate } from '../../../common/model';
+import {
+ APIKeysAPIClient,
+ InvalidateApiKeysResponse,
+} from '../../management/api_keys/api_keys_api_client';
+import { useSubmitHandler } from '../components/use_form';
+import { ConfirmModal, ConfirmModalProps } from '../components/confirm_modal';
+
+export interface InvalidateApiKeyModalProps {
+ onCancel: ConfirmModalProps['onCancel'];
+ onSuccess?(result: InvalidateApiKeysResponse): any;
+ onError?(error: Error): any;
+ apiKey: ApiKeyToInvalidate;
+}
+
+export const InvalidateApiKeyModal: FunctionComponent = ({
+ onCancel,
+ onSuccess,
+ onError,
+ apiKey,
+}) => {
+ const { services, notifications } = useKibana();
+ const [state, invalidateApiKeys] = useSubmitHandler(
+ {
+ onSubmit: () =>
+ new APIKeysAPIClient(services.http!).invalidateApiKeys([
+ { id: apiKey.id, name: apiKey.name },
+ ]),
+ onSubmitSuccess: (values) => {
+ notifications.toasts.success({
+ iconType: 'check',
+ title: i18n.translate(
+ 'xpack.security.accountManagement.invalidateApiKey.successMessage',
+ {
+ defaultMessage: 'Invalidated API key “{name}”',
+ values: { name: apiKey.name },
+ }
+ ),
+ });
+ onSuccess?.(values);
+ },
+ onSubmitError: (error) => {
+ notifications.toasts.danger({
+ iconType: 'alert',
+ title: i18n.translate('xpack.security.accountManagement.invalidateApiKey.errorMessage', {
+ defaultMessage: 'Could not invalidate API key “{name}”',
+ values: { name: apiKey.name },
+ }),
+ body: (error as any).body?.message || error.message,
+ });
+ onError?.(error);
+ },
+ },
+ [services.http]
+ );
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/security/public/account_management/components/breadcrumb.tsx b/x-pack/plugins/security/public/account_management/components/breadcrumb.tsx
new file mode 100644
index 00000000000000..52cd721b658c35
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/components/breadcrumb.tsx
@@ -0,0 +1,109 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { createContext, useEffect, useRef, FunctionComponent, ReactNode } from 'react';
+import { EuiBreadcrumb } from '@elastic/eui';
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+
+/**
+ * Component that sets breadcrumbs and doc title based on the render tree.
+ *
+ * @example
+ * ```typescript
+ *
+ *
+ * {showForm && (
+ *
+ *
+ *
+ * )}
+ *
+ * ```
+ */
+export const Breadcrumb: FunctionComponent = ({ children, ...breadcrumb }) => (
+
+ {(value) =>
+ value ? (
+
+ {children}
+
+ ) : (
+ {children}
+ )
+ }
+
+);
+
+interface BreadcrumbContext {
+ parents: BreadcrumbProps[];
+ onMount(breadcrumbs: BreadcrumbProps[]): void;
+ onUnmount(breadcrumbs: BreadcrumbProps[]): void;
+}
+
+const { Provider, Consumer } = createContext(undefined);
+
+export interface RootProviderProps {
+ breadcrumb: BreadcrumbProps;
+ children: ReactNode;
+}
+
+export const RootProvider: FunctionComponent = ({ breadcrumb, children }) => {
+ const { services } = useKibana();
+ const breadcrumbsRef = useRef([]);
+
+ const setBreadcrumbs = (breadcrumbs: BreadcrumbProps[]) => {
+ breadcrumbsRef.current = breadcrumbs;
+ services.chrome?.setBreadcrumbs(breadcrumbs);
+ services.chrome?.docTitle.change(getDocTitle(breadcrumbs));
+ };
+
+ return (
+ {
+ if (breadcrumbs.length > breadcrumbsRef.current.length) {
+ setBreadcrumbs(breadcrumbs);
+ }
+ }}
+ onUnmount={(breadcrumbs) => {
+ if (breadcrumbs.length < breadcrumbsRef.current.length) {
+ setBreadcrumbs(breadcrumbs);
+ }
+ }}
+ >
+ {children}
+
+ );
+};
+
+export function getDocTitle(breadcrumbs: BreadcrumbProps[]) {
+ return breadcrumbs
+ .slice()
+ .reverse()
+ .map(({ text }) => text);
+}
+
+export const NestedProvider: FunctionComponent = ({
+ parents,
+ onMount,
+ onUnmount,
+ breadcrumb,
+ children,
+}) => {
+ const nextParents = [...parents, breadcrumb];
+
+ useEffect(() => {
+ onMount(nextParents);
+ return () => onUnmount(parents);
+ }, [breadcrumb.text, breadcrumb.href]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return {children};
+};
+
+export interface BreadcrumbProps extends EuiBreadcrumb {
+ text: string;
+}
diff --git a/x-pack/plugins/security/public/account_management/components/confirm_modal.tsx b/x-pack/plugins/security/public/account_management/components/confirm_modal.tsx
new file mode 100644
index 00000000000000..ece2dd000490ea
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/components/confirm_modal.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import {
+ EuiButton,
+ EuiButtonProps,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalProps,
+ EuiOverlayMask,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export interface ConfirmModalProps extends Omit {
+ confirmButtonColor?: EuiButtonProps['color'];
+ confirmButtonText: string;
+ isLoading?: EuiButtonProps['isLoading'];
+ onCancel(): void;
+ onConfirm(): void;
+ ownFocus?: boolean;
+}
+
+export const ConfirmModal: FunctionComponent = ({
+ children,
+ confirmButtonColor,
+ confirmButtonText,
+ isLoading,
+ onCancel,
+ onConfirm,
+ ownFocus,
+ title,
+ ...rest
+}) => {
+ const modal = (
+
+
+ {title}
+
+ {children}
+
+
+
+
+
+
+
+
+
+ {confirmButtonText}
+
+
+
+
+
+ );
+
+ return ownFocus ? {modal} : modal;
+};
+
+ConfirmModal.defaultProps = {
+ ownFocus: true,
+};
diff --git a/x-pack/plugins/security/public/account_management/components/doc_link.tsx b/x-pack/plugins/security/public/account_management/components/doc_link.tsx
new file mode 100644
index 00000000000000..6faa675bdb8299
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/components/doc_link.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, FunctionComponent } from 'react';
+import { EuiLink } from '@elastic/eui';
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { CoreStart } from '../../../../../../src/core/public';
+
+export type DocLinks = CoreStart['docLinks']['links'];
+export type GetDocLinkFunction = (app: string, doc: string) => string;
+
+export function useDocLinks(): [DocLinks, GetDocLinkFunction] {
+ const { services } = useKibana();
+ const { links, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = services.docLinks!;
+ const getDocLink = useCallback(
+ (app, doc) => {
+ return `${ELASTIC_WEBSITE_URL}guide/en/${app}/reference/${DOC_LINK_VERSION}/${doc}`;
+ },
+ [ELASTIC_WEBSITE_URL, DOC_LINK_VERSION]
+ );
+ return [links, getDocLink];
+}
+
+export interface DocLinkProps {
+ app: string;
+ doc: string;
+}
+
+export const DocLink: FunctionComponent = ({ app, doc, children }) => {
+ const [, getDocLink] = useDocLinks();
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/security/public/account_management/components/field_text_with_copy_button.tsx b/x-pack/plugins/security/public/account_management/components/field_text_with_copy_button.tsx
new file mode 100644
index 00000000000000..5c32b74f5af6bf
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/components/field_text_with_copy_button.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { EuiButtonIcon, EuiCopy, EuiFieldText, EuiFieldTextProps } from '@elastic/eui';
+
+export interface FieldTextWithCopyButtonProps extends Omit {
+ value: string;
+}
+
+export const FieldTextWithCopyButton: FunctionComponent = (props) => (
+
+ {(copyText) => (
+
+ )}
+
+ }
+ />
+);
+
+FieldTextWithCopyButton.defaultProps = {
+ readOnly: true,
+};
diff --git a/x-pack/plugins/security/public/account_management/components/form_flyout.tsx b/x-pack/plugins/security/public/account_management/components/form_flyout.tsx
new file mode 100644
index 00000000000000..92d282bc972ccf
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/components/form_flyout.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useEffect, FunctionComponent, MouseEventHandler, RefObject } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiTitle,
+ EuiFlyout,
+ EuiFlyoutProps,
+ EuiFlyoutHeader,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButton,
+ EuiButtonProps,
+ EuiButtonEmpty,
+ EuiPortal,
+} from '@elastic/eui';
+
+export interface FormFlyoutProps extends Omit {
+ title: string;
+ isLoading?: EuiButtonProps['isLoading'];
+ initialFocus?: RefObject;
+ onSubmit: MouseEventHandler;
+ submitButtonText: string;
+ submitButtonColor?: EuiButtonProps['color'];
+}
+
+export const FormFlyout: FunctionComponent = ({
+ title,
+ submitButtonText,
+ submitButtonColor,
+ onSubmit,
+ isLoading,
+ children,
+ initialFocus,
+ ...rest
+}) => {
+ useEffect(() => {
+ if (initialFocus && initialFocus.current) {
+ initialFocus.current.focus();
+ }
+ }, [initialFocus]);
+
+ const flyout = (
+
+
+
+ {title}
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+ {submitButtonText}
+
+
+
+
+
+ );
+
+ return rest.ownFocus ? {flyout} : flyout;
+};
+
+FormFlyout.defaultProps = {
+ ownFocus: true,
+};
diff --git a/x-pack/plugins/security/public/account_management/components/tabbed_routes.tsx b/x-pack/plugins/security/public/account_management/components/tabbed_routes.tsx
new file mode 100644
index 00000000000000..0acbbd08ba68eb
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/components/tabbed_routes.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { EuiTabs, EuiTab, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui';
+import { Switch, Route, RouteProps, RedirectProps } from 'react-router-dom';
+import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public';
+
+export type TabbedRoutesTab = EuiTabbedContentTab &
+ Pick &
+ Pick;
+
+export interface TabbedRoutesProps {
+ routes: TabbedRoutesTab[];
+}
+export const TabbedRoutes: FunctionComponent = ({ routes }) => {
+ return (
+ <>
+
+ {routes.map((route) => (
+
+ {({ match, history }) => (
+
+ {route.name}
+
+ )}
+
+ ))}
+
+
+
+ {routes.map((route) => (
+
+ {route.content}
+
+ ))}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/security/public/account_management/components/use_form.ts b/x-pack/plugins/security/public/account_management/components/use_form.ts
new file mode 100644
index 00000000000000..1b9cadfba46809
--- /dev/null
+++ b/x-pack/plugins/security/public/account_management/components/use_form.ts
@@ -0,0 +1,235 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ useState,
+ useEffect,
+ DependencyList,
+ FocusEventHandler,
+ ChangeEventHandler,
+ ReactEventHandler,
+} from 'react';
+import useAsyncFn from 'react-use/lib/useAsyncFn';
+
+export interface FormOptions extends SubmitHandlerOptions {
+ validate: ValidateFunction;
+ defaultValues?: Partial;
+}
+
+export interface FormProps {
+ onSubmit: ReactEventHandler;
+ onChange: ChangeEventHandler;
+ onBlur: FocusEventHandler;
+ noValidate: boolean;
+}
+
+export type FormReturnTuple = [
+ ValidationState & SubmitState,
+ FormProps
+];
+
+/**
+ * Returns state and {@link HTMLFormElement} event handlers useful for creating
+ * forms with inline validation.
+ *
+ * @see {@link useInlineValidation} if you don't want to use {@link HTMLFormElement}.
+ * @see {@link useSubmitHandler} if you don't require inline validation.
+ *
+ * @example
+ * ```typescript
+ * const [form, eventHandlers] = useForm({
+ * onSubmit: (values) => apiClient.create(values),
+ * validate: (values) => !values.email ? { email: 'Required' } : {}
+ * });
+ *
+ *
+ *
+ * Submit
+ *
+ * ```
+ */
+export function useForm(
+ options: FormOptions,
+ deps?: DependencyList
+): FormReturnTuple {
+ const [submitState, submit] = useSubmitHandler(options, deps);
+ const validationState = useInlineValidation(options.validate, options.defaultValues);
+
+ const eventHandlers: FormProps = {
+ onSubmit: (event) => {
+ event.preventDefault();
+ validationState.handleSubmit(submit);
+ },
+ onChange: (event) => {
+ const { name, type, checked, value } = event.target;
+ validationState.setValue(name, type === 'checkbox' ? checked : value);
+ },
+ onBlur: (event) => {
+ validationState.trigger(event.target.name);
+ },
+ noValidate: true, // Native browser validation gets in the way of EUI
+ };
+
+ return [{ ...validationState, ...submitState }, eventHandlers];
+}
+
+export type ValidateFunction = (values: Partial) => ValidationErrors;
+export type ValidationErrors = Partial>;
+export type DirtyFields = Partial>;
+export type SubmitCallback = (values: Values) => Promise;
+
+export interface ValidationState {
+ setValue(name: string, value: any): void;
+ setError(name: string, message: string): void;
+ trigger(name: string): void;
+ handleSubmit(onSubmit: SubmitCallback): Promise;
+ values: Partial;
+ errors: ValidationErrors;
+ isInvalid: boolean;
+ isSubmitted: boolean;
+}
+
+/**
+ * Returns state useful for creating forms with inline validation.
+ *
+ * @example
+ * ```typescript
+ * const form = useInlineValidation((values) => !values.toggle ? { toggle: 'Required' } : {});
+ *
+ * form.setValue('toggle', e.target.checked)}
+ * onBlur={() => form.trigger('toggle')}
+ * isInvalid={!!form.errors.toggle}
+ * />
+ * apiClient.create(values))}>
+ * Submit
+ *
+ * ```
+ */
+export function useInlineValidation(
+ validate: ValidateFunction,
+ defaultValues: Partial = {}
+): ValidationState {
+ const [values, setValues] = useState>(defaultValues);
+ const [errors, setErrors] = useState>({});
+ const [dirty, setDirty] = useState>({});
+ const [submitCount, setSubmitCount] = useState(0);
+
+ const isSubmitted = submitCount > 0;
+ const isInvalid = Object.keys(errors).length > 0;
+
+ const inlineValidation = (nextValues: Partial, nextDirty: DirtyFields) => {
+ const nextErrors = getIntersection(validate(nextValues), nextDirty);
+ setErrors(nextErrors);
+ if (Object.keys(nextErrors).length === 0) {
+ setSubmitCount(0);
+ }
+ };
+
+ return {
+ setValue: (name, value) => {
+ const nextValues = { ...values, [name]: value };
+ setValues(nextValues);
+ inlineValidation(nextValues, dirty);
+ },
+ setError: (name, message) => {
+ setErrors({ ...errors, [name]: message });
+ setDirty({ ...dirty, [name]: true });
+ },
+ trigger: (name) => {
+ const nextDirty = { ...dirty, [name]: true };
+ setDirty(nextDirty);
+ inlineValidation(values, nextDirty);
+ },
+ handleSubmit: async (onSubmit) => {
+ const nextErrors = validate(values);
+ setDirty({ ...dirty, ...nextErrors });
+ setErrors(nextErrors);
+ setSubmitCount(submitCount + 1);
+ if (Object.keys(nextErrors).length === 0) {
+ return await onSubmit(values as Values);
+ }
+ },
+ values,
+ errors,
+ isInvalid,
+ isSubmitted,
+ };
+}
+
+export function getIntersection(
+ errors: ValidationErrors,
+ dirty: DirtyFields
+) {
+ const names = Object.keys(errors) as Array;
+ return names.reduce>((acc, name) => {
+ if (dirty[name]) {
+ acc[name] = errors[name];
+ }
+ return acc;
+ }, {});
+}
+
+export type AsyncFunction = (...args: any[]) => Promise;
+
+export interface SubmitHandlerOptions {
+ onSubmit: AsyncFunction;
+ onSubmitSuccess?: (result: Result) => void;
+ onSubmitError?: (error: Error) => void;
+}
+
+export interface SubmitState {
+ isSubmitting: boolean;
+ submitError?: Error;
+ submitResult?: Result;
+}
+
+export type SubmitHandlerReturnTuple = [SubmitState, AsyncFunction];
+
+/**
+ * Tracks state of an async function and triggers callbacks with the outcome.
+ *
+ * @example
+ * ```typescript
+ * const [state, deleteUser] = useSubmitHandler({
+ * onSubmit: () => apiClient.deleteUser(),
+ * onSubmitSuccess: (result) => toasts.addSuccess('Deleted user'),
+ * onSubmitError: (error) => toasts.addError('Could not delete user'),
+ * });
+ *
+ *
+ * Delete user
+ *
+ * ```
+ */
+export function useSubmitHandler(
+ { onSubmit, onSubmitSuccess, onSubmitError }: SubmitHandlerOptions,
+ deps?: DependencyList
+): SubmitHandlerReturnTuple {
+ const [state, callback] = useAsyncFn(onSubmit, deps);
+
+ useEffect(() => {
+ if (state.value && onSubmitSuccess) {
+ onSubmitSuccess(state.value);
+ }
+ }, [state.value]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ useEffect(() => {
+ if (state.error && onSubmitError) {
+ onSubmitError(state.error);
+ }
+ }, [state.error]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return [
+ {
+ isSubmitting: state.loading,
+ submitError: state.loading ? undefined : state.error,
+ submitResult: state.value,
+ },
+ callback,
+ ];
+}
diff --git a/x-pack/plugins/security/public/account_management/personal_info/index.ts b/x-pack/plugins/security/public/account_management/personal_info/index.ts
deleted file mode 100644
index 5980157f5b76e2..00000000000000
--- a/x-pack/plugins/security/public/account_management/personal_info/index.ts
+++ /dev/null
@@ -1,7 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export { PersonalInfo } from './personal_info';
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts
index 2a45d497029f41..fac8f2e1b5b24a 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts
@@ -4,10 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import type { PublicMethodsOf } from '@kbn/utility-types';
+import { APIKeysAPIClient } from './api_keys_api_client';
+
export const apiKeysAPIClientMock = {
- create: () => ({
+ create: (): jest.Mocked> => ({
checkPrivileges: jest.fn(),
getApiKeys: jest.fn(),
invalidateApiKeys: jest.fn(),
+ createApiKey: jest.fn(),
}),
};
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts
index a127379d972413..7d78a5ae5e9aa8 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts
@@ -5,23 +5,38 @@
*/
import { HttpStart } from 'src/core/public';
-import { ApiKey, ApiKeyToInvalidate } from '../../../common/model';
+import { ApiKey, ApiKeyToInvalidate, Role } from '../../../common/model';
-interface CheckPrivilegesResponse {
+export interface CheckPrivilegesResponse {
areApiKeysEnabled: boolean;
isAdmin: boolean;
canManage: boolean;
}
-interface InvalidateApiKeysResponse {
+export interface InvalidateApiKeysResponse {
itemsInvalidated: ApiKeyToInvalidate[];
errors: any[];
}
-interface GetApiKeysResponse {
+export interface GetApiKeysResponse {
apiKeys: ApiKey[];
}
+export interface CreateApiKeyRequest {
+ name: string;
+ expiration?: string;
+ role_descriptors?: {
+ [key in string]: Role['elasticsearch'];
+ };
+}
+
+export interface CreateApiKeyResponse {
+ id: string;
+ name: string;
+ expiration: number;
+ api_key: string;
+}
+
const apiKeysUrl = '/internal/security/api_key';
export class APIKeysAPIClient {
@@ -40,4 +55,10 @@ export class APIKeysAPIClient {
body: JSON.stringify({ apiKeys, isAdmin }),
});
}
+
+ public async createApiKey(apiKey: CreateApiKeyRequest) {
+ return await this.http.post(apiKeysUrl, {
+ body: JSON.stringify(apiKey),
+ });
+ }
}
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx
index 9b2ccfcb99ef30..330637087d9d8c 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx
@@ -7,7 +7,7 @@
import React, { Fragment } from 'react';
import { ApplicationStart } from 'kibana/public';
-import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DocumentationLinksService } from '../../documentation_links';
@@ -44,17 +44,7 @@ export const EmptyPrompt: React.FunctionComponent = ({
-
-
- ),
- }}
+ defaultMessage="You can create an API key from your account."
/>
@@ -62,12 +52,12 @@ export const EmptyPrompt: React.FunctionComponent = ({
actions={
navigateToApp('dev_tools')}
- data-test-subj="goToConsoleButton"
+ onClick={() => navigateToApp('security_account', { path: 'api-keys' })}
+ data-test-subj="goToAccountButton"
>
}
diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx
index c22308fa8a43e0..ee90eb1379d0ab 100644
--- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx
+++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx
@@ -127,6 +127,7 @@ export class SecurityNavControl extends Component {
),
icon: ,
href: editProfileUrl,
+ onClick: this.closeMenu,
'data-test-subj': 'profileLink',
};
@@ -139,6 +140,7 @@ export class SecurityNavControl extends Component {
),
icon: ,
href: logoutUrl,
+ onClick: this.closeMenu,
'data-test-subj': 'logoutLink',
};
@@ -153,6 +155,7 @@ export class SecurityNavControl extends Component {
name: {label},
icon: ,
href,
+ onClick: this.closeMenu,
'data-test-subj': `userMenuLink__${label}`,
}));
diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts
index 5b9788d67500b9..4c3c733b771b71 100644
--- a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts
+++ b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts
@@ -56,34 +56,38 @@ describe('SecurityNavControlService', () => {
expect(target).toMatchInlineSnapshot(`
diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx
index 5d2e7d7dfb7336..93c5f3dc625d04 100644
--- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx
+++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx
@@ -11,7 +11,7 @@ import { CoreStart } from 'src/core/public';
import ReactDOM from 'react-dom';
import React from 'react';
-
+import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
import { SecurityLicense } from '../../common/licensing';
import { SecurityNavControl, UserMenuLink } from './nav_control_component';
import { AuthenticationServiceSetup } from '../authentication';
@@ -106,7 +106,9 @@ export class SecurityNavControlService {
};
ReactDOM.render(
-
+
+
+
,
el
);
diff --git a/x-pack/plugins/security/server/routes/api_keys/create.test.ts b/x-pack/plugins/security/server/routes/api_keys/create.test.ts
new file mode 100644
index 00000000000000..6f4c2a9b85c48b
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api_keys/create.test.ts
@@ -0,0 +1,101 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { createLicensedRouteHandler } from '../licensed_route_handler';
+import { routeDefinitionParamsMock } from '../index.mock';
+import { defineCreateApiKeyRoutes } from './create';
+
+jest.mock('../licensed_route_handler');
+
+const createLicensedRouteHandlerMock = createLicensedRouteHandler as jest.Mock;
+createLicensedRouteHandlerMock.mockImplementation((handler) => handler);
+
+const contextMock = ({} as unknown) as RequestHandlerContext;
+
+describe('defineCreateApiKeyRoutes', () => {
+ beforeEach(() => {
+ createLicensedRouteHandlerMock.mockClear();
+ });
+
+ test('creates licensed route handler', () => {
+ const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+ defineCreateApiKeyRoutes(mockRouteDefinitionParams);
+ expect(createLicensedRouteHandlerMock).toHaveBeenCalledTimes(1);
+ });
+
+ test('validates request body', () => {
+ const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+ defineCreateApiKeyRoutes(mockRouteDefinitionParams);
+ const [[route]] = mockRouteDefinitionParams.router.post.mock.calls;
+ expect(() => (route.validate as any).body.validate({})).toThrow(
+ '[name]: expected value of type [string] but got [undefined]'
+ );
+ });
+
+ test('creates API key', async () => {
+ const createAPIKeyResult = {
+ id: 'ID',
+ name: 'NAME',
+ api_key: 'API_KEY',
+ };
+ const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+ mockRouteDefinitionParams.authc.createAPIKey.mockResolvedValue(createAPIKeyResult);
+ defineCreateApiKeyRoutes(mockRouteDefinitionParams);
+ const [[, handler]] = mockRouteDefinitionParams.router.post.mock.calls;
+
+ const mockRequest = httpServerMock.createKibanaRequest({
+ method: 'post',
+ path: '/internal/security/api_key',
+ body: {},
+ });
+ const response = await handler(contextMock, mockRequest, kibanaResponseFactory);
+ expect(mockRouteDefinitionParams.authc.createAPIKey).toHaveBeenCalledWith(
+ mockRequest,
+ mockRequest.body
+ );
+ expect(response.status).toBe(200);
+ expect(response.payload).toEqual(createAPIKeyResult);
+ });
+
+ test('returns bad request if api keys are not available', async () => {
+ const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+ mockRouteDefinitionParams.authc.createAPIKey.mockResolvedValue(null);
+ defineCreateApiKeyRoutes(mockRouteDefinitionParams);
+ const [[, handler]] = mockRouteDefinitionParams.router.post.mock.calls;
+
+ const response = await handler(
+ contextMock,
+ httpServerMock.createKibanaRequest({
+ method: 'post',
+ path: '/internal/security/api_key',
+ body: {},
+ }),
+ kibanaResponseFactory
+ );
+ expect(response.status).toBe(400);
+ expect(response.payload).toEqual({ message: 'API Keys are not available' });
+ });
+
+ test('returns bad request if api key could not be created', async () => {
+ const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+ mockRouteDefinitionParams.authc.createAPIKey.mockRejectedValue(new Error('Error'));
+ defineCreateApiKeyRoutes(mockRouteDefinitionParams);
+ const [[, handler]] = mockRouteDefinitionParams.router.post.mock.calls;
+
+ const response = await handler(
+ contextMock,
+ httpServerMock.createKibanaRequest({
+ method: 'post',
+ path: '/internal/security/api_key',
+ body: {},
+ }),
+ kibanaResponseFactory
+ );
+ expect(response.status).toBe(500);
+ });
+});
diff --git a/x-pack/plugins/security/server/routes/api_keys/create.ts b/x-pack/plugins/security/server/routes/api_keys/create.ts
new file mode 100644
index 00000000000000..2bb6c077d16c29
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api_keys/create.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { createLicensedRouteHandler } from '../licensed_route_handler';
+import { wrapIntoCustomErrorResponse } from '../../errors';
+import { RouteDefinitionParams } from '..';
+
+export function defineCreateApiKeyRoutes({ router, authc }: RouteDefinitionParams) {
+ router.post(
+ {
+ path: '/internal/security/api_key',
+ validate: {
+ body: schema.object({
+ name: schema.string(),
+ expiration: schema.maybe(schema.string()),
+ role_descriptors: schema.recordOf(
+ schema.string(),
+ schema.object({}, { unknowns: 'allow' }),
+ {
+ defaultValue: {},
+ }
+ ),
+ }),
+ },
+ },
+ createLicensedRouteHandler(async (context, request, response) => {
+ try {
+ const apiKey = await authc.createAPIKey(request, request.body);
+
+ if (!apiKey) {
+ return response.badRequest({ body: { message: `API Keys are not available` } });
+ }
+
+ return response.ok({ body: apiKey });
+ } catch (error) {
+ return response.customError(wrapIntoCustomErrorResponse(error));
+ }
+ })
+ );
+}
diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts
index 7ac37bbead6136..aa148732e78708 100644
--- a/x-pack/plugins/security/server/routes/api_keys/index.ts
+++ b/x-pack/plugins/security/server/routes/api_keys/index.ts
@@ -8,6 +8,7 @@ import { defineGetApiKeysRoutes } from './get';
import { defineCheckPrivilegesRoutes } from './privileges';
import { defineInvalidateApiKeysRoutes } from './invalidate';
import { defineEnabledApiKeysRoutes } from './enabled';
+import { defineCreateApiKeyRoutes } from './create';
import { RouteDefinitionParams } from '..';
export function defineApiKeysRoutes(params: RouteDefinitionParams) {
@@ -15,4 +16,5 @@ export function defineApiKeysRoutes(params: RouteDefinitionParams) {
defineGetApiKeysRoutes(params);
defineCheckPrivilegesRoutes(params);
defineInvalidateApiKeysRoutes(params);
+ defineCreateApiKeyRoutes(params);
}
diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts
index e0af14f90d01c5..5660dfe5057582 100644
--- a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts
+++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts
@@ -15,7 +15,7 @@ import { ElasticsearchRole } from './elasticsearch_role';
* Elasticsearch specific portion of the role definition.
* See more details at https://www.elastic.co/guide/en/elasticsearch/reference/master/security-api.html#security-role-apis.
*/
-const elasticsearchRoleSchema = schema.object({
+export const elasticsearchRoleSchema = schema.object({
/**
* An optional list of cluster privileges. These privileges define the cluster level actions that
* users with this role are able to execute
diff --git a/x-pack/plugins/security/server/routes/views/account_management.ts b/x-pack/plugins/security/server/routes/views/account_management.ts
index 696a5e12b64c13..2ccca46ea3418f 100644
--- a/x-pack/plugins/security/server/routes/views/account_management.ts
+++ b/x-pack/plugins/security/server/routes/views/account_management.ts
@@ -13,4 +13,12 @@ export function defineAccountManagementRoutes({ httpResources }: RouteDefinition
httpResources.register({ path: '/security/account', validate: false }, (context, req, res) =>
res.renderCoreApp()
);
+ httpResources.register(
+ { path: '/security/account/api-keys', validate: false },
+ (context, req, res) => res.renderCoreApp()
+ );
+ httpResources.register(
+ { path: '/security/account/api-keys/create', validate: false },
+ (context, req, res) => res.renderCoreApp()
+ );
}
diff --git a/x-pack/plugins/security/server/routes/views/index.test.ts b/x-pack/plugins/security/server/routes/views/index.test.ts
index fa2088a80b1833..1be0a8e11a0fe8 100644
--- a/x-pack/plugins/security/server/routes/views/index.test.ts
+++ b/x-pack/plugins/security/server/routes/views/index.test.ts
@@ -22,6 +22,8 @@ describe('View routes', () => {
Array [
"/security/access_agreement",
"/security/account",
+ "/security/account/api-keys",
+ "/security/account/api-keys/create",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
@@ -49,6 +51,8 @@ describe('View routes', () => {
"/login",
"/security/access_agreement",
"/security/account",
+ "/security/account/api-keys",
+ "/security/account/api-keys/create",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
@@ -77,6 +81,8 @@ describe('View routes', () => {
"/login",
"/security/access_agreement",
"/security/account",
+ "/security/account/api-keys",
+ "/security/account/api-keys/create",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
@@ -105,6 +111,8 @@ describe('View routes', () => {
"/login",
"/security/access_agreement",
"/security/account",
+ "/security/account/api-keys",
+ "/security/account/api-keys/create",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 8c4a66975d6efd..fb6183ae665812 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -15899,9 +15899,6 @@
"xpack.security.management.apiKeys.table.apiKeysTitle": "API キー",
"xpack.security.management.apiKeys.table.creationDateColumnName": "作成済み",
"xpack.security.management.apiKeys.table.emptyPromptAdminTitle": "API キーがありません",
- "xpack.security.management.apiKeys.table.emptyPromptConsoleButtonMessage": "コンソールに移動してください",
- "xpack.security.management.apiKeys.table.emptyPromptDescription": "コンソールで {link} を作成できます。",
- "xpack.security.management.apiKeys.table.emptyPromptDocsLinkMessage": "API キー",
"xpack.security.management.apiKeys.table.emptyPromptNonAdminTitle": "まだ API キーがありません",
"xpack.security.management.apiKeys.table.expirationDateColumnName": "有効期限",
"xpack.security.management.apiKeys.table.expirationDateNeverMessage": "無し",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 60b1a33c8f5c97..78b0a9d20cf3b2 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -15917,9 +15917,6 @@
"xpack.security.management.apiKeys.table.apiKeysTitle": "API 密钥",
"xpack.security.management.apiKeys.table.creationDateColumnName": "创建时间",
"xpack.security.management.apiKeys.table.emptyPromptAdminTitle": "无 API 密钥",
- "xpack.security.management.apiKeys.table.emptyPromptConsoleButtonMessage": "前往 Console",
- "xpack.security.management.apiKeys.table.emptyPromptDescription": "您可以从 Console 创建 {link}。",
- "xpack.security.management.apiKeys.table.emptyPromptDocsLinkMessage": "API 密钥",
"xpack.security.management.apiKeys.table.emptyPromptNonAdminTitle": "您未有任何 API 密钥",
"xpack.security.management.apiKeys.table.expirationDateColumnName": "过期",
"xpack.security.management.apiKeys.table.expirationDateNeverMessage": "永远不",
diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts
index 39d8449218ffad..52355aba3a5c7b 100644
--- a/x-pack/test/functional/apps/api_keys/home_page.ts
+++ b/x-pack/test/functional/apps/api_keys/home_page.ts
@@ -34,8 +34,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const headers = await testSubjects.findAll('noApiKeysHeader');
if (headers.length > 0) {
expect(await headers[0].getVisibleText()).to.be('No API keys');
- const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton();
- expect(await goToConsoleButton.isDisplayed()).to.be(true);
+ const goToAccountButton = await pageObjects.apiKeys.getGoToAccountButton();
+ expect(await goToAccountButton.isDisplayed()).to.be(true);
} else {
// page may already contain EiTable with data, then check API Key Admin text
const description = await pageObjects.apiKeys.getApiKeyAdminDesc();
diff --git a/x-pack/test/functional/page_objects/api_keys_page.ts b/x-pack/test/functional/page_objects/api_keys_page.ts
index fa10c5a574c095..d1aeda653f8a0b 100644
--- a/x-pack/test/functional/page_objects/api_keys_page.ts
+++ b/x-pack/test/functional/page_objects/api_keys_page.ts
@@ -18,8 +18,8 @@ export function ApiKeysPageProvider({ getService }: FtrProviderContext) {
return await testSubjects.getVisibleText('apiKeyAdminDescriptionCallOut');
},
- async getGoToConsoleButton() {
- return await testSubjects.find('goToConsoleButton');
+ async getGoToAccountButton() {
+ return await testSubjects.find('goToAccountButton');
},
async apiKeysPermissionDeniedMessage() {
diff --git a/x-pack/test/security_functional/account_management.config.ts b/x-pack/test/security_functional/account_management.config.ts
new file mode 100644
index 00000000000000..9c336fc0b833b2
--- /dev/null
+++ b/x-pack/test/security_functional/account_management.config.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { resolve } from 'path';
+import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
+import { services } from '../functional/services';
+import { pageObjects } from '../functional/page_objects';
+
+// the default export of config files must be a config provider
+// that returns an object with the projects config values
+export default async function ({ readConfigFile }: FtrConfigProviderContext) {
+ const kibanaCommonConfig = await readConfigFile(
+ require.resolve('../../../test/common/config.js')
+ );
+ const kibanaFunctionalConfig = await readConfigFile(
+ require.resolve('../../../test/functional/config.js')
+ );
+
+ return {
+ testFiles: [resolve(__dirname, './tests/account_management')],
+ services,
+ pageObjects,
+ servers: kibanaFunctionalConfig.get('servers'),
+ esTestCluster: {
+ ...kibanaCommonConfig.get('esTestCluster'),
+ license: 'trial',
+ serverArgs: [
+ ...kibanaCommonConfig.get('esTestCluster.serverArgs'),
+ 'xpack.security.authc.api_key.enabled=true',
+ ],
+ },
+ kbnTestServer: kibanaCommonConfig.get('kbnTestServer'),
+ apps: {
+ ...kibanaFunctionalConfig.get('apps'),
+ accountManagement: {
+ pathname: '/security/account',
+ },
+ accountManagementApiKeys: {
+ pathname: '/security/account/api-keys',
+ },
+ },
+ esArchiver: { directory: resolve(__dirname, 'es_archives') },
+ screenshots: { directory: resolve(__dirname, 'screenshots') },
+ junit: {
+ reportName: 'Chrome X-Pack Security Functional Tests (Account Management)',
+ },
+ };
+}
diff --git a/x-pack/test/security_functional/tests/account_management/api_keys_page.ts b/x-pack/test/security_functional/tests/account_management/api_keys_page.ts
new file mode 100644
index 00000000000000..90f1e3fc222853
--- /dev/null
+++ b/x-pack/test/security_functional/tests/account_management/api_keys_page.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const pageObjects = getPageObjects(['common']);
+ const log = getService('log');
+ const find = getService('find');
+ const security = getService('security');
+
+ describe('API keys page', function () {
+ before(async () => {
+ await security.role.create('allow_manage_own_api_key_role', {
+ elasticsearch: {
+ cluster: ['manage_own_api_key'],
+ },
+ });
+ await security.testUser.setRoles(['allow_manage_own_api_key_role']);
+ await pageObjects.common.navigateToApp('accountManagementApiKeys');
+ });
+
+ after(async () => {
+ await security.testUser.restoreDefaults();
+ });
+
+ it('creates API key', async () => {
+ log.debug('Checking for section header');
+
+ await find.clickByLinkText('Create API key');
+
+ const nameInput = await find.byName('name');
+ await nameInput.type('test');
+
+ await find.clickByButtonText('Create API key');
+
+ await find.byCssSelector('.euiCallOut--success');
+ });
+ });
+}
diff --git a/x-pack/test/security_functional/tests/account_management/index.ts b/x-pack/test/security_functional/tests/account_management/index.ts
new file mode 100644
index 00000000000000..a39109faf6b89c
--- /dev/null
+++ b/x-pack/test/security_functional/tests/account_management/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('security app - Account Management', function () {
+ this.tags('ciGroup4');
+
+ loadTestFile(require.resolve('./api_keys_page'));
+ });
+}
diff --git a/yarn.lock b/yarn.lock
index b59c134968c18f..bf61486122f503 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1192,13 +1192,20 @@
dependencies:
regenerator-runtime "^0.12.0"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.1.2":
+ version "7.12.1"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740"
+ integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.4.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
@@ -5008,10 +5015,10 @@
dependencies:
"@types/sizzle" "*"
-"@types/js-cookie@2.2.5":
- version "2.2.5"
- resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e"
- integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg==
+"@types/js-cookie@2.2.6":
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
+ integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
"@types/js-search@^1.4.0":
version "1.4.0"
@@ -6264,10 +6271,10 @@
dependencies:
tslib "^1.9.3"
-"@xobotyi/scrollbar-width@1.9.4":
- version "1.9.4"
- resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.4.tgz#a7dce20b7465bcad29cd6bbb557695e4ea7863cb"
- integrity sha512-o12FCQt/X5n3pgKEWGpt0f/7Eg4mfv3uRwPUrctiOT8ZuxbH3cNLGWfH/8y6KxVJg4L2885ucuXQ6XECZzUiJA==
+"@xobotyi/scrollbar-width@1.9.5":
+ version "1.9.5"
+ resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
+ integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
@@ -10142,13 +10149,20 @@ copy-props@^2.0.1:
each-props "^1.3.0"
is-plain-object "^2.0.1"
-copy-to-clipboard@^3.0.8, copy-to-clipboard@^3.2.0:
+copy-to-clipboard@^3.0.8:
version "3.2.0"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467"
integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==
dependencies:
toggle-selection "^1.0.6"
+copy-to-clipboard@^3.2.0:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
+ integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
+ dependencies:
+ toggle-selection "^1.0.6"
+
copy-webpack-plugin@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.0.2.tgz#10efc6ad219a61acbf2f5fb50af83da38431bc34"
@@ -10529,7 +10543,7 @@ css-to-react-native@^3.0.0:
css-color-keywords "^1.0.0"
postcss-value-parser "^4.0.2"
-css-tree@1.0.0-alpha.37, css-tree@^1.0.0-alpha.28:
+css-tree@1.0.0-alpha.37:
version "1.0.0-alpha.37"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
@@ -10537,6 +10551,14 @@ css-tree@1.0.0-alpha.37, css-tree@^1.0.0-alpha.28:
mdn-data "2.0.4"
source-map "^0.6.1"
+css-tree@^1.0.0-alpha.28:
+ version "1.0.0-alpha.39"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
+ integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
+ dependencies:
+ mdn-data "2.0.6"
+ source-map "^0.6.1"
+
css-what@2.1, css-what@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
@@ -10612,11 +10634,16 @@ cssstyle@^2.2.0:
dependencies:
cssom "~0.3.6"
-csstype@^2.2.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7:
+csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5"
integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==
+csstype@^2.5.5:
+ version "2.6.13"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
+ integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
+
cucumber-expressions@^5.0.13:
version "5.0.18"
resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-5.0.18.tgz#6c70779efd3aebc5e9e7853938b1110322429596"
@@ -12382,7 +12409,7 @@ error-stack-parser@^1.3.5:
dependencies:
stackframe "^0.3.1"
-error-stack-parser@^2.0.4, error-stack-parser@^2.0.6:
+error-stack-parser@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.6.tgz#5a99a707bd7a4c58a797902d48d82803ede6aad8"
integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==
@@ -13338,7 +13365,7 @@ fancy-log@^1.3.2:
color-support "^1.1.3"
time-stamp "^1.0.0"
-fast-deep-equal@^3.1.1, fast-deep-equal@~3.1.3:
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3:
version "3.1.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
@@ -16017,9 +16044,9 @@ hyperlinker@^1.0.0:
integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==
hyphenate-style-name@^1.0.2:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
- integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
+ integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
i18n-iso-countries@^4.3.1:
version "4.3.1"
@@ -19643,6 +19670,11 @@ mdn-data@2.0.4:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+mdn-data@2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
+ integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
+
mdurl@^1.0.0, mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@@ -20421,9 +20453,9 @@ nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.14.1:
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
nano-css@^5.2.1:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.1.tgz#73b8470fa40b028a134d3393ae36bbb34b9fa332"
- integrity sha512-T54okxMAha0+de+W8o3qFtuWhTxYvqQh2ku1cYEqTTP9mR62nWV2lLK9qRuAGWmoaYWhU7K4evT9Lc1iF65wuw==
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.0.tgz#9d3cd29788d48b6a07f52aa4aec7cf4da427b6b5"
+ integrity sha512-uM/9NGK9/E9/sTpbIZ/bQ9xOLOIHZwrrb/CRlbDHBU/GFS7Gshl24v/WJhwsVViWkpOXUmiZ66XO7fSB4Wd92Q==
dependencies:
css-tree "^1.0.0-alpha.28"
csstype "^2.5.5"
@@ -23581,24 +23613,30 @@ react-transition-group@^4.3.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
-react-use@^13.27.0:
- version "13.27.0"
- resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.27.0.tgz#53a619dc9213e2cbe65d6262e8b0e76641ade4aa"
- integrity sha512-2lyTyqJWyvnaP/woVtDcFS4B5pUYz0FQWI9pVHk/6TBWom2x3/ziJthkEn/LbCA9Twv39xSQU7Dn0zdIWfsNTQ==
+react-universal-interface@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
+ integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
+
+react-use@^15.3.4:
+ version "15.3.4"
+ resolved "https://registry.yarnpkg.com/react-use/-/react-use-15.3.4.tgz#f853d310bd71f75b38900a8caa3db93f6dc6e872"
+ integrity sha512-cHq1dELW6122oi1+xX7lwNyE/ugZs5L902BuO8eFJCfn2api1KeuPVG1M/GJouVARoUf54S2dYFMKo5nQXdTag==
dependencies:
- "@types/js-cookie" "2.2.5"
- "@xobotyi/scrollbar-width" "1.9.4"
+ "@types/js-cookie" "2.2.6"
+ "@xobotyi/scrollbar-width" "1.9.5"
copy-to-clipboard "^3.2.0"
- fast-deep-equal "^3.1.1"
+ fast-deep-equal "^3.1.3"
fast-shallow-equal "^1.0.0"
js-cookie "^2.2.1"
nano-css "^5.2.1"
+ react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.0.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^2.1.0"
ts-easing "^0.2.0"
- tslib "^1.10.0"
+ tslib "^2.0.0"
react-virtualized-auto-sizer@^1.0.2:
version "1.0.2"
@@ -24055,11 +24093,16 @@ regenerator-runtime@^0.12.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
-regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
+regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.3:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+regenerator-runtime@^0.13.4:
+ version "0.13.7"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+ integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
+
regenerator-transform@^0.14.2:
version "0.14.4"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
@@ -24794,9 +24837,9 @@ rsvp@^4.8.4:
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
rtl-css-js@^1.9.0:
- version "1.13.1"
- resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.13.1.tgz#80deabf6e8f36d6767d495cd3eb60fecb20c67e1"
- integrity sha512-jgkIDj6Xi25kAEm5oYM3ZMFiOQhpLEcXi2LY/6bVr91cVz73hciHKneL5AMVPxOcks/JuizSaaNsvNRkeAWe3w==
+ version "1.14.0"
+ resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.14.0.tgz#daa4f192a92509e292a0519f4b255e6e3c076b7d"
+ integrity sha512-Dl5xDTeN3e7scU1cWX8c9b6/Nqz3u/HgR4gePc1kWXYiQWVQbKCEyK6+Hxve9LbcJ5EieHy1J9nJCN3grTtGwg==
dependencies:
"@babel/runtime" "^7.1.2"
@@ -25035,9 +25078,9 @@ scope-analyzer@^2.0.1:
get-assigned-identifiers "^1.1.0"
screenfull@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.0.tgz#5c2010c0e84fd4157bf852877698f90b8cbe96f6"
- integrity sha512-yShzhaIoE9OtOhWVyBBffA6V98CDCoyHTsp8228blmqYy1Z5bddzE/4FPiJKlr8DVR4VBiiUyfPzIQPIYDkeMA==
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.2.tgz#b9acdcf1ec676a948674df5cd0ff66b902b0bed7"
+ integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ==
scss-tokenizer@^0.2.3:
version "0.2.3"
@@ -25665,9 +25708,9 @@ source-map@^0.7.3:
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sourcemap-codec@^1.4.1:
- version "1.4.6"
- resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9"
- integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==
+ version "1.4.8"
+ resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
+ integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
space-separated-tokens@^1.0.0:
version "1.1.2"
@@ -25875,12 +25918,12 @@ stack-chain@^2.0.0:
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-2.0.0.tgz#d73d1172af89565f07438b5bcc086831b6689b2d"
integrity sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==
-stack-generator@^2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.4.tgz#027513eab2b195bbb43b9c8360ba2dd0ab54de09"
- integrity sha512-ha1gosTNcgxwzo9uKTQ8zZ49aUp5FIUW58YHFxCqaAHtE0XqBg0chGFYA1MfmW//x1KWq3F4G7Ug7bJh4RiRtg==
+stack-generator@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.5.tgz#fb00e5b4ee97de603e0773ea78ce944d81596c36"
+ integrity sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==
dependencies:
- stackframe "^1.1.0"
+ stackframe "^1.1.1"
stack-trace@0.0.10, stack-trace@0.0.x:
version "0.0.10"
@@ -25904,10 +25947,10 @@ stackframe@^0.3.1:
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4"
integrity sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=
-stackframe@^1.1.0, stackframe@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.1.tgz#ffef0a3318b1b60c3b58564989aca5660729ec71"
- integrity sha512-0PlYhdKh6AfFxRyK/v+6/k+/mMfyiEBbTM5L94D0ZytQnJ166wuwoTYLHFWGbs2dpA8Rgq763KGWmN1EQEYHRQ==
+stackframe@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303"
+ integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==
stackman@^4.0.1:
version "4.0.1"
@@ -25920,22 +25963,22 @@ stackman@^4.0.1:
error-callsites "^2.0.3"
load-source-map "^1.0.0"
-stacktrace-gps@^3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57"
- integrity sha512-51Rr7dXkyFUKNmhY/vqZWK+EvdsfFSRiQVtgHTFlAdNIYaDD7bVh21yBHXaNWAvTD+w+QSjxHg7/v6Tz4veExA==
+stacktrace-gps@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz#7688dc2fc09ffb3a13165ebe0dbcaf41bcf0c69a"
+ integrity sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==
dependencies:
source-map "0.5.6"
- stackframe "^1.1.0"
+ stackframe "^1.1.1"
stacktrace-js@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.1.tgz#ebdb0e9a16e6f171f96ca7878404e7f15c3d42ba"
- integrity sha512-13oDNgBSeWtdGa4/2BycNyKqe+VktCoJ8VLx4pDoJkwGGJVtiHdfMOAj3aW9xTi8oR2v34z9IcvfCvT6XNdNAw==
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b"
+ integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==
dependencies:
- error-stack-parser "^2.0.4"
- stack-generator "^2.0.4"
- stacktrace-gps "^3.0.3"
+ error-stack-parser "^2.0.6"
+ stack-generator "^2.0.5"
+ stacktrace-gps "^3.0.4"
state-toggle@^1.0.0:
version "1.0.0"
@@ -26956,9 +26999,9 @@ throat@^5.0.0:
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
throttle-debounce@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5"
- integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
+ integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
throttleit@^1.0.0:
version "1.0.0"