use ErrorPanel and update cards to use errorBoundaryProps
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
Reference in New Issue
Block a user