Merge pull request #5755 from TejasQ/tejask/4292
Core: Make error boundary more helpful
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Make `ErrorBoundary` display more helpful information about the error that
|
||||
occurred.
|
||||
|
||||
The `slackChannel` (optional) prop can now be passed as an object on the form
|
||||
`{ name: string; href?: string; }` in addition to the old string form. If you
|
||||
are using the error boundary like
|
||||
|
||||
```tsx
|
||||
<ErrorBoundary slackChannel="#support">
|
||||
<InnerComponent>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
you may like to migrate it to
|
||||
|
||||
```tsx
|
||||
const support = {
|
||||
name: '#support',
|
||||
href: 'https://slack.com/channels/your-channel',
|
||||
};
|
||||
|
||||
<ErrorBoundary slackChannel={support}>
|
||||
<InnerComponent>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
Also deprecated the prop `slackChannel` on `TabbedCard` and `InfoCard`, while
|
||||
adding the prop `errorBoundaryProps` to replace it.
|
||||
@@ -107,6 +107,7 @@ export const WarningPanel = (props: Props) => {
|
||||
<Accordion
|
||||
defaultExpanded={defaultExpanded ?? false}
|
||||
className={classes.panel}
|
||||
role="alert"
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIconStyled />}
|
||||
|
||||
@@ -17,7 +17,13 @@
|
||||
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { renderInTestApp, withLogCollector } from '@backstage/test-utils';
|
||||
import {
|
||||
MockErrorApi,
|
||||
renderInTestApp,
|
||||
withLogCollector,
|
||||
} from '@backstage/test-utils';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
import { errorApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
type BombProps = {
|
||||
shouldThrow?: boolean;
|
||||
@@ -35,28 +41,33 @@ const Bomb = ({ shouldThrow }: BombProps) => {
|
||||
describe('<ErrorBoundary/>', () => {
|
||||
it('should render error boundary with and without error', async () => {
|
||||
const { error } = await withLogCollector(['error'], async () => {
|
||||
const apis = ApiRegistry.with(errorApiRef, new MockErrorApi());
|
||||
const {
|
||||
rerender,
|
||||
queryByRole,
|
||||
getByRole,
|
||||
getByText,
|
||||
} = await renderInTestApp(
|
||||
<ErrorBoundary>
|
||||
<Bomb />
|
||||
</ErrorBoundary>,
|
||||
<ApiProvider apis={apis}>
|
||||
<ErrorBoundary>
|
||||
<Bomb />
|
||||
</ErrorBoundary>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(queryByRole('alert')).not.toBeInTheDocument();
|
||||
expect(getByText(/working component/i)).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ErrorBoundary>
|
||||
<Bomb shouldThrow />
|
||||
</ErrorBoundary>,
|
||||
<ApiProvider apis={apis}>
|
||||
<ErrorBoundary>
|
||||
<Bomb shouldThrow />
|
||||
</ErrorBoundary>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
expect(getByText(/something went wrong here/i)).toBeInTheDocument();
|
||||
expect(getByText(/something went wrong/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(error).toEqual([
|
||||
|
||||
@@ -15,9 +15,16 @@
|
||||
*/
|
||||
|
||||
import React, { ComponentClass, Component, ErrorInfo } from 'react';
|
||||
import { Button } from '../../components/Button';
|
||||
import { ErrorPanel } from '../../components/ErrorPanel';
|
||||
|
||||
type Props = {
|
||||
slackChannel?: string;
|
||||
type SlackChannel = {
|
||||
name: string;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
export type ErrorBoundaryProps = {
|
||||
slackChannel?: string | SlackChannel;
|
||||
onError?: (error: Error, errorInfo: string) => null;
|
||||
};
|
||||
|
||||
@@ -26,28 +33,30 @@ type State = {
|
||||
errorInfo?: ErrorInfo;
|
||||
};
|
||||
|
||||
type EProps = {
|
||||
error?: Error;
|
||||
slackChannel?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
const SlackLink = (props: { slackChannel?: string | SlackChannel }) => {
|
||||
const { slackChannel } = props;
|
||||
|
||||
if (!slackChannel) {
|
||||
return null;
|
||||
} else if (typeof slackChannel === 'string') {
|
||||
return <>Please contact {slackChannel} for help.</>;
|
||||
} else if (!slackChannel.href) {
|
||||
return <>Please contact {slackChannel.name} for help.</>;
|
||||
}
|
||||
|
||||
const Error = ({ slackChannel }: EProps) => {
|
||||
return (
|
||||
<div role="alert">
|
||||
Something went wrong here.{' '}
|
||||
{slackChannel && <>Please contact {slackChannel} for help.</>}
|
||||
</div>
|
||||
<Button to={slackChannel.href} variant="contained">
|
||||
{slackChannel.name}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorBoundary: ComponentClass<
|
||||
Props,
|
||||
ErrorBoundaryProps,
|
||||
State
|
||||
> = class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
> = class ErrorBoundary extends Component<ErrorBoundaryProps, State> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: undefined,
|
||||
errorInfo: undefined,
|
||||
@@ -61,13 +70,17 @@ export const ErrorBoundary: ComponentClass<
|
||||
}
|
||||
|
||||
render() {
|
||||
const { slackChannel } = this.props;
|
||||
const { error, errorInfo } = this.state;
|
||||
const { slackChannel, children } = this.props;
|
||||
const { error } = this.state;
|
||||
|
||||
if (!errorInfo) {
|
||||
return this.props.children;
|
||||
if (!error) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return <Error error={error} slackChannel={slackChannel} />;
|
||||
return (
|
||||
<ErrorPanel title="Something Went Wrong" error={error}>
|
||||
<SlackLink slackChannel={slackChannel} />
|
||||
</ErrorPanel>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { ErrorBoundaryProps } from './ErrorBoundary';
|
||||
export { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import classNames from 'classnames';
|
||||
import { ErrorBoundary } from '../ErrorBoundary';
|
||||
import { ErrorBoundary, ErrorBoundaryProps } from '../ErrorBoundary';
|
||||
import { BottomLink, BottomLinkProps } from '../BottomLink';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@@ -95,8 +95,8 @@ export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem';
|
||||
* You can custom style an InfoCard with the 'style' (outer container) and 'cardStyle' (inner container)
|
||||
* styles.
|
||||
*
|
||||
* The InfoCard serves as an error boundary. As a result, if you provide a 'slackChannel' property this
|
||||
* specifies the channel to display in the error component that is displayed if an error occurs
|
||||
* The InfoCard serves as an error boundary. As a result, if you provide an 'errorBoundaryProps' property this
|
||||
* specifies the extra information to display in the error component that is displayed if an error occurs
|
||||
* in any descendent components.
|
||||
*
|
||||
* By default the InfoCard has no custom layout of its children, but is treated as a block element. A
|
||||
@@ -112,7 +112,9 @@ type Props = {
|
||||
subheader?: ReactNode;
|
||||
divider?: boolean;
|
||||
deepLink?: BottomLinkProps;
|
||||
/** @deprecated Use errorBoundaryProps instead */
|
||||
slackChannel?: string;
|
||||
errorBoundaryProps?: ErrorBoundaryProps;
|
||||
variant?: InfoCardVariants;
|
||||
style?: object;
|
||||
cardStyle?: object;
|
||||
@@ -133,7 +135,8 @@ export const InfoCard = ({
|
||||
subheader,
|
||||
divider = true,
|
||||
deepLink,
|
||||
slackChannel = '#backstage',
|
||||
slackChannel,
|
||||
errorBoundaryProps,
|
||||
variant,
|
||||
children,
|
||||
headerStyle,
|
||||
@@ -169,9 +172,12 @@ export const InfoCard = ({
|
||||
});
|
||||
}
|
||||
|
||||
const errProps: ErrorBoundaryProps =
|
||||
errorBoundaryProps || (slackChannel ? { slackChannel } : {});
|
||||
|
||||
return (
|
||||
<Card style={calculatedStyle} className={className}>
|
||||
<ErrorBoundary slackChannel={slackChannel}>
|
||||
<ErrorBoundary {...errProps}>
|
||||
{title && (
|
||||
<CardHeader
|
||||
classes={{
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
TabProps,
|
||||
} from '@material-ui/core';
|
||||
import { BottomLink, BottomLinkProps } from '../BottomLink';
|
||||
import { ErrorBoundary } from '../ErrorBoundary';
|
||||
import { ErrorBoundary, ErrorBoundaryProps } from '../ErrorBoundary';
|
||||
|
||||
const useTabsStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@@ -52,7 +52,9 @@ const BoldHeader = withStyles(theme => ({
|
||||
}))(CardHeader);
|
||||
|
||||
type Props = {
|
||||
/** @deprecated Use errorBoundaryProps instead */
|
||||
slackChannel?: string;
|
||||
errorBoundaryProps?: ErrorBoundaryProps;
|
||||
children?: ReactElement<TabProps>[];
|
||||
onChange?: (event: React.ChangeEvent<{}>, value: number | string) => void;
|
||||
title?: string;
|
||||
@@ -61,7 +63,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const TabbedCard = ({
|
||||
slackChannel = '#backstage',
|
||||
slackChannel,
|
||||
errorBoundaryProps,
|
||||
children,
|
||||
title,
|
||||
deepLink,
|
||||
@@ -87,9 +90,12 @@ const TabbedCard = ({
|
||||
});
|
||||
}
|
||||
|
||||
const errProps: ErrorBoundaryProps =
|
||||
errorBoundaryProps || (slackChannel ? { slackChannel } : {});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<ErrorBoundary slackChannel={slackChannel}>
|
||||
<ErrorBoundary {...errProps}>
|
||||
{title && <BoldHeader title={title} />}
|
||||
<Tabs
|
||||
classes={tabsClasses}
|
||||
|
||||
Reference in New Issue
Block a user