feat: 🎸 1729 - error boundary wrapper (#1764)

* Add error boundaries

* Fix PWA e2e.

* feat: ErrorBoundaryDialog

* replace component to use ErrorBoundaryDialog

* add proptypes

* fix context

* remove ErrorBoundary from extensions

Co-authored-by: igoroctaviano <igoroctaviano@gmail.com>
Co-authored-by: James A. Petts <jamesapetts@gmail.com>
This commit is contained in:
Rodrigo Antinarelli 2020-06-04 06:52:30 -03:00 committed by GitHub
parent f01640d009
commit c02b232b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 310 additions and 145 deletions

View File

@ -39,7 +39,9 @@ export default {
const onNewImageHandler = jumpData => { const onNewImageHandler = jumpData => {
commandsManager.runCommand('jumpToImage', jumpData); commandsManager.runCommand('jumpToImage', jumpData);
}; };
return <OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />; return (
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
);
}; };
return ExtendedOHIFCornerstoneViewport; return ExtendedOHIFCornerstoneViewport;

View File

@ -31,7 +31,18 @@ const refreshViewport = () => {
* @param {number} props.isOpen - isOpen * @param {number} props.isOpen - isOpen
* @returns component * @returns component
*/ */
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => { const RTPanel = ({
studies,
viewports,
activeIndex,
isOpen,
onContourItemClick,
activeContexts = [],
contexts = {}
}) => {
const isVTK = () => activeContexts.includes(contexts.VTK);
const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE);
const [selectedContour, setSelectedContour] = useState(); const [selectedContour, setSelectedContour] = useState();
const DEFAULT_SET_INDEX = 0; const DEFAULT_SET_INDEX = 0;
const DEFAULT_STATE = { const DEFAULT_STATE = {
@ -119,6 +130,7 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
onClick={() => { onClick={() => {
setSelectedContour(isSameContour ? null : ROINumber); setSelectedContour(isSameContour ? null : ROINumber);
if (isCornerstone()) {
const enabledElements = cornerstone.getEnabledElements(); const enabledElements = cornerstone.getEnabledElements();
const element = enabledElements[activeIndex].element; const element = enabledElements[activeIndex].element;
const toolState = cornerstoneTools.getToolState(element, 'stack'); const toolState = cornerstoneTools.getToolState(element, 'stack');
@ -146,6 +158,7 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
frameIndex, frameIndex,
activeViewportIndex: activeIndex activeViewportIndex: activeIndex
}); });
}
}} }}
label={`${ROIName} ${interpretedType}`} label={`${ROIName} ${interpretedType}`}
index={ROINumber} index={ROINumber}

View File

@ -20,14 +20,21 @@ export default {
preRegistration({ servicesManager, configuration = {} }) { preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration }); init({ servicesManager, configuration });
}, },
getPanelModule({ commandsManager }) { getPanelModule({ commandsManager, api }) {
const ExtendedRTPanel = props => { const ExtendedRTPanel = props => {
const { activeContexts } = api.hooks.useAppContext();
const contourItemClickHandler = contourData => { const contourItemClickHandler = contourData => {
commandsManager.runCommand('jumpToImage', contourData); commandsManager.runCommand('jumpToImage', contourData);
}; };
return ( return (
<RTPanel {...props} onContourItemClick={contourItemClickHandler} /> <RTPanel
{...props}
onContourItemClick={contourItemClickHandler}
activeContexts={activeContexts}
contexts={api.contexts}
/>
); );
}; };

View File

@ -49,8 +49,8 @@ const SegmentationPanel = ({
onConfigurationChange, onConfigurationChange,
onDisplaySetLoadFailure, onDisplaySetLoadFailure,
onSelectedSegmentationChange, onSelectedSegmentationChange,
activeContexts, activeContexts = [],
contexts, contexts = {},
}) => { }) => {
const isVTK = () => activeContexts.includes(contexts.VTK); const isVTK = () => activeContexts.includes(contexts.VTK);
const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE); const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE);

View File

@ -46,7 +46,7 @@ export default {
const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => { const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => {
commandsManager.runCommand('setSegmentConfiguration', { commandsManager.runCommand('setSegmentConfiguration', {
segmentNumber, segmentNumber,
visible visible,
}); });
}; };
@ -55,7 +55,7 @@ export default {
globalOpacity: configuration.fillAlpha, globalOpacity: configuration.fillAlpha,
outlineThickness: configuration.outlineWidth, outlineThickness: configuration.outlineWidth,
renderOutline: configuration.renderOutline, renderOutline: configuration.renderOutline,
visible: configuration.renderFill visible: configuration.renderFill,
}); });
}; };

View File

@ -1,3 +1,4 @@
import React from 'react';
import asyncComponent from './asyncComponent.js'; import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js'; import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js'; import toolbarModule from './toolbarModule.js';
@ -16,7 +17,8 @@ const vtkExtension = {
id: 'vtk', id: 'vtk',
getViewportModule({ commandsManager }) { getViewportModule({ commandsManager }) {
return withCommandsManager(OHIFVTKViewport, commandsManager); const ExtendedVTKViewport = props => <OHIFVTKViewport {...props} />;
return withCommandsManager(ExtendedVTKViewport, commandsManager);
}, },
getToolbarModule() { getToolbarModule() {
return toolbarModule; return toolbarModule;

View File

@ -53,6 +53,7 @@
"react-dnd-html5-backend": "^9.4.0", "react-dnd-html5-backend": "^9.4.0",
"react-dnd-touch-backend": "^9.4.0", "react-dnd-touch-backend": "^9.4.0",
"react-draggable": "^4.1.0", "react-draggable": "^4.1.0",
"react-error-boundary": "^2.2.1",
"react-i18next": "^10.11.0", "react-i18next": "^10.11.0",
"react-modal": "^3.11.1", "react-modal": "^3.11.1",
"react-with-direction": "1.3.0" "react-with-direction": "1.3.0"

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ErrorBoundary } from 'react-error-boundary';
import './ErrorFallback.css';
const ErrorFallback = ({ error, componentStack, resetErrorBoundary }) => {
return (
<div className="ErrorFallback" role="alert">
<p>Something went wrong.</p>
<pre>{error.message}</pre>
<pre>{componentStack}</pre>
</div>
);
};
const OHIFErrorBoundary = ({
context = 'OHIF',
onReset = () => {},
onError = () => {},
fallbackComponent,
children,
}) => {
const onErrorHandler = (error, componentStack) => {
console.error(`${context} Error Boundary`, error, componentStack);
onError(error, componentStack);
};
const onResetHandler = () => {
onReset();
};
return (
<ErrorBoundary
FallbackComponent={fallbackComponent || ErrorFallback}
onReset={onResetHandler}
onError={onErrorHandler}
>
{children}
</ErrorBoundary>
);
};
OHIFErrorBoundary.propTypes = {
context: PropTypes.string,
onReset: PropTypes.func,
onError: PropTypes.func,
children: PropTypes.node.isRequired,
fallbackComponent: PropTypes.element,
};
export default OHIFErrorBoundary;

View File

@ -0,0 +1,4 @@
.ErrorFallback {
padding: 10px;
color: var(--active-color);
}

View File

@ -0,0 +1 @@
export { default as ErrorBoundary } from './ErrorBoundary';

View File

@ -7,7 +7,6 @@ import { AboutContent } from './content/aboutContent/AboutContent';
import { TabComponents, TabFooter } from './tabComponents'; import { TabComponents, TabFooter } from './tabComponents';
import { HotkeyField } from './customForm'; import { HotkeyField } from './customForm';
import { LanguageSwitcher } from './languageSwitcher'; import { LanguageSwitcher } from './languageSwitcher';
import { Checkbox } from './checkbox'; import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog'; import { CineDialog } from './cineDialog';
import { ViewportDownloadForm } from './content/viewportDownloadForm'; import { ViewportDownloadForm } from './content/viewportDownloadForm';
@ -25,8 +24,10 @@ import {
} from './studyList'; } from './studyList';
import { ToolbarSection } from './toolbarSection'; import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip'; import { Tooltip } from './tooltip';
import { ErrorBoundary } from './errorBoundary';
export { export {
ErrorBoundary,
ContextMenu, ContextMenu,
Checkbox, Checkbox,
CineDialog, CineDialog,

View File

@ -29,6 +29,7 @@ import {
Tooltip, Tooltip,
AboutContent, AboutContent,
OHIFModal, OHIFModal,
ErrorBoundary
} from './components'; } from './components';
import { useDebounce, useMedia } from './hooks'; import { useDebounce, useMedia } from './hooks';
@ -123,6 +124,7 @@ export {
DialogProvider, DialogProvider,
withDialog, withDialog,
useDialog, useDialog,
ErrorBoundary,
// Hooks // Hooks
useDebounce, useDebounce,
useMedia, useMedia,

View File

@ -1,7 +1,7 @@
describe('OHIF Microscopy Extension', () => { describe('OHIF Microscopy Extension', () => {
before(() => { before(() => {
cy.openStudyModality('SM'); cy.openStudyModality('SM');
cy.expectMinimumThumbnails(6); cy.expectMinimumThumbnails(2);
}); });
it('checks if series thumbnails are being displayed', () => { it('checks if series thumbnails are being displayed', () => {

View File

@ -13,6 +13,7 @@ import {
ModalProvider, ModalProvider,
DialogProvider, DialogProvider,
OHIFModal, OHIFModal,
ErrorBoundary
} from '@ohif/ui'; } from '@ohif/ui';
import { import {
@ -167,6 +168,7 @@ class App extends Component {
if (this._userManager) { if (this._userManager) {
return ( return (
<ErrorBoundary context='App'>
<Provider store={store}> <Provider store={store}>
<AppProvider config={this._appConfig}> <AppProvider config={this._appConfig}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
@ -193,10 +195,12 @@ class App extends Component {
</I18nextProvider> </I18nextProvider>
</AppProvider> </AppProvider>
</Provider> </Provider>
</ErrorBoundary>
); );
} }
return ( return (
<ErrorBoundary context='App'>
<Provider store={store}> <Provider store={store}>
<AppProvider config={this._appConfig}> <AppProvider config={this._appConfig}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
@ -214,6 +218,7 @@ class App extends Component {
</I18nextProvider> </I18nextProvider>
</AppProvider> </AppProvider>
</Provider> </Provider>
</ErrorBoundary>
); );
} }

View File

@ -5,7 +5,7 @@ import { Route, Switch } from 'react-router-dom';
import { NProgress } from '@tanem/react-nprogress'; import { NProgress } from '@tanem/react-nprogress';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ViewerbaseDragDropContext } from '@ohif/ui'; import { ViewerbaseDragDropContext, ErrorBoundary } from '@ohif/ui';
import { SignoutCallbackComponent } from 'redux-oidc'; import { SignoutCallbackComponent } from 'redux-oidc';
import asyncComponent from './components/AsyncComponent.js'; import asyncComponent from './components/AsyncComponent.js';
import * as RoutesUtil from './routes/routesUtil'; import * as RoutesUtil from './routes/routesUtil';
@ -191,7 +191,9 @@ class OHIFStandaloneViewer extends Component {
{match === null ? ( {match === null ? (
<></> <></>
) : ( ) : (
<ErrorBoundary context={match.url}>
<Component match={match} location={this.props.location} /> <Component match={match} location={this.props.location} />
</ErrorBoundary>
)} )}
</CSSTransition> </CSSTransition>
)} )}

View File

@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ErrorBoundary } from '@ohif/ui';
import { servicesManager } from './../../App';
const { UIModalService } = servicesManager.services;
const ErrorBoundaryDialog = ({ context, children }) => {
const handleOnError = (error, componentStack) => {
const ErrorDialog = () => (
<div className="ErrorFallback" role="alert">
<div>
<h3>
{context}: <span>{error.message}</span>
</h3>
</div>
<pre>{componentStack}</pre>
</div>
);
UIModalService.show({
content: ErrorDialog,
title: `${context}: ${error.message}`,
});
};
const fallbackComponent = () => (
<div className="ErrorFallback" role="alert">
<p>
Error rendering {context}. <br /> Check the browser console for more
details.
</p>
</div>
);
return (
<ErrorBoundary
fallbackComponent={fallbackComponent}
context={context}
onError={handleOnError}
>
{children}
</ErrorBoundary>
);
};
ErrorBoundaryDialog.propTypes = {
context: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
export default ErrorBoundaryDialog;

View File

@ -0,0 +1,3 @@
import ErrorBoundaryDialog from './ErrorBoundaryDialog';
export default ErrorBoundaryDialog;

View File

@ -24,7 +24,7 @@ const ViewportGrid = function (props) {
studies, studies,
viewportData, viewportData,
children, children,
isStudyLoaded isStudyLoaded,
} = props; } = props;
const rowSize = 100 / numRows; const rowSize = 100 / numRows;
@ -54,7 +54,7 @@ const ViewportGrid = function (props) {
}); });
}); });
} }
}, [studies, viewportData, isStudyLoaded]); }, [studies, viewportData, isStudyLoaded, snackbar]);
const getViewportPanes = () => const getViewportPanes = () =>
layout.viewports.map((layout, viewportIndex) => { layout.viewports.map((layout, viewportIndex) => {

View File

@ -2,8 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { MODULE_TYPES } from '@ohif/core'; import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core';
import OHIF, { DICOMSR } from '@ohif/core';
import { withDialog } from '@ohif/ui'; import { withDialog } from '@ohif/ui';
import moment from 'moment'; import moment from 'moment';
import ConnectedHeader from './ConnectedHeader.js'; import ConnectedHeader from './ConnectedHeader.js';
@ -11,6 +10,7 @@ import ToolbarRow from './ToolbarRow.js';
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js'; import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js'; import SidePanel from './../components/SidePanel.js';
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
import { extensionManager } from './../App.js'; import { extensionManager } from './../App.js';
// Contexts // Contexts
@ -26,6 +26,7 @@ class Viewer extends Component {
PropTypes.shape({ PropTypes.shape({
StudyInstanceUID: PropTypes.string.isRequired, StudyInstanceUID: PropTypes.string.isRequired,
StudyDate: PropTypes.string, StudyDate: PropTypes.string,
PatientID: PropTypes.string,
displaySets: PropTypes.arrayOf( displaySets: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
displaySetInstanceUID: PropTypes.string.isRequired, displaySetInstanceUID: PropTypes.string.isRequired,
@ -256,6 +257,7 @@ class Viewer extends Component {
</WhiteLabelingContext.Consumer> </WhiteLabelingContext.Consumer>
{/* TOOLBAR */} {/* TOOLBAR */}
<ErrorBoundaryDialog context="ToolbarRow">
<ToolbarRow <ToolbarRow
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen} isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
isRightSidePanelOpen={this.state.isRightSidePanelOpen} isRightSidePanelOpen={this.state.isRightSidePanelOpen}
@ -292,6 +294,7 @@ class Viewer extends Component {
}} }}
studies={this.props.studies} studies={this.props.studies}
/> />
</ErrorBoundaryDialog>
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/} {/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
{/*<StudyPrefetcher studies={this.props.studies} />*/} {/*<StudyPrefetcher studies={this.props.studies} />*/}
@ -299,6 +302,7 @@ class Viewer extends Component {
{/* VIEWPORTS + SIDEPANELS */} {/* VIEWPORTS + SIDEPANELS */}
<div className="FlexboxLayout"> <div className="FlexboxLayout">
{/* LEFT */} {/* LEFT */}
<ErrorBoundaryDialog context="LeftSidePanel">
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}> <SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
{VisiblePanelLeft ? ( {VisiblePanelLeft ? (
<VisiblePanelLeft <VisiblePanelLeft
@ -313,13 +317,20 @@ class Viewer extends Component {
/> />
)} )}
</SidePanel> </SidePanel>
</ErrorBoundaryDialog>
{/* MAIN */} {/* MAIN */}
<div className={classNames('main-content')}> <div className={classNames('main-content')}>
<ConnectedViewerMain studies={this.props.studies} isStudyLoaded={this.props.isStudyLoaded} /> <ErrorBoundaryDialog context="ViewerMain">
<ConnectedViewerMain
studies={this.props.studies}
isStudyLoaded={this.props.isStudyLoaded}
/>
</ErrorBoundaryDialog>
</div> </div>
{/* RIGHT */} {/* RIGHT */}
<ErrorBoundaryDialog context="RightSidePanel">
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}> <SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
{VisiblePanelRight && ( {VisiblePanelRight && (
<VisiblePanelRight <VisiblePanelRight
@ -330,6 +341,7 @@ class Viewer extends Component {
/> />
)} )}
</SidePanel> </SidePanel>
</ErrorBoundaryDialog>
</div> </div>
</> </>
); );

View File

@ -1247,7 +1247,7 @@
pirates "^4.0.0" pirates "^4.0.0"
source-map-support "^0.5.9" source-map-support "^0.5.9"
"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": "@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6":
version "7.5.5" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
@ -15796,6 +15796,13 @@ react-dropzone@^10.1.7:
file-selector "^0.1.11" file-selector "^0.1.11"
prop-types "^15.7.2" prop-types "^15.7.2"
react-error-boundary@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-2.2.1.tgz#32ed74386a90482060cc2fea948bb7135465c4cb"
integrity sha512-8SZMkJRFUb0JuluHKwuUtkh5vvVWBg3O/bJIWgNMhMJv529aG//TmcphR3cSMhCWjz1vFZDb4taJd6pmwP5mEQ==
dependencies:
"@babel/runtime" "^7.9.6"
react-error-overlay@^4.0.1: react-error-overlay@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"