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;
// tools with measurements to display inside the panel
const MEASUREMENT_TOOLS = [
'EllipticalROI',
'RectangleROI',
'Length',
'Bidirectional',
];
export default function PanelMeasurementTable({
servicesManager,
commandsManager,
@ -185,12 +177,8 @@ PanelMeasurementTable.propTypes = {
function _getMappedMeasurements(MeasurementService) {
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)
);

View File

@ -122,10 +122,29 @@ function ViewerLayout({
};
}, []);
const getPanelData = id => {
const getComponent = 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 {
iconName: entry.iconName,
@ -156,7 +175,7 @@ function ViewerLayout({
}, [HangingProtocolService]);
const getViewportComponentData = viewportComponent => {
const entry = extensionManager.getModuleEntry(viewportComponent.namespace);
const { entry } = getComponent(viewportComponent.namespace);
return {
component: entry.component,

View File

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

View File

@ -1,11 +1,18 @@
import React, { useState } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import { Icon, IconButton } from '@ohif/ui';
import PropTypes from 'prop-types';
import Modal from '../Modal';
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 subtitle = `Sorry, something went wrong there. Try again.`;
return (
@ -13,17 +20,33 @@ const DefaultFallback = ({ error, componentStack, context, resetErrorBoundary, f
<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 className="rounded-md bg-secondary-dark p-5 mt-5 font-mono space-y-2">
<p className="text-primary-light">Context: {context}</p>
<p className="text-primary-light">Error Message: {error.message}</p>
<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>
);
};
const noop = () => { };
const noop = () => {};
DefaultFallback.propTypes = {
error: PropTypes.object.isRequired,
@ -32,7 +55,7 @@ DefaultFallback.propTypes = {
};
DefaultFallback.defaultProps = {
resetErrorBoundary: noop
resetErrorBoundary: noop,
};
const ErrorBoundary = ({
@ -42,7 +65,7 @@ const ErrorBoundary = ({
fallbackComponent: FallbackComponent,
children,
fallbackRoute,
isPage
isPage,
}) => {
const [isOpen, setIsOpen] = useState(true);
@ -53,7 +76,7 @@ const ErrorBoundary = ({
const onResetHandler = (...args) => onReset(...args);
const withModal = (Component) => props => (
const withModal = Component => props => (
<Modal
closeButton
shouldCloseOnEsc
@ -75,11 +98,7 @@ const ErrorBoundary = ({
return (
<ReactErrorBoundary
fallbackRender={props => (
<Fallback
{...props}
context={context}
fallbackRoute={fallbackRoute}
/>
<Fallback {...props} context={context} fallbackRoute={fallbackRoute} />
)}
onReset={onResetHandler}
onError={onErrorHandler}
@ -95,7 +114,7 @@ ErrorBoundary.propTypes = {
onError: PropTypes.func,
fallbackComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
children: PropTypes.node.isRequired,
fallbackRoute: PropTypes.string
fallbackRoute: PropTypes.string,
};
ErrorBoundary.defaultProps = {
@ -103,7 +122,7 @@ ErrorBoundary.defaultProps = {
onReset: noop,
onError: noop,
fallbackComponent: DefaultFallback,
fallbackRoute: null
fallbackRoute: null,
};
export default ErrorBoundary;

View File

@ -201,6 +201,14 @@ const ICONS = {
'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.
* 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);
}
export { getIcon, ICONS };
export { getIcon, ICONS, addIcon };

View File

@ -8,7 +8,7 @@ import Dialog from './Dialog';
import Dropdown from './Dropdown';
import EmptyStudies from './EmptyStudies';
import ErrorBoundary from './ErrorBoundary';
import Icon from './Icon';
import Icon, { addIcon } from './Icon';
import IconButton from './IconButton';
import Input from './Input';
import InputDateRange from './InputDateRange';
@ -89,6 +89,7 @@ export {
ExpandableToolbarButton,
ListMenu,
Icon,
addIcon,
IconButton,
Input,
InputDateRange,

View File

@ -107,7 +107,7 @@ export {
} from './components';
/** 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 { ModalComponent } from './contextProviders/ModalComponent';
export { Types };

View File

@ -1 +1 @@
3.7
3.8