Merge pull request #1824 from OHIF/feat/ohif-125
OHIF-125: Error boundaries
This commit is contained in:
commit
aa402183f4
@ -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,10 +92,10 @@ function ViewerLayout({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header>
|
<Header>
|
||||||
|
<ErrorBoundary context="Primary Toolbar">
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
{toolbars.primary.map(toolDef => {
|
{toolbars.primary.map(toolDef => {
|
||||||
const isNested = Array.isArray(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} />;
|
||||||
@ -114,6 +113,7 @@ function ViewerLayout({
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</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 && (
|
||||||
|
<ErrorBoundary context="Left Panel">
|
||||||
<SidePanel
|
<SidePanel
|
||||||
side="left"
|
side="left"
|
||||||
defaultComponentOpen={leftPanelComponents[0].name}
|
defaultComponentOpen={leftPanelComponents[0].name}
|
||||||
childComponents={leftPanelComponents}
|
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">
|
||||||
|
<ErrorBoundary context="Secondary Toolbar">
|
||||||
<div className="flex items-center w-full px-3 bg-primary-dark">
|
<div className="flex items-center w-full px-3 bg-primary-dark">
|
||||||
{toolbars.secondary.map(toolDef => {
|
{toolbars.secondary.map(toolDef => {
|
||||||
const { id, Component, componentProps } = 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">
|
||||||
|
<ErrorBoundary context="Grid">
|
||||||
<ViewportGridComp
|
<ViewportGridComp
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
viewportComponents={viewportComponents}
|
viewportComponents={viewportComponents}
|
||||||
/>
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{rightPanelComponents.length && (
|
{rightPanelComponents.length && (
|
||||||
|
<ErrorBoundary context="Right Panel">
|
||||||
<SidePanel
|
<SidePanel
|
||||||
side="right"
|
side="right"
|
||||||
defaultComponentOpen={rightPanelComponents[0].name}
|
defaultComponentOpen={rightPanelComponents[0].name}
|
||||||
childComponents={rightPanelComponents}
|
childComponents={rightPanelComponents}
|
||||||
/>
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export {
|
|||||||
Dialog,
|
Dialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
EmptyStudies,
|
EmptyStudies,
|
||||||
|
ErrorBoundary,
|
||||||
ExpandableToolbarButton,
|
ExpandableToolbarButton,
|
||||||
ListMenu,
|
ListMenu,
|
||||||
Icon,
|
Icon,
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
109
platform/ui/src/components/ErrorBoundary/ErrorBoundary.jsx
Normal file
109
platform/ui/src/components/ErrorBoundary/ErrorBoundary.jsx
Normal 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;
|
||||||
59
platform/ui/src/components/ErrorBoundary/ErrorBoundary.mdx
Normal file
59
platform/ui/src/components/ErrorBoundary/ErrorBoundary.mdx
Normal 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} />
|
||||||
2
platform/ui/src/components/ErrorBoundary/index.js
Normal file
2
platform/ui/src/components/ErrorBoundary/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import ErrorBoundary from './ErrorBoundary';
|
||||||
|
export default ErrorBoundary;
|
||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
|
||||||
<route.component {...props} {...route.props} route={route} />
|
<route.component {...props} {...route.props} route={route} />
|
||||||
|
</ErrorBoundary>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user