Merge pull request #5755 from TejasQ/tejask/4292

Core: Make error boundary more helpful
This commit is contained in:
Fredrik Adelöw
2021-06-28 14:05:00 +02:00
committed by GitHub
7 changed files with 107 additions and 37 deletions
+32
View File
@@ -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}