use ErrorPanel and update cards to use errorBoundaryProps

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2021-06-24 21:32:27 +02:00
parent 7dacc9b863
commit a3f258377c
8 changed files with 106 additions and 113 deletions
@@ -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,24 +41,29 @@ 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();
@@ -14,12 +14,17 @@
* limitations under the License.
*/
import { Button, CardContent, CardHeader } from '@material-ui/core';
import React, { ComponentClass, Component, ErrorInfo, useState } from 'react';
import { slackChannel as defaultSlackChannel } from '../constants';
import React, { ComponentClass, Component, ErrorInfo } from 'react';
import { Button } from '../../components/Button';
import { ErrorPanel } from '../../components/ErrorPanel';
type Props = {
slackChannel?: typeof defaultSlackChannel;
type SlackChannel = {
name: string;
href?: string;
};
export type ErrorBoundaryProps = {
slackChannel?: string | SlackChannel;
onError?: (error: Error, errorInfo: string) => null;
};
@@ -28,71 +33,30 @@ type State = {
errorInfo?: ErrorInfo;
};
type EProps = {
error?: Error;
slackChannel?: typeof defaultSlackChannel;
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, error }: EProps) => {
const [isShowingTrace, setIsShowingTrace] = useState(false);
return (
<div role="alert">
<CardHeader title="Something Went Wrong" />
<CardContent>
<p>
<strong>Error:</strong> {error?.message ?? 'No Further Info'}
</p>
<div>
<Button
onClick={() => setIsShowingTrace(!isShowingTrace)}
variant="contained"
color="primary"
>
{isShowingTrace ? 'Hide' : 'Show'} Details
</Button>
</div>
{isShowingTrace && (
<pre style={{ overflow: 'auto' }}>
<code>
{error?.stack?.split('\n').map(line => (
<>
{line}
<br />
</>
)) ?? ''}
</code>
</pre>
)}
{slackChannel && (
<p>
Please contact{' '}
{slackChannel.href ? (
<a
href={slackChannel.href}
target="_blank"
rel="noopener noreferrer"
>
<u>{slackChannel.name}</u>
</a>
) : (
slackChannel.name
)}{' '}
for help.
</p>
)}
</CardContent>
</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,
@@ -106,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,9 +26,8 @@ import {
makeStyles,
} from '@material-ui/core';
import classNames from 'classnames';
import { ErrorBoundary } from '../ErrorBoundary';
import { ErrorBoundary, ErrorBoundaryProps } from '../ErrorBoundary';
import { BottomLink, BottomLinkProps } from '../BottomLink';
import { slackChannel as defaultSlackChannel } from '../constants';
const useStyles = makeStyles(theme => ({
noPadding: {
@@ -96,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
@@ -113,7 +112,9 @@ type Props = {
subheader?: ReactNode;
divider?: boolean;
deepLink?: BottomLinkProps;
slackChannel?: typeof defaultSlackChannel;
/** @deprecated Use errorBoundaryProps instead */
slackChannel?: string;
errorBoundaryProps?: ErrorBoundaryProps;
variant?: InfoCardVariants;
style?: object;
cardStyle?: object;
@@ -134,7 +135,8 @@ export const InfoCard = ({
subheader,
divider = true,
deepLink,
slackChannel = defaultSlackChannel,
slackChannel,
errorBoundaryProps,
variant,
children,
headerStyle,
@@ -170,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,8 +32,7 @@ import {
TabProps,
} from '@material-ui/core';
import { BottomLink, BottomLinkProps } from '../BottomLink';
import { ErrorBoundary } from '../ErrorBoundary';
import { slackChannel as defaultSlackChannel } from '../constants';
import { ErrorBoundary, ErrorBoundaryProps } from '../ErrorBoundary';
const useTabsStyles = makeStyles(theme => ({
root: {
@@ -53,7 +52,9 @@ const BoldHeader = withStyles(theme => ({
}))(CardHeader);
type Props = {
slackChannel?: typeof defaultSlackChannel;
/** @deprecated Use errorBoundaryProps instead */
slackChannel?: string;
errorBoundaryProps?: ErrorBoundaryProps;
children?: ReactElement<TabProps>[];
onChange?: (event: React.ChangeEvent<{}>, value: number | string) => void;
title?: string;
@@ -62,7 +63,8 @@ type Props = {
};
const TabbedCard = ({
slackChannel = defaultSlackChannel,
slackChannel,
errorBoundaryProps,
children,
title,
deepLink,
@@ -88,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}
-25
View File
@@ -1,25 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
type SlackChannel = {
name: string;
href?: string;
};
export const slackChannel: SlackChannel = {
name: '#backstage',
href: 'https://slack.com/channels/your-channel',
};