From 032b5cb2a61858e82a0623d4636d2a31c97ecc83 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:01:04 -0500 Subject: [PATCH] Use error boundaries when lazy-loading components If we have an exception where we do not use an error boundary, I added a comment explaining why. I also standardized our usages of `lazy` and `Suspense` by destructuring the React import. --- .../advanced_settings_title.tsx | 15 ++--- .../components/selectable_spaces_control.tsx | 18 +++--- .../components/space_result.tsx | 21 ++++--- .../copy_saved_objects_to_space_action.tsx | 56 +++++++++++++------ .../copy_saved_objects_to_space_service.ts | 7 ++- .../customize_space/customize_space.tsx | 15 ++--- .../spaces_grid/spaces_grid_page.tsx | 14 +++-- .../nav_control/components/spaces_menu.tsx | 14 +++-- .../spaces/public/nav_control/nav_control.tsx | 8 +-- .../nav_control/nav_control_popover.tsx | 15 ++--- x-pack/plugins/spaces/public/plugin.tsx | 1 + .../components/selectable_spaces_control.tsx | 15 ++--- .../share_to_space_flyout_internal.tsx | 16 +++--- .../public/space_list/space_list_internal.tsx | 15 ++--- .../space_selector/components/space_card.tsx | 15 ++--- .../public/suspense_error_boundary/index.ts | 8 +++ .../suspense_error_boundary.tsx | 55 ++++++++++++++++++ .../spaces/public/ui_api/components.tsx | 14 ++--- .../spaces/public/ui_api/lazy_wrapper.tsx | 31 ++++++++-- 19 files changed, 237 insertions(+), 116 deletions(-) create mode 100644 x-pack/plugins/spaces/public/suspense_error_boundary/index.ts create mode 100644 x-pack/plugins/spaces/public/suspense_error_boundary/suspense_error_boundary.tsx diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx index 47e673fda8bd6..9bec9e32ca736 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx +++ b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx @@ -6,13 +6,18 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiTitle } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import React, { lazy, Suspense, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import type { Space } from 'src/plugins/spaces_oss/common'; import { getSpaceAvatarComponent } from '../../../space_avatar'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { getActiveSpace: () => Promise; } @@ -26,16 +31,12 @@ export const AdvancedSettingsTitle = (props: Props) => { if (!activeSpace) return null; - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); - return ( - }> + }> - + diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx index 69ff131d6f788..e1ecc06935791 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -9,13 +9,18 @@ import './selectable_spaces_control.scss'; import type { EuiSelectableOption } from '@elastic/eui'; import { EuiIconTip, EuiLoadingSpinner, EuiSelectable } from '@elastic/eui'; -import React, { Fragment } from 'react'; +import React, { lazy, Suspense } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import type { Space } from 'src/plugins/spaces_oss/common'; import { getSpaceAvatarComponent } from '../../space_avatar'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { spaces: Space[]; selectedSpaceIds: string[]; @@ -31,9 +36,6 @@ export const SelectableSpacesControl = (props: Props) => { return ; } - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); const disabledIndicator = ( { } return ( - }> + }> updateSelectedSpaces(newOptions as SpaceOption[])} @@ -85,13 +87,13 @@ export const SelectableSpacesControl = (props: Props) => { > {(list, search) => { return ( - + <> {search} {list} - + ); }} - + ); }; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx index 6bd8cecac956b..6d14584ac21a9 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx @@ -15,7 +15,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { lazy, Suspense, useState } from 'react'; import type { Space } from 'src/plugins/spaces_oss/common'; @@ -25,6 +25,11 @@ import type { ImportRetry } from '../types'; import { CopyStatusSummaryIndicator } from './copy_status_summary_indicator'; import { SpaceCopyResultDetails } from './space_result_details'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { space: Space; summarizedCopyResult: SummarizedCopyToSpaceResult; @@ -43,9 +48,6 @@ const getInitialDestinationMap = (objects: SummarizedCopyToSpaceResult['objects' export const SpaceResultProcessing = (props: Pick) => { const { space } = props; - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); return ( ) => { buttonContent={ - }> + }> - + {space.name} @@ -86,9 +88,6 @@ export const SpaceResult = (props: Props) => { const onDestinationMapChange = (value?: Map) => { setDestinationMap(value || getInitialDestinationMap(objects)); }; - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); return ( { buttonContent={ - }> + }> - + {space.name} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx index 9e98ed9ffbe39..592f12ca628f0 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx @@ -5,26 +5,43 @@ * 2.0. */ -import { EuiLoadingSpinner } from '@elastic/eui'; -import React, { lazy, useMemo } from 'react'; +import React, { lazy, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import type { NotificationsStart, StartServicesAccessor } from 'src/core/public'; import type { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; import { SavedObjectsManagementAction } from '../../../../../src/plugins/saved_objects_management/public'; +import type { PluginsStart } from '../plugin'; +import { SuspenseErrorBoundary } from '../suspense_error_boundary'; import type { CopyToSpaceFlyoutProps } from './components'; import { getCopyToSpaceFlyoutComponent } from './components'; -const Wrapper = (props: CopyToSpaceFlyoutProps) => { - const LazyComponent = useMemo( - () => lazy(() => getCopyToSpaceFlyoutComponent().then((component) => ({ default: component }))), - [] - ); +const LazyCopyToSpaceFlyout = lazy(() => + getCopyToSpaceFlyoutComponent().then((component) => ({ default: component })) +); + +interface WrapperProps { + getStartServices: StartServicesAccessor; + props: CopyToSpaceFlyoutProps; +} + +const Wrapper = ({ getStartServices, props }: WrapperProps) => { + const [notifications, setNotifications] = useState(); + useEffect(() => { + getStartServices().then(([coreStart]) => { + setNotifications(coreStart.notifications); + }); + }); + + if (!notifications) { + return null; + } return ( - }> - - + + + ); }; @@ -48,7 +65,7 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem }, }; - constructor() { + constructor(private getStartServices: StartServicesAccessor) { super(); } @@ -57,15 +74,18 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem throw new Error('No record available! `render()` was likely called before `start()`.'); } - const savedObjectTarget = { - type: this.record.type, - id: this.record.id, - namespaces: this.record.namespaces ?? [], - title: this.record.meta.title, - icon: this.record.meta.icon, + const props: CopyToSpaceFlyoutProps = { + onClose: this.onClose, + savedObjectTarget: { + type: this.record.type, + id: this.record.id, + namespaces: this.record.namespaces ?? [], + title: this.record.meta.title, + icon: this.record.meta.icon, + }, }; - return ; + return ; }; private onClose = () => { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts index 400ccc39d1555..17bb26cbf7f11 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts @@ -5,17 +5,20 @@ * 2.0. */ +import type { StartServicesAccessor } from 'src/core/public'; import type { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public'; +import type { PluginsStart } from '../plugin'; import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; interface SetupDeps { savedObjectsManagementSetup: SavedObjectsManagementPluginSetup; + getStartServices: StartServicesAccessor; } export class CopySavedObjectsToSpaceService { - public setup({ savedObjectsManagementSetup }: SetupDeps) { - const action = new CopyToSpaceSavedObjectsManagementAction(); + public setup({ savedObjectsManagementSetup, getStartServices }: SetupDeps) { + const action = new CopyToSpaceSavedObjectsManagementAction(getStartServices); savedObjectsManagementSetup.actions.register(action); } } diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx b/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx index 796a9a872b855..4bbad58b5d139 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx @@ -17,7 +17,7 @@ import { EuiTitle, } from '@elastic/eui'; import type { ChangeEvent } from 'react'; -import React, { Component, Fragment } from 'react'; +import React, { Component, Fragment, lazy, Suspense } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -31,6 +31,11 @@ import { SectionPanel } from '../section_panel'; import { CustomizeSpaceAvatar } from './customize_space_avatar'; import { SpaceIdentifier } from './space_identifier'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { validator: SpaceValidator; space: Partial; @@ -63,10 +68,6 @@ export class CustomizeSpace extends Component { initialFocus: 'input[name="spaceInitials"]', }; - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); - return ( { )} onClick={this.togglePopover} > - }> + }> - + } closePopover={this.closePopover} diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 03064b3672bb0..ac57a566e2a00 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -18,7 +18,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { Component, Fragment } from 'react'; +import React, { Component, Fragment, lazy, Suspense } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -40,6 +40,11 @@ import type { SpacesManager } from '../../spaces_manager'; import { ConfirmDeleteModal, UnauthorizedPrompt } from '../components'; import { getEnabledFeatures } from '../lib/feature_utils'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { spacesManager: SpacesManager; notifications: NotificationsStart; @@ -259,15 +264,12 @@ export class SpacesGridPage extends Component { name: '', width: '50px', render: (value: string, record: Space) => { - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); return ( - }> + }> - + ); }, }, diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx index 87e307483867d..fda79bd93e39a 100644 --- a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx @@ -16,7 +16,7 @@ import { EuiText, } from '@elastic/eui'; import type { ReactElement } from 'react'; -import React, { Component } from 'react'; +import React, { Component, lazy, Suspense } from 'react'; import type { InjectedIntl } from '@kbn/i18n/react'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; @@ -27,6 +27,11 @@ import { addSpaceIdToPath, ENTER_SPACE_PATH, SPACE_SEARCH_COUNT_THRESHOLD } from import { getSpaceAvatarComponent } from '../../space_avatar'; import { ManageSpacesButton } from './manage_spaces_button'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { id: string; spaces: Space[]; @@ -187,13 +192,10 @@ class SpacesMenuUI extends Component { }; private renderSpaceMenuItem = (space: Space): JSX.Element => { - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); const icon = ( - }> + }> - + ); return ( null; } - const LazyNavControlPopover = React.lazy(() => + const LazyNavControlPopover = lazy(() => import('./nav_control_popover').then(({ NavControlPopover }) => ({ default: NavControlPopover, })) @@ -30,7 +30,7 @@ export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreSta ReactDOM.render( - }> + }> - + , targetDomElement ); diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx index 1c43e9a2d2088..392219d480e67 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx @@ -7,7 +7,7 @@ import type { PopoverAnchorPosition } from '@elastic/eui'; import { EuiHeaderSectionItemButton, EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; -import React, { Component } from 'react'; +import React, { Component, lazy, Suspense } from 'react'; import type { Subscription } from 'rxjs'; import type { ApplicationStart, Capabilities } from 'src/core/public'; @@ -18,6 +18,11 @@ import type { SpacesManager } from '../spaces_manager'; import { SpacesDescription } from './components/spaces_description'; import { SpacesMenu } from './components/spaces_menu'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { spacesManager: SpacesManager; anchorPosition: PopoverAnchorPosition; @@ -137,14 +142,10 @@ export class NavControlPopover extends Component { return this.getButton(, 'loading'); } - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); - return this.getButton( - }> + }> - , + , (activeSpace as Space).name ); }; diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index dad0f0f3ba426..062a534c91c84 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -86,6 +86,7 @@ export class SpacesPlugin implements Plugin + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { spaces: ShareToSpaceTarget[]; shareOptions: ShareOptions; @@ -90,10 +95,6 @@ export const SelectableSpacesControl = (props: Props) => { const { application, docLinks } = services; const { selectedSpaceIds, initiallySelectedSpaceIds } = shareOptions; - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); - const activeSpaceId = !enableSpaceAgnosticBehavior && spaces.find((space) => space.isActiveSpace)!.id; const isGlobalControlChecked = selectedSpaceIds.includes(ALL_SPACES_ID); @@ -200,7 +201,7 @@ export const SelectableSpacesControl = (props: Props) => { fullWidth > <> - }> + }> updateSelectedSpaces(newOptions as SpaceOption[])} @@ -222,7 +223,7 @@ export const SelectableSpacesControl = (props: Props) => { ); }} - + {getUnknownSpacesLabel()} {getNoSpacesAvailable()} diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx index 61c9d05d22da7..fc5d42df8af5e 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx @@ -20,7 +20,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -39,6 +39,12 @@ import type { ShareOptions } from '../types'; import { DEFAULT_OBJECT_NOUN } from './constants'; import { ShareToSpaceForm } from './share_to_space_form'; +// No need to wrap LazyCopyToSpaceFlyout in an error boundary, because the ShareToSpaceFlyoutInternal component itself is only ever used in +// a lazy-loaded fashion with an error boundary. +const LazyCopyToSpaceFlyout = lazy(() => + getCopyToSpaceFlyoutComponent().then((component) => ({ default: component })) +); + const ALL_SPACES_TARGET = i18n.translate('xpack.spaces.shareToSpace.allSpacesTarget', { defaultMessage: 'all', }); @@ -270,14 +276,10 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { }; if (showMakeCopy) { - const LazyCopyToSpaceFlyout = React.lazy(() => - getCopyToSpaceFlyoutComponent().then((component) => ({ default: component })) - ); - return ( - }> + }> - + ); } diff --git a/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx b/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx index f67cdb918f6e9..1a512fb2d31f4 100644 --- a/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx +++ b/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, } from '@elastic/eui'; import type { ReactNode } from 'react'; -import React, { useEffect, useState } from 'react'; +import React, { lazy, Suspense, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -25,6 +25,11 @@ import { getSpaceAvatarComponent } from '../space_avatar'; import { useSpaces } from '../spaces_context'; import type { ShareToSpacesData, ShareToSpaceTarget } from '../types'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + const DEFAULT_DISPLAY_LIMIT = 5; /** @@ -133,12 +138,8 @@ export const SpaceListInternal = ({ ) : null; - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); - return ( - }> + }> {displayedSpaces.map((space) => { // color may be undefined, which is intentional; SpacesAvatar calls the getSpaceColor function before rendering @@ -152,6 +153,6 @@ export const SpaceListInternal = ({ {unauthorizedSpacesCountBadge} {button} - + ); }; diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx index 0684c4a5b789d..0628f79990af6 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx @@ -8,13 +8,18 @@ import './space_card.scss'; import { EuiCard, EuiLoadingSpinner } from '@elastic/eui'; -import React from 'react'; +import React, { lazy, Suspense } from 'react'; import type { Space } from 'src/plugins/spaces_oss/common'; import { addSpaceIdToPath, ENTER_SPACE_PATH } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; +// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. +const LazySpaceAvatar = lazy(() => + getSpaceAvatarComponent().then((component) => ({ default: component })) +); + interface Props { space: Space; serverBasePath: string; @@ -35,16 +40,12 @@ export const SpaceCard = (props: Props) => { }; function renderSpaceAvatar(space: Space) { - const LazySpaceAvatar = React.lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) - ); - // not announcing space name here because the title of the EuiCard that the SpaceAvatar lives in is already // announcing it. See https://github.com/elastic/kibana/issues/27748 return ( - }> + }> - + ); } diff --git a/x-pack/plugins/spaces/public/suspense_error_boundary/index.ts b/x-pack/plugins/spaces/public/suspense_error_boundary/index.ts new file mode 100644 index 0000000000000..061923c8445c2 --- /dev/null +++ b/x-pack/plugins/spaces/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/spaces/public/suspense_error_boundary/suspense_error_boundary.tsx b/x-pack/plugins/spaces/public/suspense_error_boundary/suspense_error_boundary.tsx new file mode 100644 index 0000000000000..010a01d13cb8a --- /dev/null +++ b/x-pack/plugins/spaces/public/suspense_error_boundary/suspense_error_boundary.tsx @@ -0,0 +1,55 @@ +/* + * 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, { componentStack }: React.ErrorInfo) { + const { notifications } = this.props; + if (notifications) { + const title = i18n.translate('xpack.spaces.uiApi.errorBoundaryToastTitle', { + defaultMessage: 'Failed to load Kibana asset', + }); + const toastMessage = i18n.translate('xpack.spaces.uiApi.errorBoundaryToastMessage', { + defaultMessage: 'Reload page to continue.', + values: { componentStack }, + }); + 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/spaces/public/ui_api/components.tsx b/x-pack/plugins/spaces/public/ui_api/components.tsx index ea470285e34e7..acbabc3e08f4b 100644 --- a/x-pack/plugins/spaces/public/ui_api/components.tsx +++ b/x-pack/plugins/spaces/public/ui_api/components.tsx @@ -34,17 +34,17 @@ export const getComponents = ({ /** * Returns a function that creates a lazy-loading version of a component. */ - function lazy(fn: () => Promise>) { + function wrapLazy(fn: () => Promise>) { return (props: JSX.IntrinsicAttributes & PropsWithRef>) => ( - + ); } return { - getSpacesContext: lazy(() => getSpacesContextWrapper({ spacesManager, getStartServices })), - getShareToSpaceFlyout: lazy(getShareToSpaceFlyoutComponent), - getSpaceList: lazy(getSpaceListComponent), - getLegacyUrlConflict: lazy(() => getLegacyUrlConflict({ getStartServices })), - getSpaceAvatar: lazy(getSpaceAvatarComponent), + getSpacesContext: wrapLazy(() => getSpacesContextWrapper({ spacesManager, getStartServices })), + getShareToSpaceFlyout: wrapLazy(getShareToSpaceFlyoutComponent), + getSpaceList: wrapLazy(getSpaceListComponent), + getLegacyUrlConflict: wrapLazy(() => getLegacyUrlConflict({ getStartServices })), + getSpaceAvatar: wrapLazy(getSpaceAvatarComponent), }; }; diff --git a/x-pack/plugins/spaces/public/ui_api/lazy_wrapper.tsx b/x-pack/plugins/spaces/public/ui_api/lazy_wrapper.tsx index 3a82558525e7e..092d49fe8d535 100644 --- a/x-pack/plugins/spaces/public/ui_api/lazy_wrapper.tsx +++ b/x-pack/plugins/spaces/public/ui_api/lazy_wrapper.tsx @@ -5,20 +5,41 @@ * 2.0. */ -import { EuiLoadingSpinner } from '@elastic/eui'; import type { FC, PropsWithChildren, PropsWithRef, ReactElement } from 'react'; -import React, { lazy, Suspense, useMemo } from 'react'; +import React, { lazy, useEffect, useMemo, useState } from 'react'; + +import type { NotificationsStart, StartServicesAccessor } from 'src/core/public'; + +import type { PluginsStart } from '../plugin'; +import { SuspenseErrorBoundary } from '../suspense_error_boundary'; interface InternalProps { fn: () => Promise>; + getStartServices: StartServicesAccessor; props: JSX.IntrinsicAttributes & PropsWithRef>; } -export const LazyWrapper: (props: InternalProps) => ReactElement = ({ fn, props }) => { +export const LazyWrapper: (props: InternalProps) => ReactElement = ({ + fn, + getStartServices, + props, +}) => { + const [notifications, setNotifications] = useState(undefined); + useEffect(() => { + getStartServices().then(([coreStart]) => { + setNotifications(coreStart.notifications); + }); + }); + const LazyComponent = useMemo(() => lazy(() => fn().then((x) => ({ default: x }))), [fn]); + + if (!notifications) { + return <>; + } + return ( - }> + - + ); };