From a9a834a105ae91d420fe6e93abab702fbe562af2 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Thu, 10 Jun 2021 14:20:30 -0300 Subject: [PATCH] [Workplace Search] Add Account Settings page imported from Security plugin (#99791) * Copy lazy_wrapper and suspense_error_boundary from Spaces plugin These components are needed to enable async loading of Security components into Enterprise Search. The components are copied without any changes except for i18n ids, so it's easier to DRY out in the future if needed. * Create async versions of personal_info and change_password components * Create ui_api that allows to load Security components asuncronously The patterns were mostly copied from Spaces plugin * Make ui_api available through Security components's lifecycle methods * Import Security plugin into Enterprise Search * Add Security plugin and Notifications service to Kibana Logic file * Export the required components from the Security plugin and use them in the new AccountSettings component * Update link to the Account Settings page * Move getUiApi call to security start and pass core instead of getStartServices * Simplify import of change_password_async component by providing... ... `notifications` and `userAPIClient` props in the security plugin * Remove UserAPIClient from ui_api It's not needed anymore since the components are initiated with this prop already passed * Export ChangePasswordProps and PersonalInfoProps from account_management/index.ts This makes it easier to import these props from outside the account_management folder * Remove notifications service from kibana_logic It is not needed anymore since we're initializing security components with notifications already provided * Add UiApi to SecurityPluginStart interface * Utilize index files for exporting Props types * Replace Pick<...> with two separate interfaces as it doesn't work well with our docs * Add a comment explaining why we're not loading async components through index file --- .../__mocks__/kea_logic/kibana_logic.mock.ts | 3 ++ .../public/applications/index.test.tsx | 2 + .../public/applications/index.tsx | 1 + .../shared/kibana/kibana_logic.ts | 3 ++ .../layout/account_header/account_header.tsx | 5 +- .../applications/workplace_search/index.tsx | 7 +++ .../applications/workplace_search/routes.ts | 1 - .../account_settings/account_settings.tsx | 39 ++++++++++++++ .../views/account_settings/index.ts | 8 +++ .../enterprise_search/public/plugin.ts | 3 ++ .../change_password/change_password.tsx | 7 ++- .../change_password/change_password_async.tsx | 29 ++++++++++ .../change_password/index.ts | 2 + .../public/account_management/index.ts | 3 ++ .../account_management/personal_info/index.ts | 2 + .../personal_info/personal_info.tsx | 4 +- .../personal_info/personal_info_async.tsx | 17 ++++++ x-pack/plugins/security/public/mocks.ts | 2 + .../plugins/security/public/plugin.test.tsx | 6 +++ x-pack/plugins/security/public/plugin.tsx | 7 +++ .../public/suspense_error_boundary/index.ts | 8 +++ .../suspense_error_boundary.tsx | 54 +++++++++++++++++++ .../security/public/ui_api/components.tsx | 43 +++++++++++++++ .../security/public/ui_api/index.mock.ts | 17 ++++++ .../plugins/security/public/ui_api/index.ts | 34 ++++++++++++ .../security/public/ui_api/lazy_wrapper.tsx | 39 ++++++++++++++ 26 files changed, 338 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts create mode 100644 x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx create mode 100644 x-pack/plugins/security/public/account_management/personal_info/personal_info_async.tsx create mode 100644 x-pack/plugins/security/public/suspense_error_boundary/index.ts create mode 100644 x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx create mode 100644 x-pack/plugins/security/public/ui_api/components.tsx create mode 100644 x-pack/plugins/security/public/ui_api/index.mock.ts create mode 100644 x-pack/plugins/security/public/ui_api/index.ts create mode 100644 x-pack/plugins/security/public/ui_api/lazy_wrapper.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts index ebb6f8c4fe5aa..f2c6ccfacf2bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts @@ -7,6 +7,8 @@ import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; +import { securityMock } from '../../../../../security/public/mocks'; + import { mockHistory } from '../react_router/state.mock'; export const mockKibanaValues = { @@ -18,6 +20,7 @@ export const mockKibanaValues = { }, history: mockHistory, navigateToUrl: jest.fn(), + security: securityMock.createStart(), setBreadcrumbs: jest.fn(), setChromeIsVisible: jest.fn(), setDocTitle: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 2e0940b9c4af2..cb73686eb4c5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -12,6 +12,7 @@ import { getContext } from 'kea'; import { coreMock } from '../../../../../src/core/public/mocks'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { licensingMock } from '../../../licensing/public/mocks'; +import { securityMock } from '../../../security/public/mocks'; import { AppSearch } from './app_search'; import { EnterpriseSearch } from './enterprise_search'; @@ -27,6 +28,7 @@ describe('renderApp', () => { plugins: { licensing: licensingMock.createStart(), charts: chartPluginMock.createStartContract(), + security: securityMock.createStart(), }, } as any; const pluginData = { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index c2bf77751528a..ba2b28e64b9cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -48,6 +48,7 @@ export const renderApp = ( cloud: plugins.cloud || {}, history: params.history, navigateToUrl: core.application.navigateToUrl, + security: plugins.security || {}, setBreadcrumbs: core.chrome.setBreadcrumbs, setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 2bef7d373f160..9c6db7d09f72c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -13,6 +13,7 @@ import { kea, MakeLogicType } from 'kea'; import { ApplicationStart, ChromeBreadcrumb } from '../../../../../../../src/core/public'; import { ChartsPluginStart } from '../../../../../../../src/plugins/charts/public'; import { CloudSetup } from '../../../../../cloud/public'; +import { SecurityPluginStart } from '../../../../../security/public'; import { HttpLogic } from '../http'; import { createHref, CreateHrefOptions } from '../react_router_helpers'; @@ -23,6 +24,7 @@ interface KibanaLogicProps { cloud: Partial; charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; + security: Partial; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; @@ -47,6 +49,7 @@ export const KibanaLogic = kea>({ }, {}, ], + security: [props.security || {}, {}], setBreadcrumbs: [props.setBreadcrumbs, {}], setChromeIsVisible: [props.setChromeIsVisible, {}], setDocTitle: [props.setDocTitle, {}], diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx index 92a936fcdbefe..b1e190edade2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx @@ -27,7 +27,7 @@ import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url' import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import { WORKPLACE_SEARCH_TITLE, ACCOUNT_NAV } from '../../../constants'; -import { PERSONAL_SOURCES_PATH, LOGOUT_ROUTE, KIBANA_ACCOUNT_ROUTE } from '../../../routes'; +import { PERSONAL_SOURCES_PATH, LOGOUT_ROUTE, PERSONAL_SETTINGS_PATH } from '../../../routes'; export const AccountHeader: React.FC = () => { const [isPopoverOpen, setPopover] = useState(false); @@ -44,8 +44,7 @@ export const AccountHeader: React.FC = () => { const accountNavItems = [ - {/* TODO: Once auth is completed, we need to have non-admins redirect to the self-hosted form */} - {ACCOUNT_NAV.SETTINGS} + {ACCOUNT_NAV.SETTINGS} , {ACCOUNT_NAV.LOGOUT} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index ba5fb7c9d377d..0fc8a6e7c7c0d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -28,7 +28,9 @@ import { ORG_SETTINGS_PATH, ROLE_MAPPINGS_PATH, SECURITY_PATH, + PERSONAL_SETTINGS_PATH, } from './routes'; +import { AccountSettings } from './views/account_settings'; import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; @@ -103,6 +105,11 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + + + + } />} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 5e5d6e2c82b31..1fe8019c4b364 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -13,7 +13,6 @@ export const SETUP_GUIDE_PATH = '/setup_guide'; export const NOT_FOUND_PATH = '/404'; export const LOGOUT_ROUTE = '/logout'; -export const KIBANA_ACCOUNT_ROUTE = '/security/account'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx new file mode 100644 index 0000000000000..e28faaeec8993 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, useMemo } from 'react'; + +import { useValues } from 'kea'; + +import type { AuthenticatedUser } from '../../../../../../security/public'; +import { KibanaLogic } from '../../../shared/kibana/kibana_logic'; + +export const AccountSettings: React.FC = () => { + const { security } = useValues(KibanaLogic); + + const [currentUser, setCurrentUser] = useState(null); + + useEffect(() => { + security!.authc!.getCurrentUser().then(setCurrentUser); + }, [security.authc]); + + const PersonalInfo = useMemo(() => security!.uiApi!.components.getPersonalInfo, [security.uiApi]); + const ChangePassword = useMemo(() => security!.uiApi!.components.getChangePassword, [ + security.uiApi, + ]); + + if (!currentUser) { + return null; + } + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts new file mode 100644 index 0000000000000..016b8f721aea4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AccountSettings } from './account_settings'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index e402d233da58d..6e521efc369df 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -21,6 +21,7 @@ import { } from '../../../../src/plugins/home/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/public'; import { APP_SEARCH_PLUGIN, @@ -42,11 +43,13 @@ export interface ClientData extends InitialAppData { interface PluginsSetup { cloud?: CloudSetup; home?: HomePublicPluginSetup; + security?: SecurityPluginSetup; } export interface PluginsStart { cloud?: CloudSetup; licensing: LicensingPluginStart; charts: ChartsPluginStart; + security?: SecurityPluginStart; } export class EnterpriseSearchPlugin implements Plugin { diff --git a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx index 90d63d8b43bc7..ac0e284c8b9ad 100644 --- a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -17,13 +17,16 @@ import { canUserChangePassword } from '../../../common/model'; import type { UserAPIClient } from '../../management/users'; import { ChangePasswordForm } from '../../management/users/components/change_password_form'; -interface Props { +export interface ChangePasswordProps { user: AuthenticatedUser; +} + +export interface ChangePasswordPropsInternal extends ChangePasswordProps { userAPIClient: PublicMethodsOf; notifications: NotificationsSetup; } -export class ChangePassword extends Component { +export class ChangePassword extends Component { public render() { const canChangePassword = canUserChangePassword(this.props.user); diff --git a/x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx new file mode 100644 index 0000000000000..a4ad769146e59 --- /dev/null +++ b/x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import type { CoreStart } from 'src/core/public'; + +import { UserAPIClient } from '../../management/users'; +import type { ChangePasswordProps } from './change_password'; + +export const getChangePasswordComponent = async ( + core: CoreStart +): Promise> => { + const { ChangePassword } = await import('./change_password'); + + return (props: ChangePasswordProps) => { + return ( + + ); + }; +}; diff --git a/x-pack/plugins/security/public/account_management/change_password/index.ts b/x-pack/plugins/security/public/account_management/change_password/index.ts index c73b497512cdf..028d0f6cc7497 100644 --- a/x-pack/plugins/security/public/account_management/change_password/index.ts +++ b/x-pack/plugins/security/public/account_management/change_password/index.ts @@ -6,3 +6,5 @@ */ export { ChangePassword } from './change_password'; + +export type { ChangePasswordProps } from './change_password'; diff --git a/x-pack/plugins/security/public/account_management/index.ts b/x-pack/plugins/security/public/account_management/index.ts index bfba213c632d0..2d1045723a6e1 100644 --- a/x-pack/plugins/security/public/account_management/index.ts +++ b/x-pack/plugins/security/public/account_management/index.ts @@ -6,3 +6,6 @@ */ export { accountManagementApp } from './account_management_app'; + +export type { ChangePasswordProps } from './change_password'; +export type { PersonalInfoProps } from './personal_info'; 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 index a7d2873e85391..6dc6489afa8c5 100644 --- a/x-pack/plugins/security/public/account_management/personal_info/index.ts +++ b/x-pack/plugins/security/public/account_management/personal_info/index.ts @@ -6,3 +6,5 @@ */ export { PersonalInfo } from './personal_info'; + +export type { PersonalInfoProps } 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/personal_info/personal_info.tsx index e9de2b8a69bfa..20b21fc0c30ce 100644 --- a/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx +++ b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx @@ -12,11 +12,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import type { AuthenticatedUser } from '../../../common/model'; -interface Props { +export interface PersonalInfoProps { user: AuthenticatedUser; } -export const PersonalInfo = (props: Props) => { +export const PersonalInfo = (props: PersonalInfoProps) => { return ( > => { + const { PersonalInfo } = await import('./personal_info'); + return (props: PersonalInfoProps) => { + return ; + }; +}; diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts index 829c3ced9dddb..b936f8d01cfd5 100644 --- a/x-pack/plugins/security/public/mocks.ts +++ b/x-pack/plugins/security/public/mocks.ts @@ -11,6 +11,7 @@ import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock'; import { authenticationMock } from './authentication/index.mock'; import { navControlServiceMock } from './nav_control/index.mock'; import { createSessionTimeoutMock } from './session/session_timeout.mock'; +import { getUiApiMock } from './ui_api/index.mock'; function createSetupMock() { return { @@ -23,6 +24,7 @@ function createStartMock() { return { authc: authenticationMock.createStart(), navControlService: navControlServiceMock.createStart(), + uiApi: getUiApiMock.createStart(), }; } diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 9c31919bd5d29..c4c551e4bb5b5 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -100,6 +100,12 @@ describe('Security Plugin', () => { features: {} as FeaturesPluginStart, }) ).toEqual({ + uiApi: { + components: { + getChangePassword: expect.any(Function), + getPersonalInfo: expect.any(Function), + }, + }, authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function), diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 69533ea534802..fbb282ee246f9 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -30,6 +30,8 @@ import type { SecurityNavControlServiceStart } from './nav_control'; import { SecurityNavControlService } from './nav_control'; import { SecurityCheckupService } from './security_checkup'; import { SessionExpired, SessionTimeout, UnauthorizedResponseHttpInterceptor } from './session'; +import type { UiApi } from './ui_api'; +import { getUiApi } from './ui_api'; export interface PluginSetupDependencies { licensing: LicensingPluginSetup; @@ -150,6 +152,7 @@ export class SecurityPlugin } return { + uiApi: getUiApi({ core }), navControlService: this.navControlService.start({ core }), authc: this.authc as AuthenticationServiceStart, }; @@ -184,4 +187,8 @@ export interface SecurityPluginStart { * Exposes authentication information about the currently logged in user. */ authc: AuthenticationServiceStart; + /** + * Exposes UI components that will be loaded asynchronously. + */ + uiApi: UiApi; } diff --git a/x-pack/plugins/security/public/suspense_error_boundary/index.ts b/x-pack/plugins/security/public/suspense_error_boundary/index.ts new file mode 100644 index 0000000000000..061923c8445c2 --- /dev/null +++ b/x-pack/plugins/security/public/suspense_error_boundary/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SuspenseErrorBoundary } from './suspense_error_boundary'; diff --git a/x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx b/x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx new file mode 100644 index 0000000000000..313401ff45bd9 --- /dev/null +++ b/x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLoadingSpinner } from '@elastic/eui'; +import type { PropsWithChildren } from 'react'; +import React, { Component, Suspense } from 'react'; + +import { i18n } from '@kbn/i18n'; +import type { NotificationsStart } from 'src/core/public'; + +interface Props { + notifications: NotificationsStart; +} + +interface State { + error: Error | null; +} + +export class SuspenseErrorBoundary extends Component, State> { + state: State = { + error: null, + }; + + static getDerivedStateFromError(error: Error) { + // Update state so next render shows fallback UI. + return { error }; + } + + public componentDidCatch(error: Error) { + const { notifications } = this.props; + if (notifications) { + const title = i18n.translate('xpack.security.uiApi.errorBoundaryToastTitle', { + defaultMessage: 'Failed to load Kibana asset', + }); + const toastMessage = i18n.translate('xpack.security.uiApi.errorBoundaryToastMessage', { + defaultMessage: 'Reload page to continue.', + }); + notifications.toasts.addError(error, { title, toastMessage }); + } + } + + render() { + const { children, notifications } = this.props; + const { error } = this.state; + if (!notifications || error) { + return null; + } + return }>{children}; + } +} diff --git a/x-pack/plugins/security/public/ui_api/components.tsx b/x-pack/plugins/security/public/ui_api/components.tsx new file mode 100644 index 0000000000000..a488bc359b538 --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/components.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC, PropsWithChildren, PropsWithRef } from 'react'; +import React from 'react'; + +import type { CoreStart } from 'src/core/public'; + +/** + * We're importing specific files here instead of passing them + * through the index file. It helps to keep the bundle size low. + * + * Importing async components through the index file increases the bundle size. + * It happens because the bundle starts to also include all the sync dependencies + * available through the index file. + */ +import { getChangePasswordComponent } from '../account_management/change_password/change_password_async'; +import { getPersonalInfoComponent } from '../account_management/personal_info/personal_info_async'; +import { LazyWrapper } from './lazy_wrapper'; + +export interface GetComponentsOptions { + core: CoreStart; +} + +export const getComponents = ({ core }: GetComponentsOptions) => { + /** + * Returns a function that creates a lazy-loading version of a component. + */ + function wrapLazy(fn: () => Promise>) { + return (props: JSX.IntrinsicAttributes & PropsWithRef>) => ( + + ); + } + + return { + getPersonalInfo: wrapLazy(getPersonalInfoComponent), + getChangePassword: wrapLazy(() => getChangePasswordComponent(core)), + }; +}; diff --git a/x-pack/plugins/security/public/ui_api/index.mock.ts b/x-pack/plugins/security/public/ui_api/index.mock.ts new file mode 100644 index 0000000000000..c35f9342be6ca --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UiApi } from './'; + +export const getUiApiMock = { + createStart: (): jest.Mocked => ({ + components: { + getPersonalInfo: jest.fn(), + getChangePassword: jest.fn(), + }, + }), +}; diff --git a/x-pack/plugins/security/public/ui_api/index.ts b/x-pack/plugins/security/public/ui_api/index.ts new file mode 100644 index 0000000000000..e53564074940a --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/index.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ReactElement } from 'react'; + +import type { CoreStart } from 'src/core/public'; + +import type { ChangePasswordProps, PersonalInfoProps } from '../account_management'; +import { getComponents } from './components'; + +interface GetUiApiOptions { + core: CoreStart; +} + +type LazyComponentFn = (props: T) => ReactElement; + +export interface UiApi { + components: { + getPersonalInfo: LazyComponentFn; + getChangePassword: LazyComponentFn; + }; +} + +export const getUiApi = ({ core }: GetUiApiOptions): UiApi => { + const components = getComponents({ core }); + + return { + components, + }; +}; diff --git a/x-pack/plugins/security/public/ui_api/lazy_wrapper.tsx b/x-pack/plugins/security/public/ui_api/lazy_wrapper.tsx new file mode 100644 index 0000000000000..6a37b35df7327 --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/lazy_wrapper.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC, PropsWithChildren, PropsWithRef, ReactElement } from 'react'; +import React, { lazy, useMemo } from 'react'; + +import type { CoreStart } from 'src/core/public'; + +import { SuspenseErrorBoundary } from '../suspense_error_boundary'; + +interface InternalProps { + fn: () => Promise>; + core: CoreStart; + props: JSX.IntrinsicAttributes & PropsWithRef>; +} + +export const LazyWrapper: (props: InternalProps) => ReactElement | null = ({ + fn, + core, + props, +}) => { + const { notifications } = core; + + const LazyComponent = useMemo(() => lazy(() => fn().then((x) => ({ default: x }))), [fn]); + + if (!notifications) { + return null; + } + + return ( + + + + ); +};