diff --git a/extensions/debugging/src/DebugReportModal.css b/extensions/debugging/src/DebugReportModal.css
index ea83312f7..f61e83327 100644
--- a/extensions/debugging/src/DebugReportModal.css
+++ b/extensions/debugging/src/DebugReportModal.css
@@ -12,3 +12,15 @@
display: flex;
flex-direction: row;
}
+
+.debug-report-modal-container .errors {
+ margin-top: 10px;
+}
+
+.debug-report-modal-container .errors-container {
+ border: 1px solid var(--active-color);
+ border-radius: 5px;
+ padding: 10px;
+ overflow: scroll;
+ max-height: 300px;
+}
diff --git a/extensions/debugging/src/DebugReportModal.js b/extensions/debugging/src/DebugReportModal.js
index edc722335..5926bb291 100644
--- a/extensions/debugging/src/DebugReportModal.js
+++ b/extensions/debugging/src/DebugReportModal.js
@@ -10,6 +10,7 @@ const DubugReportModal = ({
extensionManager,
mailTo,
debugModalMessage,
+ errors = [],
}) => {
const copyDebugDataToClipboard = () => {
const body = getEmailBody();
@@ -131,6 +132,19 @@ const DubugReportModal = ({
{getLayout(viewports)}
+
+
Errors ({errors.length})
+
+ {errors.map(error => {
+ return (
+
+
Message: {error.message}
+ {error.error &&
Stack: {error.error.stack}}
+
+ );
+ })}
+
+
);
};
diff --git a/extensions/debugging/src/commandsModule.js b/extensions/debugging/src/commandsModule.js
index 4969a07d0..a6ad0498f 100644
--- a/extensions/debugging/src/commandsModule.js
+++ b/extensions/debugging/src/commandsModule.js
@@ -1,4 +1,6 @@
import OHIF from '@ohif/core';
+import { ToolbarButton, useLogger } from '@ohif/ui';
+
import {
save,
upload,
@@ -95,6 +97,7 @@ export function getCommands(context, servicesManager, extensionManager) {
const { UIModalService } = servicesManager.services;
const WrappedDebugReportModal = function() {
+ const { state } = useLogger();
return (
);
};
diff --git a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js
index 37b539ea5..8e047007f 100644
--- a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js
+++ b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js
@@ -70,12 +70,18 @@ class DicomMicroscopyViewport extends Component {
});
} catch (error) {
console.error('[Microscopy Viewer] Failed to load:', error);
- const { UINotificationService } = this.props.servicesManager.services;
+ const {
+ UINotificationService,
+ LoggerService,
+ } = this.props.servicesManager.services;
if (UINotificationService) {
+ const message =
+ 'Failed to load viewport. Please check that you have hardware acceleration enabled.';
+ LoggerService.error({ error, message });
UINotificationService.show({
+ autoClose: false,
title: 'Microscopy Viewport',
- message:
- 'Failed to load viewport. Please check that you have hardware acceleration enabled.',
+ message,
type: 'error',
});
}
diff --git a/extensions/dicom-segmentation/src/index.js b/extensions/dicom-segmentation/src/index.js
index d6c171490..33123b23e 100644
--- a/extensions/dicom-segmentation/src/index.js
+++ b/extensions/dicom-segmentation/src/index.js
@@ -25,12 +25,13 @@ export default {
return toolbarModule;
},
getPanelModule({ commandsManager, api, servicesManager }) {
- const { UINotificationService } = servicesManager.services;
+ const { UINotificationService, LoggerService } = servicesManager.services;
const ExtendedSegmentationPanel = props => {
const { activeContexts } = api.hooks.useAppContext();
const onDisplaySetLoadFailureHandler = error => {
+ LoggerService.error({ error, message: error.message });
UINotificationService.show({
title: 'DICOM Segmentation Loader',
message: error.message,
diff --git a/extensions/vtk/src/OHIFVTKViewport.js b/extensions/vtk/src/OHIFVTKViewport.js
index bac284a4e..aa11b5c32 100644
--- a/extensions/vtk/src/OHIFVTKViewport.js
+++ b/extensions/vtk/src/OHIFVTKViewport.js
@@ -360,12 +360,15 @@ class OHIFVTKViewport extends Component {
} catch (error) {
const errorTitle = 'Failed to load 2D MPR';
console.error(errorTitle, error);
- const { UINotificationService } = this.props.servicesManager.services;
+ const {
+ UINotificationService,
+ LoggerService,
+ } = this.props.servicesManager.services;
if (this.props.viewportIndex === 0) {
const message = error.message.includes('buffer')
? 'Dataset is too big to display in MPR'
: error.message;
- console.error(errorTitle, error);
+ LoggerService.error({ error, message });
UINotificationService.show({
title: errorTitle,
message,
@@ -428,11 +431,15 @@ class OHIFVTKViewport extends Component {
};
const onPixelDataInsertedErrorCallback = error => {
- const { UINotificationService } = this.props.servicesManager.services;
+ const {
+ UINotificationService,
+ LoggerService,
+ } = this.props.servicesManager.services;
if (!this.hasError) {
if (this.props.viewportIndex === 0) {
// Only show the notification from one viewport 1 in MPR2D.
+ LoggerService.error({ error, message: error.message });
UINotificationService.show({
title: 'MPR Load Error',
message: error.message,
diff --git a/extensions/vtk/src/commandsModule.js b/extensions/vtk/src/commandsModule.js
index dcd1e2d7e..287f92f9d 100644
--- a/extensions/vtk/src/commandsModule.js
+++ b/extensions/vtk/src/commandsModule.js
@@ -14,7 +14,9 @@ import OHIFVTKViewport from './OHIFVTKViewport';
const { BlendMode } = Constants;
-const commandsModule = ({ commandsManager, UINotificationService }) => {
+const commandsModule = ({ commandsManager, servicesManager }) => {
+ const { UINotificationService, LoggerService } = servicesManager.services;
+
// TODO: Put this somewhere else
let apis = {};
let defaultVOI;
@@ -175,7 +177,7 @@ const commandsModule = ({ commandsManager, UINotificationService }) => {
segmentNumber,
frameIndex,
frame,
- done = () => { },
+ done = () => {},
}) => {
let api = apis[viewports.activeViewportIndex];
@@ -473,10 +475,12 @@ const commandsModule = ({ commandsManager, UINotificationService }) => {
const volumeLength = dimensions[0] * dimensions[1] * dimensions[2];
if (volumeLength > maxBufferLengthFloat32) {
+ const message =
+ 'This volume is too large to fit in WebGL 1 textures and will display incorrectly. Please use a different browser to view this data';
+ LoggerService.error({ message });
UINotificationService.show({
title: 'Browser does not support WebGL 2',
- message:
- 'This volume is too large to fit in WebGL 1 textures and will display incorrectly. Please use a different browser to view this data',
+ message,
type: 'error',
autoClose: false,
});
diff --git a/extensions/vtk/src/index.js b/extensions/vtk/src/index.js
index 18e69c22c..9fb5475bc 100644
--- a/extensions/vtk/src/index.js
+++ b/extensions/vtk/src/index.js
@@ -35,8 +35,7 @@ const vtkExtension = {
return toolbarModule;
},
getCommandsModule({ commandsManager, servicesManager }) {
- const { UINotificationService } = servicesManager.services;
- return commandsModule({ commandsManager, UINotificationService });
+ return commandsModule({ commandsManager, servicesManager });
},
};
diff --git a/platform/core/src/classes/HotkeysManager.js b/platform/core/src/classes/HotkeysManager.js
index ee65f08a3..f4369b2d7 100644
--- a/platform/core/src/classes/HotkeysManager.js
+++ b/platform/core/src/classes/HotkeysManager.js
@@ -63,10 +63,15 @@ export class HotkeysManager {
definitions.forEach(definition => this.registerHotkeys(definition));
} catch (error) {
- const { UINotificationService } = this._servicesManager.services;
+ const {
+ UINotificationService,
+ LoggerService,
+ } = this._servicesManager.services;
+ const message = 'Erro while setting hotkeys';
+ LoggerService.error({ error, message });
UINotificationService.show({
title: 'Hotkeys Manager',
- message: 'Erro while setting hotkeys',
+ message,
type: 'error',
});
}
diff --git a/platform/core/src/index.js b/platform/core/src/index.js
index 3f7727d2d..8aa24ea05 100644
--- a/platform/core/src/index.js
+++ b/platform/core/src/index.js
@@ -26,6 +26,7 @@ import {
UIModalService,
UIDialogService,
MeasurementService,
+ LoggerService,
} from './services';
const OHIF = {
@@ -60,6 +61,7 @@ const OHIF = {
UIModalService,
UIDialogService,
MeasurementService,
+ LoggerService,
};
export {
@@ -93,6 +95,7 @@ export {
UIModalService,
UIDialogService,
MeasurementService,
+ LoggerService,
};
export { OHIF };
diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js
index 47ee09488..e42fb6e24 100644
--- a/platform/core/src/index.test.js
+++ b/platform/core/src/index.test.js
@@ -14,6 +14,7 @@ describe('Top level exports', () => {
'UIModalService',
'UIDialogService',
'MeasurementService',
+ 'LoggerService',
//
'utils',
'hotkeys',
diff --git a/platform/core/src/services/LoggerService/index.js b/platform/core/src/services/LoggerService/index.js
new file mode 100644
index 000000000..3a42ac118
--- /dev/null
+++ b/platform/core/src/services/LoggerService/index.js
@@ -0,0 +1,67 @@
+const name = 'LoggerService';
+
+const publicAPI = {
+ name,
+ info: _info,
+ error: _error,
+ setServiceImplementation,
+};
+
+const serviceImplementation = {
+ _info: () => console.warn('info() NOT IMPLEMENTED'),
+ _error: () => console.warn('error() NOT IMPLEMENTED'),
+};
+
+/**
+ * Logs an info
+ *
+ * @param {object} props { message, displayOnConsole }
+ */
+function _info({ message, displayOnConsole }) {
+ return serviceImplementation._info({
+ message,
+ displayOnConsole,
+ });
+}
+
+/**
+ * Logs an error
+ *
+ * @param {object} props { error, stack, message, displayOnConsole }
+ * @returns void
+ */
+function _error({ error, stack, message, displayOnConsole }) {
+ return serviceImplementation._error({
+ error,
+ stack,
+ message,
+ displayOnConsole,
+ });
+}
+
+/**
+ *
+ *
+ * @param {*} {
+ * info: infoImplementation,
+ * error: errorImplementation,
+ * }
+ */
+function setServiceImplementation({
+ info: infoImplementation,
+ error: errorImplementation,
+}) {
+ if (infoImplementation) {
+ serviceImplementation._info = infoImplementation;
+ }
+ if (errorImplementation) {
+ serviceImplementation._error = errorImplementation;
+ }
+}
+
+export default {
+ name,
+ create: ({ configuration = {} }) => {
+ return publicAPI;
+ },
+};
diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js
index a7fa17072..b4eb4cbfc 100644
--- a/platform/core/src/services/index.js
+++ b/platform/core/src/services/index.js
@@ -3,6 +3,7 @@ import UINotificationService from './UINotificationService';
import UIModalService from './UIModalService';
import UIDialogService from './UIDialogService';
import MeasurementService from './MeasurementService';
+import LoggerService from './LoggerService';
export {
UINotificationService,
@@ -10,4 +11,5 @@ export {
UIDialogService,
ServicesManager,
MeasurementService,
+ LoggerService,
};
diff --git a/platform/ui/src/contextProviders/LoggerProvider.js b/platform/ui/src/contextProviders/LoggerProvider.js
new file mode 100644
index 000000000..624a788fe
--- /dev/null
+++ b/platform/ui/src/contextProviders/LoggerProvider.js
@@ -0,0 +1,104 @@
+import React, { useState, createContext, useContext, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+const LoggerContext = createContext(null);
+const { Provider } = LoggerContext;
+
+export const useLogger = () => useContext(LoggerContext);
+
+const LoggerProvider = ({ children, service }) => {
+ const [state, setState] = useState({
+ errors: [],
+ infos: [],
+ });
+
+ useEffect(() => {
+ const onErrorHandler = ({ error: errorObject, message }) => {
+ error({ error: errorObject, message });
+ };
+ window.addEventListener('error', onErrorHandler);
+ return () => {
+ window.removeEventListener('error', onErrorHandler);
+ };
+ }, []);
+
+ /**
+ * Logs an error
+ *
+ * @param {object} props { error, stack, message, displayOnConsole }
+ * @returns void
+ */
+ const error = ({
+ error = {},
+ stack = '',
+ message = '',
+ displayOnConsole = true,
+ }) => {
+ const errorObject = { error, stack, message, displayOnConsole };
+ setState(state => ({ ...state, errors: [...state.errors, errorObject] }));
+
+ if (displayOnConsole) {
+ console.error(error);
+ }
+ };
+
+ /**
+ * Logs an info
+ *
+ * @param {object} props { message, displayOnConsole }
+ * @returns void
+ */
+ const info = ({ message = '', displayOnConsole = true }) => {
+ setState(state => ({
+ ...state,
+ infos: state.infos.push({ message, displayOnConsole }),
+ }));
+
+ if (displayOnConsole) {
+ console.info(message);
+ }
+ };
+
+ /**
+ * Sets the implementation of a log service that can be used by extensions
+ *
+ * @returns void
+ */
+ useEffect(() => {
+ if (service) {
+ service.setServiceImplementation({ error, info });
+ }
+ }, [error, service, info]);
+
+ return {children};
+};
+
+/**
+ * Higher Order Component to use the log methods through a Class Component
+ *
+ * @returns
+ */
+export const withLogger = Component => {
+ return function WrappedComponent(props) {
+ const { error, info, state } = useLogger();
+ return ;
+ };
+};
+
+LoggerProvider.defaultProps = {
+ service: null,
+};
+
+LoggerProvider.propTypes = {
+ children: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.node),
+ PropTypes.node,
+ ]).isRequired,
+ service: PropTypes.shape({
+ setServiceImplementation: PropTypes.func,
+ }),
+};
+
+export default LoggerProvider;
+
+export const LogConsumer = LoggerContext.Consumer;
diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js
index e4ca06143..725f48109 100644
--- a/platform/ui/src/contextProviders/index.js
+++ b/platform/ui/src/contextProviders/index.js
@@ -18,3 +18,8 @@ export {
withDialog,
useDialog,
} from './DialogProvider.js';
+export {
+ default as LoggerProvider,
+ withLogger,
+ useLogger,
+} from './LoggerProvider.js';
diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js
index e95547357..fa79a831f 100644
--- a/platform/ui/src/index.js
+++ b/platform/ui/src/index.js
@@ -30,7 +30,7 @@ import {
AboutContent,
OHIFModal,
ErrorBoundary,
- ErrorPage
+ ErrorPage,
} from './components';
import { useDebounce, useMedia } from './hooks';
@@ -66,6 +66,9 @@ import {
ModalConsumer,
useModal,
withModal,
+ LoggerProvider,
+ withLogger,
+ useLogger,
} from './contextProviders';
export {
@@ -127,11 +130,14 @@ export {
useDialog,
ErrorBoundary,
ErrorPage,
+ LoggerProvider,
+ withLogger,
+ useLogger,
// Hooks
useDebounce,
useMedia,
// Utils
ViewerbaseDragDropContext,
asyncComponent,
- retryImport
+ retryImport,
};
diff --git a/platform/viewer/src/App.js b/platform/viewer/src/App.js
index 2bf95a8bf..603ee4e36 100644
--- a/platform/viewer/src/App.js
+++ b/platform/viewer/src/App.js
@@ -13,6 +13,7 @@ import {
ModalProvider,
DialogProvider,
OHIFModal,
+ LoggerProvider,
ErrorBoundary,
} from '@ohif/ui';
@@ -24,6 +25,7 @@ import {
UINotificationService,
UIModalService,
UIDialogService,
+ LoggerService,
MeasurementService,
utils,
redux as reduxOHIF,
@@ -141,6 +143,7 @@ class App extends Component {
UIModalService,
UIDialogService,
MeasurementService,
+ LoggerService,
]);
_initExtensions(
[...defaultExtensions, ...extensions],
@@ -164,6 +167,7 @@ class App extends Component {
UIDialogService,
UIModalService,
MeasurementService,
+ LoggerService,
} = servicesManager.services;
if (this._userManager) {
@@ -176,18 +180,20 @@ class App extends Component {
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -206,13 +212,18 @@ class App extends Component {
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js
index 862c251fb..2541caf08 100644
--- a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js
+++ b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js
@@ -4,7 +4,7 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { utils } from '@ohif/core';
-import { useSnackbarContext } from '@ohif/ui';
+import { useSnackbarContext, useLogger } from '@ohif/ui';
//
import ViewportPane from './ViewportPane.js';
import DefaultViewport from './DefaultViewport.js';
@@ -12,7 +12,7 @@ import EmptyViewport from './EmptyViewport.js';
const { loadAndCacheDerivedDisplaySets } = utils;
-const ViewportGrid = function (props) {
+const ViewportGrid = function(props) {
const {
activeViewportIndex,
availablePlugins,
@@ -36,14 +36,15 @@ const ViewportGrid = function (props) {
}
const snackbar = useSnackbarContext();
+ const logger = useLogger();
useEffect(() => {
if (isStudyLoaded) {
viewportData.forEach(displaySet => {
const promises = loadAndCacheDerivedDisplaySets(displaySet, studies);
-
promises.forEach(promise => {
promise.catch(error => {
+ logger.error({ error, message: error.message });
snackbar.show({
title: 'Error loading derived display set:',
message: error.message,