Merge pull request #1824 from OHIF/feat/ohif-125

OHIF-125: Error boundaries
This commit is contained in:
Danny Brown 2020-06-26 16:47:52 -04:00 committed by GitHub
commit aa402183f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 239 additions and 47 deletions

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SidePanel } from '@ohif/ui'; import { SidePanel, ErrorBoundary } from '@ohif/ui';
import Header from './Header.jsx'; import Header from './Header.jsx';
import NestedMenu from './ToolbarButtonNestedMenu.jsx'; import NestedMenu from './ToolbarButtonNestedMenu.jsx';
@ -93,27 +92,28 @@ function ViewerLayout({
return ( return (
<div> <div>
<Header> <Header>
<div className="relative flex justify-center"> <ErrorBoundary context="Primary Toolbar">
{toolbars.primary.map(toolDef => { <div className="relative flex justify-center">
const isNested = Array.isArray(toolDef); {toolbars.primary.map(toolDef => {
const isNested = Array.isArray(toolDef);
if (!isNested) { if (!isNested) {
const { id, Component, componentProps } = toolDef; const { id, Component, componentProps } = toolDef;
return <Component key={id} id={id} {...componentProps} />; return <Component key={id} id={id} {...componentProps} />;
} else { } else {
return ( return (
<NestedMenu isActive={activeTool.isActive} icon={activeTool.icon} label={activeTool.label}> <NestedMenu isActive={activeTool.isActive} icon={activeTool.icon} label={activeTool.label}>
<div className="flex"> <div className="flex">
{toolDef.map(x => { {toolDef.map(x => {
const { id, Component, componentProps } = x; const { id, Component, componentProps } = x;
return <Component key={id} id={id} {...componentProps} />; return <Component key={id} id={id} {...componentProps} />;
})} })}
</div> </div>
</NestedMenu> </NestedMenu>
); );
} }
})} })}
</div> </div>
</ErrorBoundary>
</Header> </Header>
<div <div
className="flex flex-row flex-no-wrap items-stretch w-full overflow-hidden" className="flex flex-row flex-no-wrap items-stretch w-full overflow-hidden"
@ -121,36 +121,43 @@ function ViewerLayout({
> >
{/* LEFT SIDEPANELS */} {/* LEFT SIDEPANELS */}
{leftPanelComponents.length && ( {leftPanelComponents.length && (
<SidePanel <ErrorBoundary context="Left Panel">
side="left" <SidePanel
defaultComponentOpen={leftPanelComponents[0].name} side="left"
childComponents={leftPanelComponents} defaultComponentOpen={leftPanelComponents[0].name}
/> childComponents={leftPanelComponents}
/>
</ErrorBoundary>
)} )}
{/* TOOLBAR + GRID */} {/* TOOLBAR + GRID */}
<div className="flex flex-col flex-1 h-full"> <div className="flex flex-col flex-1 h-full">
<div className="flex h-12 border-b border-transparent flex-2 w-100"> <div className="flex h-12 border-b border-transparent flex-2 w-100">
<div className="flex items-center w-full px-3 bg-primary-dark"> <ErrorBoundary context="Secondary Toolbar">
{toolbars.secondary.map(toolDef => { <div className="flex items-center w-full px-3 bg-primary-dark">
const { id, Component, componentProps } = toolDef; {toolbars.secondary.map(toolDef => {
const { id, Component, componentProps } = toolDef;
return <Component key={id} id={id} {...componentProps} />; return <Component key={id} id={id} {...componentProps} />;
})} })}
</div> </div>
</ErrorBoundary>
</div> </div>
<div className="flex items-center justify-center flex-1 h-full pt-1 pb-2 overflow-hidden bg-black"> <div className="flex items-center justify-center flex-1 h-full pt-1 pb-2 overflow-hidden bg-black">
<ViewportGridComp <ErrorBoundary context="Grid">
servicesManager={servicesManager} <ViewportGridComp
viewportComponents={viewportComponents} servicesManager={servicesManager}
/> viewportComponents={viewportComponents}
/>
</ErrorBoundary>
</div> </div>
</div> </div>
{rightPanelComponents.length && ( {rightPanelComponents.length && (
<SidePanel <ErrorBoundary context="Right Panel">
side="right" <SidePanel
defaultComponentOpen={rightPanelComponents[0].name} side="right"
childComponents={rightPanelComponents} defaultComponentOpen={rightPanelComponents[0].name}
/> childComponents={rightPanelComponents}
/>
</ErrorBoundary>
)} )}
</div> </div>
</div> </div>

View File

@ -33,6 +33,7 @@ export {
Dialog, Dialog,
Dropdown, Dropdown,
EmptyStudies, EmptyStudies,
ErrorBoundary,
ExpandableToolbarButton, ExpandableToolbarButton,
ListMenu, ListMenu,
Icon, Icon,

View File

@ -42,6 +42,7 @@
"react-dnd": "^10.0.2", "react-dnd": "^10.0.2",
"react-dnd-html5-backend": "^10.0.2", "react-dnd-html5-backend": "^10.0.2",
"react-dnd-touch-backend": "^10.0.2", "react-dnd-touch-backend": "^10.0.2",
"react-error-boundary": "2.2.x",
"react-dom": "16.11.0", "react-dom": "16.11.0",
"react-modal": "^3.11.2", "react-modal": "^3.11.2",
"react-powerplug": "1.0.0", "react-powerplug": "1.0.0",

View File

@ -0,0 +1,109 @@
import React, { useState } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import PropTypes from 'prop-types';
import Modal from '../Modal';
const isProduction = process.env.NODE_ENV === 'production';
const DefaultFallback = ({ error, componentStack, context, resetErrorBoundary, fallbackRoute }) => {
const title = `Something went wrong${!isProduction && ` in ${context}`}.`;
const subtitle = `Sorry, something went wrong there. Try again.`;
return (
<div className="ErrorFallback bg-primary-dark w-full h-full" role="alert">
<p className="text-primary-light text-xl">{title}</p>
<p className="text-primary-light text-base">{subtitle}</p>
{!isProduction && (
<div className="rounded-md bg-secondary-dark p-5 mt-5">
<pre className="text-primary-light">Context: {context}</pre>
<pre className="text-primary-light">Error Message: {error.message}</pre>
<pre className="text-primary-light">Stack: {componentStack}</pre>
</div>
)}
</div>
);
};
const noop = () => { };
DefaultFallback.propTypes = {
error: PropTypes.object.isRequired,
resetErrorBoundary: PropTypes.func,
componentStack: PropTypes.string,
};
DefaultFallback.defaultProps = {
resetErrorBoundary: noop
};
const ErrorBoundary = ({
context,
onReset,
onError,
fallbackComponent: FallbackComponent,
children,
fallbackRoute,
isPage
}) => {
const [isOpen, setIsOpen] = useState(true);
const onErrorHandler = (error, componentStack) => {
console.error(`${context} Error Boundary`, error, componentStack, context);
onError(error, componentStack, context);
};
const onResetHandler = (...args) => onReset(...args);
const withModal = (Component) => props => (
<Modal
closeButton
shouldCloseOnEsc
isOpen={isOpen}
title={'Something went wrong'}
onClose={() => {
setIsOpen(false);
if (fallbackRoute) {
window.location = fallbackRoute;
}
}}
>
<Component {...props} />
</Modal>
);
const Fallback = isPage ? FallbackComponent : withModal(FallbackComponent);
return (
<ReactErrorBoundary
fallbackRender={props => (
<Fallback
{...props}
context={context}
fallbackRoute={fallbackRoute}
/>
)}
onReset={onResetHandler}
onError={onErrorHandler}
>
{children}
</ReactErrorBoundary>
);
};
ErrorBoundary.propTypes = {
context: PropTypes.string,
onReset: PropTypes.func,
onError: PropTypes.func,
fallbackComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
children: PropTypes.node.isRequired,
fallbackRoute: PropTypes.string
};
ErrorBoundary.defaultProps = {
context: 'OHIF',
onReset: noop,
onError: noop,
fallbackComponent: DefaultFallback,
fallbackRoute: null
};
export default ErrorBoundary;

View File

@ -0,0 +1,59 @@
---
name: Error Boundary
menu: Data Display
route: components/errorBoundary
---
import { Playground, Props } from 'docz';
import { ErrorBoundary } from '@ohif/ui';
# Error Boundary
This component can be used to display a message or handle unhandled errors.
## Import
```javascript
import { ErrorBoundary } from '@ohif/ui';
```
## Basic usage
<Playground>
<div className="p-4">
<ErrorBoundary context="Somewhere">
{() => {
throw new Error('Error!');
return <p></p>;
}}
</ErrorBoundary>
</div>
</Playground>
## Custom error fallback
<Playground>
{() => {
const CustomFallback = ({ error, componentStack, resetErrorBoundary }) => {
return (
<div>
<p>This is a custom fallback!</p>
</div>
);
};
return (
<div className="p-4">
<ErrorBoundary context="Somewhere" fallbackComponent={CustomFallback}>
{() => {
throw new Error('Error!');
return <p></p>;
}}
</ErrorBoundary>
</div>
);
}}
</Playground>
## Properties
<Props of={ErrorBoundary} />

View File

@ -0,0 +1,2 @@
import ErrorBoundary from './ErrorBoundary';
export default ErrorBoundary;

View File

@ -136,7 +136,7 @@ const ViewportActionBar = ({
</ButtonGroup> </ButtonGroup>
</div> </div>
)} )}
<div className="flex ml-4 mr-2 cursor-pointer" onClick={onPatientInfoClick}> <div className="flex ml-4 mr-2" onClick={onPatientInfoClick}>
<PatientInfo <PatientInfo
isOpen={showPatientInfo} isOpen={showPatientInfo}
patientName={patientName} patientName={patientName}
@ -245,7 +245,7 @@ function PatientInfo({
</div> </div>
)} )}
> >
<div className="relative flex justify-end"> <div className="relative flex justify-end cursor-pointer">
<div className="relative"> <div className="relative">
<Icon name="profile" className="w-5 text-white" /> <Icon name="profile" className="w-5 text-white" />
<Icon <Icon

View File

@ -4,6 +4,7 @@ import DateRange from './DateRange';
import Dialog from './Dialog'; import Dialog from './Dialog';
import Dropdown from './Dropdown'; import Dropdown from './Dropdown';
import EmptyStudies from './EmptyStudies'; import EmptyStudies from './EmptyStudies';
import ErrorBoundary from './ErrorBoundary';
import Icon from './Icon'; import Icon from './Icon';
import IconButton from './IconButton'; import IconButton from './IconButton';
import Input from './Input'; import Input from './Input';
@ -57,6 +58,7 @@ export {
Dialog, Dialog,
Dropdown, Dropdown,
EmptyStudies, EmptyStudies,
ErrorBoundary,
ExpandableToolbarButton, ExpandableToolbarButton,
ListMenu, ListMenu,
Icon, Icon,

View File

@ -5,6 +5,7 @@ import DataSourceWrapper from './DataSourceWrapper';
import WorkList from './WorkList'; import WorkList from './WorkList';
import NotFound from './NotFound'; import NotFound from './NotFound';
import buildModeRoutes from './buildModeRoutes'; import buildModeRoutes from './buildModeRoutes';
import { ErrorBoundary } from '@ohif/ui';
// TODO: Make these configurable // TODO: Make these configurable
// TODO: Include "routes" debug route if dev build // TODO: Include "routes" debug route if dev build
@ -43,6 +44,7 @@ const createRoutes = (
return ( return (
<Switch> <Switch>
{allRoutes.map((route, i) => { {allRoutes.map((route, i) => {
console.log(route);
return ( return (
<Route <Route
key={i} key={i}
@ -51,7 +53,9 @@ const createRoutes = (
strict={route.strict} strict={route.strict}
render={props => ( render={props => (
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
<route.component {...props} {...route.props} route={route} /> <ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
<route.component {...props} {...route.props} route={route} />
</ErrorBoundary>
)} )}
/> />
); );

View File

@ -18075,6 +18075,13 @@ react-dropzone@^10.1.7:
file-selector "^0.1.12" file-selector "^0.1.12"
prop-types "^15.7.2" prop-types "^15.7.2"
react-error-boundary@2.2.x:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-2.2.3.tgz#34c8238012d3b4148cec47a1b3cec669d5206578"
integrity sha512-Jiaiu6CJ4ho3sMCVI7gg+O/JB5vlFFZGwlnpFBTCOSyheYRTzz+FhBMo7tfnCTB/ZR0LaMzAPGbZGrEzAOd0eg==
dependencies:
"@babel/runtime" "^7.9.6"
react-error-overlay@^3.0.0: react-error-overlay@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655"