feat: enhanced error handling ui (#3021)

* feat: better error handling

* better error handling

* try to fix netlify build

* remove new line
This commit is contained in:
Alireza 2022-11-16 20:27:24 -05:00 committed by GitHub
parent 83ea228d86
commit eddd510a35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 39 deletions

View File

@ -8,14 +8,6 @@ import { utils } from '@ohif/core';
const { downloadCSVReport } = utils; const { downloadCSVReport } = utils;
// tools with measurements to display inside the panel
const MEASUREMENT_TOOLS = [
'EllipticalROI',
'RectangleROI',
'Length',
'Bidirectional',
];
export default function PanelMeasurementTable({ export default function PanelMeasurementTable({
servicesManager, servicesManager,
commandsManager, commandsManager,
@ -185,12 +177,8 @@ PanelMeasurementTable.propTypes = {
function _getMappedMeasurements(MeasurementService) { function _getMappedMeasurements(MeasurementService) {
const measurements = MeasurementService.getMeasurements(); const measurements = MeasurementService.getMeasurements();
// filter out measurements whose toolName is not in MEASUREMENT_TOOLS
const measurementTools = measurements.filter(measurement =>
MEASUREMENT_TOOLS.includes(measurement.toolName)
);
const mappedMeasurements = measurementTools.map((m, index) => const mappedMeasurements = measurements.map((m, index) =>
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
); );

View File

@ -122,10 +122,29 @@ function ViewerLayout({
}; };
}, []); }, []);
const getPanelData = id => { const getComponent = id => {
const entry = extensionManager.getModuleEntry(id); const entry = extensionManager.getModuleEntry(id);
// TODO, not sure why sidepanel content has to be JSX, and not a children prop?
const content = entry.component; if (!entry) {
throw new Error(
`${id} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.`
);
}
let content;
if (entry && entry.component) {
content = entry.component;
} else {
throw new Error(
`No component found from extension ${id}. Check the reference string to the extension in your Mode configuration`
);
}
return { entry, content };
};
const getPanelData = id => {
const { content, entry } = getComponent(id);
return { return {
iconName: entry.iconName, iconName: entry.iconName,
@ -156,7 +175,7 @@ function ViewerLayout({
}, [HangingProtocolService]); }, [HangingProtocolService]);
const getViewportComponentData = viewportComponent => { const getViewportComponentData = viewportComponent => {
const entry = extensionManager.getModuleEntry(viewportComponent.namespace); const { entry } = getComponent(viewportComponent.namespace);
return { return {
component: entry.component, component: entry.component,

View File

@ -498,8 +498,7 @@ class MeasurementService {
measurement.source = source; measurement.source = source;
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}:`, `Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}`
error.message
); );
} }

View File

@ -1,11 +1,18 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import { Icon, IconButton } from '@ohif/ui';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from '../Modal'; import Modal from '../Modal';
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const DefaultFallback = ({ error, componentStack, context, resetErrorBoundary, fallbackRoute }) => { const DefaultFallback = ({
error,
context,
resetErrorBoundary,
fallbackRoute,
}) => {
const [showDetails, setShowDetails] = useState(false);
const title = `Something went wrong${!isProduction && ` in ${context}`}.`; const title = `Something went wrong${!isProduction && ` in ${context}`}.`;
const subtitle = `Sorry, something went wrong there. Try again.`; const subtitle = `Sorry, something went wrong there. Try again.`;
return ( return (
@ -13,10 +20,26 @@ const DefaultFallback = ({ error, componentStack, context, resetErrorBoundary, f
<p className="text-primary-light text-xl">{title}</p> <p className="text-primary-light text-xl">{title}</p>
<p className="text-primary-light text-base">{subtitle}</p> <p className="text-primary-light text-base">{subtitle}</p>
{!isProduction && ( {!isProduction && (
<div className="rounded-md bg-secondary-dark p-5 mt-5"> <div className="rounded-md bg-secondary-dark p-5 mt-5 font-mono space-y-2">
<pre className="text-primary-light">Context: {context}</pre> <p className="text-primary-light">Context: {context}</p>
<pre className="text-primary-light">Error Message: {error.message}</pre> <p className="text-primary-light">Error Message: {error.message}</p>
<pre className="text-primary-light">Stack: {componentStack}</pre>
<IconButton
variant="contained"
color="inherit"
size="initial"
className="text-primary-active"
onClick={() => setShowDetails(!showDetails)}
>
<React.Fragment>
<div>{'Stack Trace'}</div>
<Icon width="15px" height="15px" name="chevron-down" />
</React.Fragment>
</IconButton>
{showDetails && (
<p className="px-4 text-primary-light">Stack: {error.stack}</p>
)}
</div> </div>
)} )}
</div> </div>
@ -32,7 +55,7 @@ DefaultFallback.propTypes = {
}; };
DefaultFallback.defaultProps = { DefaultFallback.defaultProps = {
resetErrorBoundary: noop resetErrorBoundary: noop,
}; };
const ErrorBoundary = ({ const ErrorBoundary = ({
@ -42,7 +65,7 @@ const ErrorBoundary = ({
fallbackComponent: FallbackComponent, fallbackComponent: FallbackComponent,
children, children,
fallbackRoute, fallbackRoute,
isPage isPage,
}) => { }) => {
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(true);
@ -53,7 +76,7 @@ const ErrorBoundary = ({
const onResetHandler = (...args) => onReset(...args); const onResetHandler = (...args) => onReset(...args);
const withModal = (Component) => props => ( const withModal = Component => props => (
<Modal <Modal
closeButton closeButton
shouldCloseOnEsc shouldCloseOnEsc
@ -75,11 +98,7 @@ const ErrorBoundary = ({
return ( return (
<ReactErrorBoundary <ReactErrorBoundary
fallbackRender={props => ( fallbackRender={props => (
<Fallback <Fallback {...props} context={context} fallbackRoute={fallbackRoute} />
{...props}
context={context}
fallbackRoute={fallbackRoute}
/>
)} )}
onReset={onResetHandler} onReset={onResetHandler}
onError={onErrorHandler} onError={onErrorHandler}
@ -95,7 +114,7 @@ ErrorBoundary.propTypes = {
onError: PropTypes.func, onError: PropTypes.func,
fallbackComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), fallbackComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
fallbackRoute: PropTypes.string fallbackRoute: PropTypes.string,
}; };
ErrorBoundary.defaultProps = { ErrorBoundary.defaultProps = {
@ -103,7 +122,7 @@ ErrorBoundary.defaultProps = {
onReset: noop, onReset: noop,
onError: noop, onError: noop,
fallbackComponent: DefaultFallback, fallbackComponent: DefaultFallback,
fallbackRoute: null fallbackRoute: null,
}; };
export default ErrorBoundary; export default ErrorBoundary;

View File

@ -201,6 +201,14 @@ const ICONS = {
'old-stop': oldStop, 'old-stop': oldStop,
}; };
function addIcon(iconName, iconSVG) {
if (ICONS[iconName]) {
console.warn(`Icon ${iconName} already exists.`);
}
ICONS[iconName] = iconSVG;
}
/** /**
* Return the matching SVG Icon as a React Component. * Return the matching SVG Icon as a React Component.
* Results in an inlined SVG Element. If there's no match, * Results in an inlined SVG Element. If there's no match,
@ -214,4 +222,4 @@ export default function getIcon(key, props) {
return React.createElement(ICONS[key], props); return React.createElement(ICONS[key], props);
} }
export { getIcon, ICONS }; export { getIcon, ICONS, addIcon };

View File

@ -8,7 +8,7 @@ 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 ErrorBoundary from './ErrorBoundary';
import Icon from './Icon'; import Icon, { addIcon } from './Icon';
import IconButton from './IconButton'; import IconButton from './IconButton';
import Input from './Input'; import Input from './Input';
import InputDateRange from './InputDateRange'; import InputDateRange from './InputDateRange';
@ -89,6 +89,7 @@ export {
ExpandableToolbarButton, ExpandableToolbarButton,
ListMenu, ListMenu,
Icon, Icon,
addIcon,
IconButton, IconButton,
Input, Input,
InputDateRange, InputDateRange,

View File

@ -107,7 +107,7 @@ export {
} from './components'; } from './components';
/** These are mostly used in the docs */ /** These are mostly used in the docs */
export { getIcon, ICONS } from './components/Icon/getIcon'; export { getIcon, ICONS, addIcon } from './components/Icon/getIcon';
export { BackgroundColor } from './pages/Colors/BackgroundColor'; export { BackgroundColor } from './pages/Colors/BackgroundColor';
export { ModalComponent } from './contextProviders/ModalComponent'; export { ModalComponent } from './contextProviders/ModalComponent';
export { Types }; export { Types };

View File

@ -1 +1 @@
3.7 3.8