mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
KibanaErrorBoundary initial implementation (#168754)
## Summary * Meta issue: https://github.com/elastic/kibana/issues/166584 * This PR implements tasks in: https://github.com/elastic/kibana/issues/167159 * [Technical doc [Elastic internal]](https://docs.google.com/document/d/1kVD3T08AzLuvRMnFrXzWd6rTQWZDFfjqmOMCoXRI-14/edit) This PR creates the `ErrorBoundary` component and its provider for services. It implements the wrapper around a few management apps owned by Appex-SharedUX. ### Screenshots Updated 2023-10-18 **Server upgrade scenario:** In this case, the caught error is known to be recoverable via window refresh: * <img width="1413" alt="image" src="7f34fbab
-0e67-4c67-a4a1-989464d5b0d0"> **Unknown/Custom error:** In this case, the error is something outside of known cases where the fix is to refresh: * <img width="1413" alt="image" src="7c39b5df
-d4da-4e33-aeca-9ea447010762"> ### Testing 1. Use a script proxy in between the browser and the Kibana server. * Try **https://github.com/tsullivan/simple-node-proxy** * or **https://chrome.google.com/webstore/detail/tweak-mock-and-modify-htt/feahianecghpnipmhphmfgmpdodhcapi**. 2. Script the proxy to send 404 responses for the Reporting plugin bundle, and for a bundle of some Management app. 3. Try the Share > CSV menu in Discover. It should be blocked, and handled with a toast message. Buttons in the toast should work. 4. Try the SharedUX management apps that use the wrapper. It should be blocked, and handled with an EuiCallout. Refresh button and EuiAccordion should work. ### Checklist - [x] Ensure the package code is delivered to the browser in the initial loading of the page (c2559e83d2
) - [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] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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> Co-authored-by: Tiago Costa <tiago.costa@elastic.co>
This commit is contained in:
parent
ce931e2820
commit
53c83e789b
36 changed files with 1474 additions and 427 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -681,6 +681,7 @@ packages/shared-ux/card/no_data/impl @elastic/appex-sharedux
|
|||
packages/shared-ux/card/no_data/mocks @elastic/appex-sharedux
|
||||
packages/shared-ux/card/no_data/types @elastic/appex-sharedux
|
||||
packages/shared-ux/chrome/navigation @elastic/appex-sharedux
|
||||
packages/shared-ux/error_boundary @elastic/appex-sharedux
|
||||
packages/shared-ux/file/context @elastic/appex-sharedux
|
||||
packages/shared-ux/file/image/impl @elastic/appex-sharedux
|
||||
packages/shared-ux/file/image/mocks @elastic/appex-sharedux
|
||||
|
|
|
@ -684,6 +684,7 @@
|
|||
"@kbn/shared-ux-card-no-data-mocks": "link:packages/shared-ux/card/no_data/mocks",
|
||||
"@kbn/shared-ux-card-no-data-types": "link:packages/shared-ux/card/no_data/types",
|
||||
"@kbn/shared-ux-chrome-navigation": "link:packages/shared-ux/chrome/navigation",
|
||||
"@kbn/shared-ux-error-boundary": "link:packages/shared-ux/error_boundary",
|
||||
"@kbn/shared-ux-file-context": "link:packages/shared-ux/file/context",
|
||||
"@kbn/shared-ux-file-image": "link:packages/shared-ux/file/image/impl",
|
||||
"@kbn/shared-ux-file-image-mocks": "link:packages/shared-ux/file/image/mocks",
|
||||
|
|
|
@ -51,7 +51,27 @@ Array [
|
|||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"children": <EuiErrorBoundary>
|
||||
"children": <KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>,
|
||||
},
|
||||
Object {},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": <KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
>
|
||||
|
@ -60,24 +80,8 @@ Array [
|
|||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</EuiErrorBoundary>,
|
||||
},
|
||||
Object {},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": <EuiErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</EuiErrorBoundary>,
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -110,7 +114,27 @@ Array [
|
|||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"children": <EuiErrorBoundary>
|
||||
"children": <KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>,
|
||||
},
|
||||
Object {},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": <KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
>
|
||||
|
@ -119,24 +143,8 @@ Array [
|
|||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</EuiErrorBoundary>,
|
||||
},
|
||||
Object {},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": <EuiErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</EuiErrorBoundary>,
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -30,7 +30,7 @@ pageLoadAssetSize:
|
|||
data: 454087
|
||||
dataViewEditor: 28082
|
||||
dataViewFieldEditor: 27000
|
||||
dataViewManagement: 5000
|
||||
dataViewManagement: 5100
|
||||
dataViews: 48300
|
||||
dataVisualizer: 27530
|
||||
devTools: 38637
|
||||
|
|
|
@ -32,6 +32,7 @@ webpack_cli(
|
|||
"//packages/kbn-safer-lodash-set",
|
||||
"//packages/kbn-peggy",
|
||||
"//packages/kbn-peggy-loader",
|
||||
"//packages/shared-ux/error_boundary",
|
||||
"//packages/kbn-rison",
|
||||
],
|
||||
output_dir = True,
|
||||
|
|
|
@ -91,6 +91,7 @@ const externals = {
|
|||
'@kbn/es-query': '__kbnSharedDeps__.KbnEsQuery',
|
||||
'@kbn/std': '__kbnSharedDeps__.KbnStd',
|
||||
'@kbn/safer-lodash-set': '__kbnSharedDeps__.SaferLodashSet',
|
||||
'@kbn/shared-ux-error-boundary': '__kbnSharedDeps__.KbnSharedUxErrorBoundary',
|
||||
'@kbn/rison': '__kbnSharedDeps__.KbnRison',
|
||||
history: '__kbnSharedDeps__.History',
|
||||
classnames: '__kbnSharedDeps__.Classnames',
|
||||
|
|
|
@ -66,6 +66,8 @@ export const KbnAnalytics = require('@kbn/analytics');
|
|||
export const KbnEsQuery = require('@kbn/es-query');
|
||||
export const KbnStd = require('@kbn/std');
|
||||
export const SaferLodashSet = require('@kbn/safer-lodash-set');
|
||||
|
||||
export const KbnSharedUxErrorBoundary = require('@kbn/shared-ux-error-boundary');
|
||||
export const KbnRison = require('@kbn/rison');
|
||||
export const History = require('history');
|
||||
export const Classnames = require('classnames');
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"@kbn/rison",
|
||||
"@kbn/std",
|
||||
"@kbn/safer-lodash-set",
|
||||
"@kbn/repo-info"
|
||||
"@kbn/repo-info",
|
||||
"@kbn/shared-ux-error-boundary"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
KibanaRootContextProvider,
|
||||
type KibanaRootContextProviderProps,
|
||||
} from '@kbn/react-kibana-context-root';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
|
||||
|
||||
/** Props for the KibanaContextProvider */
|
||||
export type KibanaRenderContextProviderProps = Omit<KibanaRootContextProviderProps, 'globalStyles'>;
|
||||
|
@ -27,7 +27,9 @@ export const KibanaRenderContextProvider: FC<KibanaRenderContextProviderProps> =
|
|||
}) => {
|
||||
return (
|
||||
<KibanaRootContextProvider globalStyles={false} {...props}>
|
||||
<EuiErrorBoundary>{children}</EuiErrorBoundary>
|
||||
<KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundary>{children}</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>
|
||||
</KibanaRootContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/shared-ux-error-boundary",
|
||||
]
|
||||
}
|
||||
|
|
35
packages/shared-ux/error_boundary/BUILD.bazel
Normal file
35
packages/shared-ux/error_boundary/BUILD.bazel
Normal file
|
@ -0,0 +1,35 @@
|
|||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
|
||||
SRCS = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
exclude = [
|
||||
"**/test_helpers.ts",
|
||||
"**/*.config.js",
|
||||
"**/*.mock.*",
|
||||
"**/*.test.*",
|
||||
"**/*.stories.*",
|
||||
"**/__snapshots__/**",
|
||||
"**/integration_tests/**",
|
||||
"**/mocks/**",
|
||||
"**/scripts/**",
|
||||
"**/storybook/**",
|
||||
"**/test_fixtures/**",
|
||||
"**/test_helpers/**",
|
||||
],
|
||||
)
|
||||
|
||||
BUNDLER_DEPS = [
|
||||
"@npm//react",
|
||||
"@npm//tslib",
|
||||
]
|
||||
|
||||
js_library(
|
||||
name = "error_boundary",
|
||||
package_name = "@kbn/shared-ux-error-boundary",
|
||||
srcs = ["package.json"] + SRCS,
|
||||
deps = BUNDLER_DEPS,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
16
packages/shared-ux/error_boundary/README.mdx
Normal file
16
packages/shared-ux/error_boundary/README.mdx
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
id: sharedUX/KibanaErrorBoundary
|
||||
slug: /shared-ux/error_boundary/kibana_error_boundary
|
||||
title: Kibana Error Boundary
|
||||
description: Container to catch errors thrown by child component
|
||||
tags: ['shared-ux', 'component', 'error', 'error_boundary']
|
||||
date: 2023-10-03
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
## API
|
||||
|
||||
## EUI Promotion Status
|
||||
|
||||
This component is specialized for error messages internal to Kibana and is not intended for promotion to EUI.
|
10
packages/shared-ux/error_boundary/index.ts
Normal file
10
packages/shared-ux/error_boundary/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { KibanaErrorBoundary } from './src/ui/error_boundary';
|
||||
export { KibanaErrorBoundaryProvider } from './src/services/error_boundary_services';
|
13
packages/shared-ux/error_boundary/jest.config.js
Normal file
13
packages/shared-ux/error_boundary/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/shared-ux/error_boundary'],
|
||||
};
|
5
packages/shared-ux/error_boundary/kibana.jsonc
Normal file
5
packages/shared-ux/error_boundary/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/shared-ux-error-boundary",
|
||||
"owner": "@elastic/appex-sharedux"
|
||||
}
|
12
packages/shared-ux/error_boundary/mocks/index.ts
Normal file
12
packages/shared-ux/error_boundary/mocks/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { BadComponent } from './src/bad_component';
|
||||
export { ChunkLoadErrorComponent } from './src/chunk_load_error_component';
|
||||
export { getServicesMock } from './src/jest';
|
||||
export { KibanaErrorBoundaryStorybookMock } from './src/storybook';
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { EuiButton } from '@elastic/eui';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const BadComponent = () => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
if (hasError) {
|
||||
throw new Error('This is an error to show the test user!'); // custom error
|
||||
}
|
||||
|
||||
const clickedForError = action('clicked for error');
|
||||
const handleClick = () => {
|
||||
clickedForError();
|
||||
setHasError(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiButton onClick={handleClick} data-test-subj="clickForErrorBtn">
|
||||
Click for error
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 { EuiButton } from '@elastic/eui';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const ChunkLoadErrorComponent = () => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
if (hasError) {
|
||||
const chunkError = new Error('Could not load chunk');
|
||||
chunkError.name = 'ChunkLoadError'; // specific error known to be recoverable with a click of a refresh button
|
||||
throw chunkError;
|
||||
}
|
||||
|
||||
const clickedForError = action('clicked for error');
|
||||
const handleClick = () => {
|
||||
clickedForError();
|
||||
setHasError(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiButton onClick={handleClick} fill={true} data-test-subj="clickForErrorBtn">
|
||||
Click for error
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
17
packages/shared-ux/error_boundary/mocks/src/jest.ts
Normal file
17
packages/shared-ux/error_boundary/mocks/src/jest.ts
Normal file
|
@ -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 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 { KibanaErrorService } from '../../src/services/error_service';
|
||||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
|
||||
export const getServicesMock = (): KibanaErrorBoundaryServices => {
|
||||
return {
|
||||
onClickRefresh: jest.fn().mockResolvedValue(undefined),
|
||||
errorService: new KibanaErrorService(),
|
||||
};
|
||||
};
|
43
packages/shared-ux/error_boundary/mocks/src/storybook.ts
Normal file
43
packages/shared-ux/error_boundary/mocks/src/storybook.ts
Normal file
|
@ -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 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 { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { KibanaErrorService } from '../../src/services/error_service';
|
||||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Params {}
|
||||
|
||||
export class KibanaErrorBoundaryStorybookMock extends AbstractStorybookMock<
|
||||
{},
|
||||
KibanaErrorBoundaryServices
|
||||
> {
|
||||
propArguments = {};
|
||||
|
||||
serviceArguments = {};
|
||||
|
||||
dependencies = [];
|
||||
|
||||
getServices(params: Params = {}): KibanaErrorBoundaryServices {
|
||||
const reloadWindowAction = action('Reload window');
|
||||
const onClickRefresh = () => {
|
||||
reloadWindowAction();
|
||||
};
|
||||
|
||||
return {
|
||||
...params,
|
||||
onClickRefresh,
|
||||
errorService: new KibanaErrorService(),
|
||||
};
|
||||
}
|
||||
|
||||
getProps(params: Params) {
|
||||
return params;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 React, { FC } from 'react';
|
||||
|
||||
import {
|
||||
EuiCollapsibleNavBeta,
|
||||
EuiHeader,
|
||||
EuiHeaderSection,
|
||||
EuiLink,
|
||||
EuiPageTemplate,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const Template: FC = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiHeader position="fixed">
|
||||
<EuiHeaderSection>
|
||||
<EuiCollapsibleNavBeta />
|
||||
</EuiHeaderSection>
|
||||
</EuiHeader>
|
||||
<EuiPageTemplate>
|
||||
<EuiPageTemplate.Header pageTitle="Welcome to my page" />
|
||||
<EuiPageTemplate.Section grow={true}>{children}</EuiPageTemplate.Section>
|
||||
<EuiPageTemplate.Section grow={false}>
|
||||
<EuiLink>Contact us</EuiLink>
|
||||
</EuiPageTemplate.Section>
|
||||
</EuiPageTemplate>
|
||||
</>
|
||||
);
|
||||
};
|
6
packages/shared-ux/error_boundary/package.json
Normal file
6
packages/shared-ux/error_boundary/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/shared-ux-error-boundary",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -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 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 React, { FC, useContext, useMemo } from 'react';
|
||||
|
||||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
import { KibanaErrorService } from './error_service';
|
||||
|
||||
const Context = React.createContext<KibanaErrorBoundaryServices | null>(null);
|
||||
|
||||
/**
|
||||
* A Context Provider for Jest and Storybooks
|
||||
*/
|
||||
export const KibanaErrorBoundaryDepsProvider: FC<KibanaErrorBoundaryServices> = ({
|
||||
children,
|
||||
onClickRefresh,
|
||||
errorService,
|
||||
}) => {
|
||||
return <Context.Provider value={{ onClickRefresh, errorService }}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Kibana-specific Provider that maps dependencies to services.
|
||||
*/
|
||||
export const KibanaErrorBoundaryProvider: FC = ({ children }) => {
|
||||
const value: KibanaErrorBoundaryServices = useMemo(
|
||||
() => ({
|
||||
onClickRefresh: () => window.location.reload(),
|
||||
errorService: new KibanaErrorService(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* React hook for accessing pre-wired services.
|
||||
*/
|
||||
export function useErrorBoundary(): KibanaErrorBoundaryServices {
|
||||
const context = useContext(Context);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'Kibana Error Boundary Context is missing. Ensure your component or React root is wrapped with Kibana Error Boundary Context.'
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { KibanaErrorService } from './error_service';
|
||||
|
||||
describe('KibanaErrorBoundary KibanaErrorService', () => {
|
||||
const service = new KibanaErrorService();
|
||||
|
||||
it('construction', () => {
|
||||
expect(service).toHaveProperty('registerError');
|
||||
});
|
||||
|
||||
it('decorates fatal error object', () => {
|
||||
const testFatal = new Error('This is an unrecognized and fatal error');
|
||||
const serviceError = service.registerError(testFatal, {});
|
||||
|
||||
expect(serviceError.isFatal).toBe(true);
|
||||
});
|
||||
|
||||
it('decorates recoverable error object', () => {
|
||||
const testRecoverable = new Error('Could not load chunk blah blah');
|
||||
testRecoverable.name = 'ChunkLoadError';
|
||||
const serviceError = service.registerError(testRecoverable, {});
|
||||
|
||||
expect(serviceError.isFatal).toBe(false);
|
||||
});
|
||||
|
||||
it('derives component name', () => {
|
||||
const testFatal = new Error('This is an unrecognized and fatal error');
|
||||
|
||||
const errorInfo = {
|
||||
componentStack: `
|
||||
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);
|
||||
|
||||
expect(serviceError.name).toBe('BadComponent');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
const MATCH_CHUNK_LOADERROR = /ChunkLoadError/;
|
||||
|
||||
interface ErrorServiceError {
|
||||
error: Error;
|
||||
errorInfo?: Partial<React.ErrorInfo> | null;
|
||||
name: string | null;
|
||||
isFatal: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kibana Error Boundary Services: Error Service
|
||||
* Each Error Boundary tracks an instance of this class
|
||||
* @internal
|
||||
*/
|
||||
export class KibanaErrorService {
|
||||
/**
|
||||
* Determines if the error fallback UI should appear as an apologetic but promising "Refresh" button,
|
||||
* or treated with "danger" coloring and include a detailed error message.
|
||||
*/
|
||||
private getIsFatal(error: Error) {
|
||||
const isChunkLoadError = MATCH_CHUNK_LOADERROR.test(error.name);
|
||||
return !isChunkLoadError; // "ChunkLoadError" is recoverable by refreshing the page
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the name of the component that threw the error
|
||||
*/
|
||||
private getErrorComponentName(errorInfo: Partial<React.ErrorInfo> | null) {
|
||||
let errorComponentName: string | null = null;
|
||||
const stackLines = errorInfo?.componentStack?.split('\n');
|
||||
const errorIndicator = /^ at (\S+).*/;
|
||||
|
||||
if (stackLines) {
|
||||
let i = 0;
|
||||
while (i < stackLines.length - 1) {
|
||||
// scan the stack trace text
|
||||
if (stackLines[i].match(errorIndicator)) {
|
||||
// extract the name of the bad component
|
||||
errorComponentName = stackLines[i].replace(errorIndicator, '$1');
|
||||
if (errorComponentName) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return errorComponentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a decorated error object
|
||||
* TODO: capture telemetry
|
||||
*/
|
||||
public registerError(
|
||||
error: Error,
|
||||
errorInfo: Partial<React.ErrorInfo> | null
|
||||
): ErrorServiceError {
|
||||
const isFatal = this.getIsFatal(error);
|
||||
const name = this.getErrorComponentName(errorInfo);
|
||||
|
||||
return {
|
||||
error,
|
||||
errorInfo,
|
||||
isFatal,
|
||||
name,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Template } from '../../mocks/src/storybook_template';
|
||||
import { BadComponent, KibanaErrorBoundaryStorybookMock } from '../../mocks';
|
||||
import { KibanaErrorBoundaryDepsProvider } from '../services/error_boundary_services';
|
||||
import { KibanaErrorBoundary } from './error_boundary';
|
||||
|
||||
import mdx from '../../README.mdx';
|
||||
|
||||
const storybookMock = new KibanaErrorBoundaryStorybookMock();
|
||||
|
||||
export default {
|
||||
title: 'Errors/Fatal Errors',
|
||||
description:
|
||||
'This is the Kibana Error Boundary. Use this to put a boundary around React components that may throw errors when rendering. It will intercept the error and determine if it is fatal or recoverable.',
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
export const ErrorInCallout: Story = () => {
|
||||
const services = storybookMock.getServices();
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<KibanaErrorBoundaryDepsProvider {...services}>
|
||||
<KibanaErrorBoundary>
|
||||
<BadComponent />
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryDepsProvider>
|
||||
</Template>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Template } from '../../mocks/src/storybook_template';
|
||||
import { ChunkLoadErrorComponent, KibanaErrorBoundaryStorybookMock } from '../../mocks';
|
||||
import { KibanaErrorBoundaryDepsProvider } from '../services/error_boundary_services';
|
||||
import { KibanaErrorBoundary } from './error_boundary';
|
||||
|
||||
import mdx from '../../README.mdx';
|
||||
|
||||
const storybookMock = new KibanaErrorBoundaryStorybookMock();
|
||||
|
||||
export default {
|
||||
title: 'Errors/Recoverable Errors',
|
||||
description:
|
||||
'This is the Kibana Error Boundary.' +
|
||||
' Use this to put a boundary around React components that may throw errors when rendering.' +
|
||||
' It will intercept the error and determine if it is fatal or recoverable.',
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
export const ErrorInCallout: Story = () => {
|
||||
const services = storybookMock.getServices();
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<KibanaErrorBoundaryDepsProvider {...services}>
|
||||
<KibanaErrorBoundary>
|
||||
<ChunkLoadErrorComponent />
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryDepsProvider>
|
||||
</Template>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { KibanaErrorBoundary } from '../..';
|
||||
import { BadComponent, ChunkLoadErrorComponent, getServicesMock } from '../../mocks';
|
||||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
import { errorMessageStrings as strings } from './message_strings';
|
||||
import { KibanaErrorBoundaryDepsProvider } from '../services/error_boundary_services';
|
||||
|
||||
describe('<KibanaErrorBoundary>', () => {
|
||||
let services: KibanaErrorBoundaryServices;
|
||||
beforeEach(() => {
|
||||
services = getServicesMock();
|
||||
});
|
||||
|
||||
const Template: FC = ({ children }) => {
|
||||
return (
|
||||
<KibanaErrorBoundaryDepsProvider {...services}>
|
||||
<KibanaErrorBoundary>{children}</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryDepsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
it('allow children to render when there is no error', () => {
|
||||
const inputText = 'Hello, beautiful world.';
|
||||
const res = render(<Template>{inputText}</Template>);
|
||||
expect(res.getByText(inputText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a "soft" callout when an unknown error is caught', async () => {
|
||||
const reloadSpy = jest.spyOn(services, 'onClickRefresh');
|
||||
|
||||
const { findByTestId, findByText } = render(
|
||||
<Template>
|
||||
<ChunkLoadErrorComponent />
|
||||
</Template>
|
||||
);
|
||||
(await findByTestId('clickForErrorBtn')).click();
|
||||
|
||||
expect(await findByText(strings.recoverable.callout.title())).toBeVisible();
|
||||
expect(await findByText(strings.recoverable.callout.pageReloadButton())).toBeVisible();
|
||||
|
||||
(await findByTestId('recoverablePromptReloadBtn')).click();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders a fatal callout when an unknown error is caught', async () => {
|
||||
const reloadSpy = jest.spyOn(services, 'onClickRefresh');
|
||||
|
||||
const { findByTestId, findByText } = render(
|
||||
<Template>
|
||||
<BadComponent />
|
||||
</Template>
|
||||
);
|
||||
(await findByTestId('clickForErrorBtn')).click();
|
||||
|
||||
expect(await findByText(strings.fatal.callout.title())).toBeVisible();
|
||||
expect(await findByText(strings.fatal.callout.body())).toBeVisible();
|
||||
expect(await findByText(strings.fatal.callout.showDetailsButton())).toBeVisible();
|
||||
expect(await findByText(strings.fatal.callout.pageReloadButton())).toBeVisible();
|
||||
|
||||
(await findByTestId('fatalPromptReloadBtn')).click();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
89
packages/shared-ux/error_boundary/src/ui/error_boundary.tsx
Normal file
89
packages/shared-ux/error_boundary/src/ui/error_boundary.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
import { useErrorBoundary } from '../services/error_boundary_services';
|
||||
import { FatalPrompt, RecoverablePrompt } from './message_components';
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
error: null | Error;
|
||||
errorInfo: null | Partial<React.ErrorInfo>;
|
||||
componentName: null | string;
|
||||
isFatal: null | boolean;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ServiceContext {
|
||||
services: KibanaErrorBoundaryServices;
|
||||
}
|
||||
|
||||
class ErrorBoundaryInternal extends React.Component<
|
||||
ErrorBoundaryProps & ServiceContext,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: ErrorBoundaryProps & ServiceContext) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
componentName: null,
|
||||
isFatal: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: Partial<React.ErrorInfo>) {
|
||||
const { name, isFatal } = this.props.services.errorService.registerError(error, errorInfo);
|
||||
this.setState(() => {
|
||||
return { error, errorInfo, componentName: name, isFatal };
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error != null) {
|
||||
const { error, errorInfo, componentName, isFatal } = this.state;
|
||||
|
||||
if (isFatal) {
|
||||
return (
|
||||
<FatalPrompt
|
||||
error={error}
|
||||
errorInfo={errorInfo}
|
||||
name={componentName}
|
||||
onClickRefresh={this.props.services.onClickRefresh}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RecoverablePrompt
|
||||
error={error}
|
||||
errorInfo={errorInfo}
|
||||
name={componentName}
|
||||
onClickRefresh={this.props.services.onClickRefresh}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// not in error state
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of Kibana Error Boundary
|
||||
* @param {ErrorBoundaryProps} props - ErrorBoundaryProps
|
||||
* @public
|
||||
*/
|
||||
export const KibanaErrorBoundary = (props: ErrorBoundaryProps) => {
|
||||
const services = useErrorBoundary();
|
||||
return <ErrorBoundaryInternal {...props} services={services} />;
|
||||
};
|
141
packages/shared-ux/error_boundary/src/ui/message_components.tsx
Normal file
141
packages/shared-ux/error_boundary/src/ui/message_components.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCodeBlock,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiLink,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiCopy,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { errorMessageStrings as strings } from './message_strings';
|
||||
|
||||
export interface ErrorCalloutProps {
|
||||
error: Error;
|
||||
errorInfo: Partial<React.ErrorInfo> | null;
|
||||
name: string | null;
|
||||
onClickRefresh: () => void;
|
||||
}
|
||||
|
||||
const CodePanel: React.FC<ErrorCalloutProps & { onClose: () => void }> = (props) => {
|
||||
const { error, errorInfo, name: errorComponentName, onClose } = props;
|
||||
const simpleFlyoutTitleId = useGeneratedHtmlId({
|
||||
prefix: 'simpleFlyoutTitle',
|
||||
});
|
||||
|
||||
const errorMessage = errorComponentName
|
||||
? strings.fatal.callout.details.componentName(errorComponentName)
|
||||
: error.message;
|
||||
const errorTrace = errorInfo?.componentStack ?? error.stack ?? error.toString();
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} aria-labelledby={simpleFlyoutTitleId} paddingSize="s">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{strings.fatal.callout.details.title()}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiCodeBlock>
|
||||
<p>{errorMessage}</p>
|
||||
<p>{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>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
export const FatalPrompt: React.FC<ErrorCalloutProps> = (props) => {
|
||||
const { onClickRefresh } = props;
|
||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>{strings.fatal.callout.title()}</h2>}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
body={
|
||||
<>
|
||||
<p>{strings.fatal.callout.body()}</p>
|
||||
<p>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
iconType="refresh"
|
||||
fill={true}
|
||||
onClick={onClickRefresh}
|
||||
data-test-subj="fatalPromptReloadBtn"
|
||||
>
|
||||
{strings.fatal.callout.pageReloadButton()}
|
||||
</EuiButton>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink color="danger" onClick={() => setIsFlyoutVisible(true)}>
|
||||
{strings.fatal.callout.showDetailsButton()}
|
||||
</EuiLink>
|
||||
{isFlyoutVisible ? (
|
||||
<CodePanel {...props} onClose={() => setIsFlyoutVisible(false)} />
|
||||
) : null}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RecoverablePrompt = (props: ErrorCalloutProps) => {
|
||||
const { onClickRefresh } = props;
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
title={<h2>{strings.recoverable.callout.title()}</h2>}
|
||||
body={<p>{strings.recoverable.callout.body()}</p>}
|
||||
color="warning"
|
||||
actions={
|
||||
<EuiButton
|
||||
color="warning"
|
||||
iconType="refresh"
|
||||
fill={true}
|
||||
onClick={onClickRefresh}
|
||||
data-test-subj="recoverablePromptReloadBtn"
|
||||
>
|
||||
{strings.recoverable.callout.pageReloadButton()}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
67
packages/shared-ux/error_boundary/src/ui/message_strings.ts
Normal file
67
packages/shared-ux/error_boundary/src/ui/message_strings.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const errorMessageStrings = {
|
||||
fatal: {
|
||||
callout: {
|
||||
title: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.title', {
|
||||
defaultMessage: 'Unable to load page',
|
||||
}),
|
||||
body: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.body', {
|
||||
defaultMessage: 'Try refreshing the page to resolve the issue.',
|
||||
}),
|
||||
showDetailsButton: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.detailButton', {
|
||||
defaultMessage: 'Show details',
|
||||
}),
|
||||
details: {
|
||||
title: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.details.title', {
|
||||
defaultMessage: 'Error details',
|
||||
}),
|
||||
componentName: (errorComponentName: string) =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.details', {
|
||||
defaultMessage: 'An error occurred in {name}:',
|
||||
values: { name: errorComponentName },
|
||||
}),
|
||||
closeButton: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.details.close', {
|
||||
defaultMessage: 'Close',
|
||||
}),
|
||||
copyToClipboardButton: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.details.copyToClipboard', {
|
||||
defaultMessage: 'Copy error to clipboard',
|
||||
}),
|
||||
},
|
||||
pageReloadButton: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.fatal.prompt.pageReloadButton', {
|
||||
defaultMessage: 'Refresh page',
|
||||
}),
|
||||
},
|
||||
},
|
||||
recoverable: {
|
||||
callout: {
|
||||
title: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.recoverable.prompt.title', {
|
||||
defaultMessage: 'Refresh the page',
|
||||
}),
|
||||
body: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.recoverable.prompt.body', {
|
||||
defaultMessage: 'A refresh fixes problems caused by upgrades or being offline.',
|
||||
}),
|
||||
pageReloadButton: () =>
|
||||
i18n.translate('sharedUXPackages.error_boundary.recoverable.prompt.pageReloadButton', {
|
||||
defaultMessage: 'Refresh page',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
25
packages/shared-ux/error_boundary/tsconfig.json
Normal file
25
packages/shared-ux/error_boundary/tsconfig.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"react",
|
||||
"@emotion/react/types/css-prop",
|
||||
"@testing-library/jest-dom",
|
||||
"@testing-library/react",
|
||||
"@kbn/ambient-ui-types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/shared-ux-storybook-mock",
|
||||
"@kbn/i18n",
|
||||
]
|
||||
}
|
18
packages/shared-ux/error_boundary/types.ts
Normal file
18
packages/shared-ux/error_boundary/types.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { KibanaErrorService } from './src/services/error_service';
|
||||
|
||||
/**
|
||||
* Services that are consumed internally in this component.
|
||||
* @internal
|
||||
*/
|
||||
export interface KibanaErrorBoundaryServices {
|
||||
onClickRefresh: () => void;
|
||||
errorService: KibanaErrorService;
|
||||
}
|
|
@ -1356,6 +1356,8 @@
|
|||
"@kbn/shared-ux-card-no-data-types/*": ["packages/shared-ux/card/no_data/types/*"],
|
||||
"@kbn/shared-ux-chrome-navigation": ["packages/shared-ux/chrome/navigation"],
|
||||
"@kbn/shared-ux-chrome-navigation/*": ["packages/shared-ux/chrome/navigation/*"],
|
||||
"@kbn/shared-ux-error-boundary": ["packages/shared-ux/error_boundary"],
|
||||
"@kbn/shared-ux-error-boundary/*": ["packages/shared-ux/error_boundary/*"],
|
||||
"@kbn/shared-ux-file-context": ["packages/shared-ux/file/context"],
|
||||
"@kbn/shared-ux-file-context/*": ["packages/shared-ux/file/context/*"],
|
||||
"@kbn/shared-ux-file-image": ["packages/shared-ux/file/image/impl"],
|
||||
|
|
|
@ -5616,6 +5616,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/shared-ux-error-boundary@link:packages/shared-ux/error_boundary":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/shared-ux-file-context@link:packages/shared-ux/file/context":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue