diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index cf2c11826..cbf414efc 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; -import classnames from 'classnames'; import PropTypes from 'prop-types'; -import { SidePanel } from '@ohif/ui'; +import { SidePanel, ErrorBoundary } from '@ohif/ui'; import Header from './Header.jsx'; import NestedMenu from './ToolbarButtonNestedMenu.jsx'; @@ -93,27 +92,28 @@ function ViewerLayout({ return (
-
- {toolbars.primary.map(toolDef => { - const isNested = Array.isArray(toolDef); - - if (!isNested) { - const { id, Component, componentProps } = toolDef; - return ; - } else { - return ( - -
- {toolDef.map(x => { - const { id, Component, componentProps } = x; - return ; - })} -
-
- ); - } - })} -
+ +
+ {toolbars.primary.map(toolDef => { + const isNested = Array.isArray(toolDef); + if (!isNested) { + const { id, Component, componentProps } = toolDef; + return ; + } else { + return ( + +
+ {toolDef.map(x => { + const { id, Component, componentProps } = x; + return ; + })} +
+
+ ); + } + })} +
+
{/* LEFT SIDEPANELS */} {leftPanelComponents.length && ( - + + + )} {/* TOOLBAR + GRID */}
-
- {toolbars.secondary.map(toolDef => { - const { id, Component, componentProps } = toolDef; - - return ; - })} -
+ +
+ {toolbars.secondary.map(toolDef => { + const { id, Component, componentProps } = toolDef; + return ; + })} +
+
- + + +
{rightPanelComponents.length && ( - + + + )}
diff --git a/platform/ui/index.js b/platform/ui/index.js index e3c8a8f25..5aa3402d6 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -33,6 +33,7 @@ export { Dialog, Dropdown, EmptyStudies, + ErrorBoundary, ExpandableToolbarButton, ListMenu, Icon, diff --git a/platform/ui/package.json b/platform/ui/package.json index 17619f2cf..601b7862d 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -42,6 +42,7 @@ "react-dnd": "^10.0.2", "react-dnd-html5-backend": "^10.0.2", "react-dnd-touch-backend": "^10.0.2", + "react-error-boundary": "2.2.x", "react-dom": "16.11.0", "react-modal": "^3.11.2", "react-powerplug": "1.0.0", diff --git a/platform/ui/src/components/ErrorBoundary/ErrorBoundary.jsx b/platform/ui/src/components/ErrorBoundary/ErrorBoundary.jsx new file mode 100644 index 000000000..f6915592c --- /dev/null +++ b/platform/ui/src/components/ErrorBoundary/ErrorBoundary.jsx @@ -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 ( +
+

{title}

+

{subtitle}

+ {!isProduction && ( +
+
Context: {context}
+
Error Message: {error.message}
+
Stack: {componentStack}
+
+ )} +
+ ); +}; + +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 => ( + { + setIsOpen(false); + if (fallbackRoute) { + window.location = fallbackRoute; + } + }} + > + + + ); + + const Fallback = isPage ? FallbackComponent : withModal(FallbackComponent); + + return ( + ( + + )} + onReset={onResetHandler} + onError={onErrorHandler} + > + {children} + + ); +}; + +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; diff --git a/platform/ui/src/components/ErrorBoundary/ErrorBoundary.mdx b/platform/ui/src/components/ErrorBoundary/ErrorBoundary.mdx new file mode 100644 index 000000000..bb65df025 --- /dev/null +++ b/platform/ui/src/components/ErrorBoundary/ErrorBoundary.mdx @@ -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 + + +
+ + {() => { + throw new Error('Error!'); + return

; + }} +
+
+
+ +## Custom error fallback + + + {() => { + const CustomFallback = ({ error, componentStack, resetErrorBoundary }) => { + return ( +
+

This is a custom fallback!

+
+ ); + }; + return ( +
+ + {() => { + throw new Error('Error!'); + return

; + }} +
+
+ ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/ErrorBoundary/index.js b/platform/ui/src/components/ErrorBoundary/index.js new file mode 100644 index 000000000..b54419282 --- /dev/null +++ b/platform/ui/src/components/ErrorBoundary/index.js @@ -0,0 +1,2 @@ +import ErrorBoundary from './ErrorBoundary'; +export default ErrorBoundary; diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index 9129d871c..12f052e76 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -136,7 +136,7 @@ const ViewportActionBar = ({ )} -
+
)} > -
+
{allRoutes.map((route, i) => { + console.log(route); return ( ( // eslint-disable-next-line react/jsx-props-no-spreading - + + + )} /> ); diff --git a/yarn.lock b/yarn.lock index 1b0b1ec12..7cb2da6e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18075,6 +18075,13 @@ react-dropzone@^10.1.7: file-selector "^0.1.12" 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: version "3.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655"