mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
537f121c7d
commit
7f0f9ca7dd
19 changed files with 311 additions and 199 deletions
|
@ -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);
|
||||
|
|
|
@ -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)} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"@kbn/core-analytics-browser-mocks",
|
||||
"@kbn/core-analytics-browser",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/shared-ux-error-boundary",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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>
|
||||
));
|
||||
|
|
|
@ -14,5 +14,8 @@
|
|||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/shared-ux-error-boundary",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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', {
|
||||
|
|
24
packages/shared-ux/error_boundary/src/ui/throw_if_error.ts
Normal file
24
packages/shared-ux/error_boundary/src/ui/throw_if_error.ts
Normal 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;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/**/*"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue