Use KibanaErrorBoundary to handle errors loading main apps (#169324)

Meta issue: https://github.com/elastic/kibana/issues/166584

Technical doc [internal]:
https://docs.google.com/document/d/1kVD3T08AzLuvRMnFrXzWd6rTQWZDFfjqmOMCoXRI-14/edit?pli=1

## Summary

This PR updates high-level areas to catch 80% of the areas where an
offline browser would fail to load an application.

The changes made were in components where:

1. There was no React error handling, giving the "blank" screen
2. It was using `EuiErrorBoundary` (deprecated) and not the new UI we
want to use.

## Screenshot
In this scenario, the user loads Discover with server being online. Then
the server went offline. At that point, navigation attempts fail with
the new error message.


13c39305-358a-4a91-a2f4-650dec472be9

### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2023-10-26 08:12:10 -07:00 committed by GitHub
parent 537f121c7d
commit 7f0f9ca7dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 311 additions and 199 deletions

View file

@ -12,6 +12,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import { type AppMountParameters, AppStatus } from '@kbn/core-application-browser';
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
import { AppContainer } from './app_container';
import type { Mounter } from '../types';
import { createMemoryHistory } from 'history';
@ -222,20 +223,24 @@ describe('AppContainer', () => {
};
const wrapper = mountWithIntl(
<AppContainer
appPath={`/app/${appId}`}
appId={appId}
appStatus={AppStatus.accessible}
mounter={mounter}
setAppLeaveHandler={setAppLeaveHandler}
setAppActionMenu={setAppActionMenu}
setIsMounting={setIsMounting}
createScopedHistory={(appPath: string) =>
// Create a history using the appPath as the current location
new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath)
}
theme$={theme$}
/>
<KibanaErrorBoundaryProvider>
<KibanaErrorBoundary>
<AppContainer
appPath={`/app/${appId}`}
appId={appId}
appStatus={AppStatus.accessible}
mounter={mounter}
setAppLeaveHandler={setAppLeaveHandler}
setAppActionMenu={setAppActionMenu}
setIsMounting={setIsMounting}
createScopedHistory={(appPath: string) =>
// Create a history using the appPath as the current location
new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath)
}
theme$={theme$}
/>
</KibanaErrorBoundary>
</KibanaErrorBoundaryProvider>
);
expect(setIsMounting).toHaveBeenCalledTimes(1);

View file

@ -22,6 +22,7 @@ import {
type AppUnmount,
type ScopedHistory,
} from '@kbn/core-application-browser';
import { ThrowIfError } from '@kbn/shared-ux-error-boundary';
import type { Mounter } from '../types';
import { AppNotFound } from './app_not_found_screen';
@ -51,6 +52,7 @@ export const AppContainer: FC<Props> = ({
theme$,
showPlainSpinner,
}: Props) => {
const [error, setError] = useState<Error | null>(null);
const [showSpinner, setShowSpinner] = useState(true);
const [appNotFound, setAppNotFound] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
@ -87,14 +89,14 @@ export const AppContainer: FC<Props> = ({
setHeaderActionMenu: (menuMount) => setAppActionMenu(appId, menuMount),
})) || null;
} catch (e) {
// TODO: add error UI
setError(e);
// eslint-disable-next-line no-console
console.error(e);
} finally {
if (elementRef.current) {
setShowSpinner(false);
setIsMounting(false);
}
setIsMounting(false);
}
};
@ -115,6 +117,7 @@ export const AppContainer: FC<Props> = ({
return (
<Fragment>
<ThrowIfError error={error} />
{appNotFound && <AppNotFound />}
{showSpinner && !appNotFound && (
<AppLoadingPlaceholder showPlainSpinner={Boolean(showPlainSpinner)} />

View file

@ -16,6 +16,7 @@ import useObservable from 'react-use/lib/useObservable';
import type { CoreTheme } from '@kbn/core-theme-browser';
import type { MountPoint } from '@kbn/core-mount-utils-browser';
import { type AppLeaveHandler, AppStatus } from '@kbn/core-application-browser';
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
import type { Mounter } from '../types';
import { AppContainer } from './app_container';
import { CoreScopedHistory } from '../scoped_history';
@ -54,61 +55,65 @@ export const AppRouter: FunctionComponent<Props> = ({
const showPlainSpinner = useObservable(hasCustomBranding$ ?? EMPTY, false);
return (
<Router history={history}>
<Routes>
{[...mounters].map(([appId, mounter]) => (
<Route
key={mounter.appRoute}
path={mounter.appRoute}
exact={mounter.exactRoute}
render={({ match: { path } }) => (
<AppContainer
appPath={path}
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
createScopedHistory={createScopedHistory}
{...{
appId,
mounter,
setAppLeaveHandler,
setAppActionMenu,
setIsMounting,
theme$,
showPlainSpinner,
}}
<KibanaErrorBoundaryProvider>
<KibanaErrorBoundary>
<Router history={history}>
<Routes>
{[...mounters].map(([appId, mounter]) => (
<Route
key={mounter.appRoute}
path={mounter.appRoute}
exact={mounter.exactRoute}
render={({ match: { path } }) => (
<AppContainer
appPath={path}
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
createScopedHistory={createScopedHistory}
{...{
appId,
mounter,
setAppLeaveHandler,
setAppActionMenu,
setIsMounting,
theme$,
showPlainSpinner,
}}
/>
)}
/>
)}
/>
))}
{/* handler for legacy apps and used as a catch-all to display 404 page on not existing /app/appId apps*/}
<Route
path="/app/:appId"
render={({
match: {
params: { appId },
url,
},
}: RouteComponentProps<Params>) => {
// the id/mounter retrieval can be removed once #76348 is addressed
const [id, mounter] = mounters.has(appId) ? [appId, mounters.get(appId)] : [];
return (
<AppContainer
appPath={url}
appId={id ?? appId}
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
createScopedHistory={createScopedHistory}
{...{
mounter,
setAppLeaveHandler,
setAppActionMenu,
setIsMounting,
theme$,
showPlainSpinner,
}}
/>
);
}}
/>
</Routes>
</Router>
))}
{/* handler for legacy apps and used as a catch-all to display 404 page on not existing /app/appId apps*/}
<Route
path="/app/:appId"
render={({
match: {
params: { appId },
url,
},
}: RouteComponentProps<Params>) => {
// the id/mounter retrieval can be removed once #76348 is addressed
const [id, mounter] = mounters.has(appId) ? [appId, mounters.get(appId)] : [];
return (
<AppContainer
appPath={url}
appId={id ?? appId}
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
createScopedHistory={createScopedHistory}
{...{
mounter,
setAppLeaveHandler,
setAppActionMenu,
setIsMounting,
theme$,
showPlainSpinner,
}}
/>
);
}}
/>
</Routes>
</Router>
</KibanaErrorBoundary>
</KibanaErrorBoundaryProvider>
);
};

View file

@ -36,6 +36,7 @@
"@kbn/core-analytics-browser-mocks",
"@kbn/core-analytics-browser",
"@kbn/shared-ux-router",
"@kbn/shared-ux-error-boundary",
],
"exclude": [
"target/**/*",

View file

@ -7,7 +7,7 @@
*/
import React, { Suspense, ComponentType, ReactElement, Ref } from 'react';
import { EuiErrorBoundary } from '@elastic/eui';
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
import { Fallback } from './fallback';
@ -21,9 +21,11 @@ export const withSuspense = <P extends {}, R = {}>(
fallback: ReactElement | null = <Fallback />
) =>
React.forwardRef((props: P, ref: Ref<R>) => (
<EuiErrorBoundary>
<Suspense fallback={fallback}>
<Component {...props} ref={ref} />
</Suspense>
</EuiErrorBoundary>
<KibanaErrorBoundaryProvider>
<KibanaErrorBoundary>
<Suspense fallback={fallback}>
<Component {...props} ref={ref} />
</Suspense>
</KibanaErrorBoundary>
</KibanaErrorBoundaryProvider>
));

View file

@ -14,5 +14,8 @@
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/shared-ux-error-boundary",
]
}

View file

@ -8,3 +8,4 @@
export { KibanaErrorBoundary } from './src/ui/error_boundary';
export { KibanaErrorBoundaryProvider } from './src/services/error_boundary_services';
export { ThrowIfError } from './src/ui/throw_if_error';

View file

@ -49,4 +49,26 @@ describe('KibanaErrorBoundary KibanaErrorService', () => {
expect(serviceError.name).toBe('BadComponent');
});
it('passes the common helper utility when deriving component name', () => {
const testFatal = new Error('This is an mind-bendingly fatal error');
const errorInfo = {
componentStack: `
at ThrowIfError (http://localhost:9001/main.iframe.bundle.js:11616:73)
at BadComponent (http://localhost:9001/main.iframe.bundle.js:11616:73)
at ErrorBoundaryInternal (http://localhost:9001/main.iframe.bundle.js:12232:81)
at KibanaErrorBoundary (http://localhost:9001/main.iframe.bundle.js:12295:116)
at KibanaErrorBoundaryDepsProvider (http://localhost:9001/main.iframe.bundle.js:11879:23)
at div
at http://localhost:9001/kbn-ui-shared-deps-npm.dll.js:164499:73
at section
at http://localhost:9001/kbn-ui-shared-deps-npm.dll.js`,
};
const serviceError = service.registerError(testFatal, errorInfo);
// should not be "ThrowIfError"
expect(serviceError.name).toBe('BadComponent');
});
});

View file

@ -7,6 +7,7 @@
*/
import React from 'react';
import { ThrowIfError } from '../..';
const MATCH_CHUNK_LOADERROR = /ChunkLoadError/;
@ -47,7 +48,8 @@ export class KibanaErrorService {
if (stackLines[i].match(errorIndicator)) {
// extract the name of the bad component
errorComponentName = stackLines[i].replace(errorIndicator, '$1');
if (errorComponentName) {
// If the component is the utility for throwing errors, skip
if (errorComponentName && errorComponentName !== ThrowIfError.name) {
break;
}
}

View file

@ -23,6 +23,7 @@ import {
EuiFlexItem,
EuiButtonEmpty,
EuiCopy,
EuiPanel,
} from '@elastic/eui';
import { errorMessageStrings as strings } from './message_strings';
@ -40,41 +41,47 @@ const CodePanel: React.FC<ErrorCalloutProps & { onClose: () => void }> = (props)
prefix: 'simpleFlyoutTitle',
});
const errorMessage = errorComponentName
? strings.fatal.callout.details.componentName(errorComponentName)
: error.message;
const errorName =
errorComponentName && strings.fatal.callout.details.componentName(errorComponentName);
const errorTrace = errorInfo?.componentStack ?? error.stack ?? error.toString();
return (
<EuiFlyout onClose={onClose} aria-labelledby={simpleFlyoutTitleId} paddingSize="s">
<EuiFlyout onClose={onClose} aria-labelledby={simpleFlyoutTitleId} paddingSize="none">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>{strings.fatal.callout.details.title()}</h2>
</EuiTitle>
<EuiPanel paddingSize="m" hasBorder={false} hasShadow={false}>
<EuiTitle size="m">
<h2>{strings.fatal.callout.details.title()}</h2>
</EuiTitle>
</EuiPanel>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiCodeBlock>
<p>{errorMessage}</p>
<p>{errorTrace}</p>
<p>{(error.stack ?? error.toString()) + '\n\n'}</p>
<p>
{errorName}
{errorTrace}
</p>
</EuiCodeBlock>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClose} flush="left">
{strings.fatal.callout.details.closeButton()}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={errorMessage + '\n\n' + errorTrace}>
{(copy) => (
<EuiButton onClick={copy} fill iconType="copyClipboard">
{strings.fatal.callout.details.copyToClipboardButton()}
</EuiButton>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
<EuiPanel paddingSize="m" hasBorder={false} hasShadow={false}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClose} flush="left">
{strings.fatal.callout.details.closeButton()}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={errorName + '\n\n' + errorTrace}>
{(copy) => (
<EuiButton onClick={copy} fill iconType="copyClipboard">
{strings.fatal.callout.details.copyToClipboardButton()}
</EuiButton>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlyoutFooter>
</EuiFlyout>
);

View file

@ -30,7 +30,7 @@ export const errorMessageStrings = {
}),
componentName: (errorComponentName: string) =>
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.details', {
defaultMessage: 'An error occurred in {name}:',
defaultMessage: 'The above error occurred in {name}:',
values: { name: errorComponentName },
}),
closeButton: () =>
@ -56,7 +56,7 @@ export const errorMessageStrings = {
}),
body: () =>
i18n.translate('sharedUXPackages.error_boundary.recoverable.prompt.body', {
defaultMessage: 'A refresh fixes problems caused by upgrades or being offline.',
defaultMessage: 'This should resolve any issues loading the page.',
}),
pageReloadButton: () =>
i18n.translate('sharedUXPackages.error_boundary.recoverable.prompt.pageReloadButton', {

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FC } from 'react';
/**
* This component allows errors to be caught outside of a render tree, and re-thrown within a render tree
* wrapped by KibanaErrorBoundary. The purpose is to let KibanaErrorBoundary control the user experience when
* React can not render due to an error.
*
* @public
*/
export const ThrowIfError: FC<{ error: Error | null }> = ({ error }) => {
if (error) {
throw error;
}
return null;
};

View file

@ -10,6 +10,7 @@ import React, { memo } from 'react';
import { Redirect } from 'react-router-dom';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from '@kbn/core/public';
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
import { ManagementAppWrapper } from '../management_app_wrapper';
import { ManagementLandingPage } from '../landing';
import { ManagementSection } from '../../utils';
@ -25,41 +26,48 @@ interface ManagementRouterProps {
export const ManagementRouter = memo(
({ history, setBreadcrumbs, onAppMounted, sections, theme$ }: ManagementRouterProps) => {
return (
<Router history={history}>
<Routes>
{sections.map((section) =>
section
.getAppsEnabled()
.map((app) => (
<Route
path={`${app.basePath}`}
component={() => (
<ManagementAppWrapper
app={app}
setBreadcrumbs={setBreadcrumbs}
onAppMounted={onAppMounted}
history={history}
theme$={theme$}
<KibanaErrorBoundaryProvider>
<KibanaErrorBoundary>
<Router history={history}>
<Routes>
{sections.map((section) =>
section
.getAppsEnabled()
.map((app) => (
<Route
path={`${app.basePath}`}
component={() => (
<ManagementAppWrapper
app={app}
setBreadcrumbs={setBreadcrumbs}
onAppMounted={onAppMounted}
history={history}
theme$={theme$}
/>
)}
/>
)}
/>
))
)}
{sections.map((section) =>
section
.getAppsEnabled()
.filter((app) => app.redirectFrom)
.map((app) => <Redirect path={`/${app.redirectFrom}*`} to={`${app.basePath}*`} />)
)}
))
)}
{sections.map((section) =>
section
.getAppsEnabled()
.filter((app) => app.redirectFrom)
.map((app) => <Redirect path={`/${app.redirectFrom}*`} to={`${app.basePath}*`} />)
)}
<Route
path={'/'}
component={() => (
<ManagementLandingPage setBreadcrumbs={setBreadcrumbs} onAppMounted={onAppMounted} />
)}
/>
</Routes>
</Router>
<Route
path={'/'}
component={() => (
<ManagementLandingPage
setBreadcrumbs={setBreadcrumbs}
onAppMounted={onAppMounted}
/>
)}
/>
</Routes>
</Router>
</KibanaErrorBoundary>
</KibanaErrorBoundaryProvider>
);
}
);

View file

@ -11,6 +11,7 @@ import React, { createRef, Component } from 'react';
import { ChromeBreadcrumb, AppMountParameters, ScopedHistory } from '@kbn/core/public';
import classNames from 'classnames';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { ThrowIfError } from '@kbn/shared-ux-error-boundary';
import { ManagementApp } from '../../utils';
import { Unmount } from '../../types';
@ -22,10 +23,22 @@ interface ManagementSectionWrapperProps {
theme$: AppMountParameters['theme$'];
}
export class ManagementAppWrapper extends Component<ManagementSectionWrapperProps> {
interface ManagementSectionWrapperState {
error: Error | null;
}
export class ManagementAppWrapper extends Component<
ManagementSectionWrapperProps,
ManagementSectionWrapperState
> {
private unmount?: Unmount;
private mountElementRef = createRef<HTMLDivElement>();
constructor(props: ManagementSectionWrapperProps) {
super(props);
this.state = { error: null };
}
componentDidMount() {
const { setBreadcrumbs, app, onAppMounted, history, theme$ } = this.props;
const { mount, basePath } = app;
@ -42,9 +55,15 @@ export class ManagementAppWrapper extends Component<ManagementSectionWrapperProp
onAppMounted(app.id);
if (mountResult instanceof Promise) {
mountResult.then((um) => {
this.unmount = um;
});
mountResult
.then((um) => {
this.unmount = um;
})
.catch((error) => {
this.setState(() => ({
error,
}));
});
} else {
this.unmount = mountResult;
}
@ -58,11 +77,14 @@ export class ManagementAppWrapper extends Component<ManagementSectionWrapperProp
render() {
return (
<div
// The following classes are a stop-gap for this element that wraps children of KibanaPageTemplate
className={classNames('euiPageContentBody', APP_WRAPPER_CLASS)}
ref={this.mountElementRef}
/>
<>
<ThrowIfError error={this.state.error} />
<div
// The following classes are a stop-gap for this element that wraps children of KibanaPageTemplate
className={classNames('euiPageContentBody', APP_WRAPPER_CLASS)}
ref={this.mountElementRef}
/>
</>
);
}
}

View file

@ -27,7 +27,8 @@
"@kbn/serverless",
"@kbn/management-settings-application",
"@kbn/react-kibana-context-render",
"@kbn/shared-ux-utility"
"@kbn/shared-ux-utility",
"@kbn/shared-ux-error-boundary"
],
"exclude": [
"target/**/*"

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiSideNavItemType, EuiPageSectionProps, EuiErrorBoundary } from '@elastic/eui';
import { EuiSideNavItemType, EuiPageSectionProps } from '@elastic/eui';
import { _EuiPageBottomBarProps } from '@elastic/eui/src/components/page_template/bottom_bar/page_bottom_bar';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
@ -14,6 +14,7 @@ import useObservable from 'react-use/lib/useObservable';
import type { BehaviorSubject, Observable } from 'rxjs';
import type { ApplicationStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
import {
KibanaPageTemplate,
KibanaPageTemplateKibanaProvider,
@ -208,15 +209,17 @@ export function ObservabilityPageTemplate({
: undefined
}
>
<EuiErrorBoundary>
<KibanaPageTemplate.Section
component="div"
alignment={pageTemplateProps.isEmptyState ? 'center' : 'top'}
{...pageSectionProps}
>
{children}
</KibanaPageTemplate.Section>
</EuiErrorBoundary>
<KibanaErrorBoundaryProvider>
<KibanaErrorBoundary>
<KibanaPageTemplate.Section
component="div"
alignment={pageTemplateProps.isEmptyState ? 'center' : 'top'}
{...pageSectionProps}
>
{children}
</KibanaPageTemplate.Section>
</KibanaErrorBoundary>
</KibanaErrorBoundaryProvider>
{bottomBar && (
<KibanaPageTemplate.BottomBar {...bottomBarProps}>
{bottomBar}

View file

@ -36,7 +36,8 @@
"@kbn/profiling-utils",
"@kbn/advanced-settings-plugin",
"@kbn/utility-types",
"@kbn/share-plugin"
"@kbn/share-plugin",
"@kbn/shared-ux-error-boundary"
],
"exclude": ["target/**/*"]
}

View file

@ -11,13 +11,12 @@ import React, { memo } from 'react';
import type { Store, Action } from 'redux';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { EuiErrorBoundary } from '@elastic/eui';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { AppLeaveHandler, AppMountParameters } from '@kbn/core/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { CellActionsProvider } from '@kbn/cell-actions';
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import { UpsellingProvider } from '../common/components/upselling_provider';
import { ManageUserInfo } from '../detections/components/user_info';
@ -60,43 +59,45 @@ const StartAppComponent: FC<StartAppComponent> = ({
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
return (
<EuiErrorBoundary>
<i18n.Context>
<ManageGlobalToaster>
<ReduxStoreProvider store={store}>
<KibanaThemeProvider theme$={theme$}>
<EuiThemeProvider darkMode={darkMode}>
<MlCapabilitiesProvider>
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<ManageUserInfo>
<NavigationProvider core={services}>
<ReactQueryClientProvider>
<CellActionsProvider
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
>
<UpsellingProvider upsellingService={upselling}>
<DiscoverInTimelineContextProvider>
<AssistantProvider>
<PageRouter history={history} onAppLeave={onAppLeave}>
{children}
</PageRouter>
</AssistantProvider>
</DiscoverInTimelineContextProvider>
</UpsellingProvider>
</CellActionsProvider>
</ReactQueryClientProvider>
</NavigationProvider>
</ManageUserInfo>
</UserPrivilegesProvider>
</MlCapabilitiesProvider>
</EuiThemeProvider>
</KibanaThemeProvider>
<ErrorToastDispatcher />
<GlobalToaster />
</ReduxStoreProvider>
</ManageGlobalToaster>
</i18n.Context>
</EuiErrorBoundary>
<KibanaErrorBoundaryProvider>
<KibanaErrorBoundary>
<i18n.Context>
<ManageGlobalToaster>
<ReduxStoreProvider store={store}>
<KibanaThemeProvider theme$={theme$}>
<EuiThemeProvider darkMode={darkMode}>
<MlCapabilitiesProvider>
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<ManageUserInfo>
<NavigationProvider core={services}>
<ReactQueryClientProvider>
<CellActionsProvider
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
>
<UpsellingProvider upsellingService={upselling}>
<DiscoverInTimelineContextProvider>
<AssistantProvider>
<PageRouter history={history} onAppLeave={onAppLeave}>
{children}
</PageRouter>
</AssistantProvider>
</DiscoverInTimelineContextProvider>
</UpsellingProvider>
</CellActionsProvider>
</ReactQueryClientProvider>
</NavigationProvider>
</ManageUserInfo>
</UserPrivilegesProvider>
</MlCapabilitiesProvider>
</EuiThemeProvider>
</KibanaThemeProvider>
<ErrorToastDispatcher />
<GlobalToaster />
</ReduxStoreProvider>
</ManageGlobalToaster>
</i18n.Context>
</KibanaErrorBoundary>
</KibanaErrorBoundaryProvider>
);
};

View file

@ -178,6 +178,7 @@
"@kbn/openapi-generator",
"@kbn/es",
"@kbn/react-kibana-mount",
"@kbn/unified-doc-viewer-plugin"
"@kbn/unified-doc-viewer-plugin",
"@kbn/shared-ux-error-boundary"
]
}