diff --git a/extensions/vtk/src/asyncComponent.js b/extensions/vtk/src/asyncComponent.js
deleted file mode 100644
index e8f344a35..000000000
--- a/extensions/vtk/src/asyncComponent.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * We use this component to leverage "Code Splitting"
- *
- * Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
- */
-
-import React, { Component } from 'react';
-
-export default function asyncComponent(importComponent) {
- class AsyncComponent extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- component: null,
- };
- }
-
- async componentDidMount() {
- // Add dynamically loaded component to state
- const { default: component } = await importComponent();
-
- this.setState({
- component: component,
- });
- }
-
- render() {
- const C = this.state.component;
-
- // Render the loaded component, or null
- return C ? : null;
- }
- }
-
- return AsyncComponent;
-}
diff --git a/extensions/vtk/src/index.js b/extensions/vtk/src/index.js
index e68552069..dde0d6ce0 100644
--- a/extensions/vtk/src/index.js
+++ b/extensions/vtk/src/index.js
@@ -1,5 +1,6 @@
import React from 'react';
-import asyncComponent from './asyncComponent.js';
+import { asyncComponent, retryImport } from '@ohif/ui';
+
import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js';
import withCommandsManager from './withCommandsManager.js';
@@ -8,7 +9,7 @@ import { version } from '../package.json';
// import loadLocales from './loadLocales';
const OHIFVTKViewport = asyncComponent(() =>
- import(/* webpackChunkName: "OHIFVTKViewport" */ './OHIFVTKViewport.js')
+ retryImport(() => import(/* webpackChunkName: "OHIFVTKViewport" */ './OHIFVTKViewport.js'))
);
const vtkExtension = {
diff --git a/platform/ui/src/components/errorPage/ErrorPage.css b/platform/ui/src/components/errorPage/ErrorPage.css
new file mode 100644
index 000000000..98c938bcc
--- /dev/null
+++ b/platform/ui/src/components/errorPage/ErrorPage.css
@@ -0,0 +1,24 @@
+.ErrorPage {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ color: var(--active-color);
+}
+
+.ErrorPage .error-container {
+ margin: 10px;
+ width: 50%;
+ height: 25%;
+ overflow: scroll;
+ border-radius: 15px;
+ border-color: var(--active-color);
+ border: 1px solid;
+ padding: 5px;
+}
+
+.ErrorPage .retry-icon {
+ cursor: pointer;
+}
diff --git a/platform/ui/src/components/errorPage/ErrorPage.js b/platform/ui/src/components/errorPage/ErrorPage.js
new file mode 100644
index 000000000..ce15f1274
--- /dev/null
+++ b/platform/ui/src/components/errorPage/ErrorPage.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Icon } from '@ohif/ui';
+
+import './ErrorPage.css';
+
+const ErrorPage = ({ error, title, description, onRetry }) => {
+ return (
+
+ {title &&
{title}
}
+
{description}
+
+ {error && (
+
+
{error.message}
+
{error.stack}
+
+ )}
+
+ );
+};
+
+ErrorPage.propTypes = {
+ error: PropTypes.object,
+ title: PropTypes.string,
+ description: PropTypes.string,
+ onRetry: PropTypes.func
+};
+
+ErrorPage.defaultProps = {
+ description: 'Oh snap, something went wrong, please try reloading',
+ onRetry: () => window.location.reload()
+};
+
+export default ErrorPage;
diff --git a/platform/ui/src/components/errorPage/index.js b/platform/ui/src/components/errorPage/index.js
new file mode 100644
index 000000000..0ad40318a
--- /dev/null
+++ b/platform/ui/src/components/errorPage/index.js
@@ -0,0 +1,2 @@
+import ErrorPage from './ErrorPage';
+export default ErrorPage;
diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js
index c3853be93..bc62197cb 100644
--- a/platform/ui/src/components/index.js
+++ b/platform/ui/src/components/index.js
@@ -16,6 +16,7 @@ import { SelectTree } from './selectTree';
import { SimpleDialog } from './simpleDialog';
import { OHIFModal } from './ohifModal';
import { ContextMenu } from './contextMenu';
+import ErrorPage from './errorPage';
import {
PageToolbar,
StudyList,
@@ -58,4 +59,5 @@ export {
Tooltip,
AboutContent,
OHIFModal,
+ ErrorPage
};
diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js
index 36237ac06..e95547357 100644
--- a/platform/ui/src/index.js
+++ b/platform/ui/src/index.js
@@ -29,7 +29,8 @@ import {
Tooltip,
AboutContent,
OHIFModal,
- ErrorBoundary
+ ErrorBoundary,
+ ErrorPage
} from './components';
import { useDebounce, useMedia } from './hooks';
@@ -53,6 +54,7 @@ import { ScrollableArea } from './ScrollableArea/ScrollableArea.js';
import Toolbar from './viewer/Toolbar.js';
import ToolbarButton from './viewer/ToolbarButton.js';
import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
+import { asyncComponent, retryImport } from './utils/asyncComponent';
import {
SnackbarProvider,
useSnackbarContext,
@@ -112,7 +114,6 @@ export {
ToolbarSection,
Tooltip,
AboutContent,
- ViewerbaseDragDropContext,
SnackbarProvider,
useSnackbarContext,
withSnackbar,
@@ -125,7 +126,12 @@ export {
withDialog,
useDialog,
ErrorBoundary,
+ ErrorPage,
// Hooks
useDebounce,
useMedia,
+ // Utils
+ ViewerbaseDragDropContext,
+ asyncComponent,
+ retryImport
};
diff --git a/platform/ui/src/utils/asyncComponent/asyncComponent.js b/platform/ui/src/utils/asyncComponent/asyncComponent.js
new file mode 100644
index 000000000..7e544883e
--- /dev/null
+++ b/platform/ui/src/utils/asyncComponent/asyncComponent.js
@@ -0,0 +1,62 @@
+import React, { useState, useEffect } from 'react';
+import { ErrorPage } from '@ohif/ui';
+
+export const retryImport = (fn, retriesLeft = 5, interval = 1000) =>
+ new Promise((resolve, reject) => {
+ fn().then(resolve).catch((error) => {
+ setTimeout(() => {
+ if (retriesLeft === 1) {
+ /* reject('maximum retries exceeded'); */
+ reject(error);
+ return;
+ }
+
+ /* Passing on "reject" is the important part */
+ retry(fn, retriesLeft - 1, interval).then(resolve, reject);
+ }, interval);
+ });
+ });
+
+const onError = (error, setState) => setState({ component: ErrorPage });
+
+/**
+ * We use this function to lazy load the import of a component to leverage 'Code Splitting'
+ * Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
+ */
+const asyncComponent = (importComponent, options = { onError }) => props => {
+ const [state, setState] = useState({ component: null });
+
+ const isFunction = item => typeof item === 'function';
+ const isChunkError = error => error.toString().indexOf('ChunkLoadError') > -1;
+
+ useEffect(() => {
+ const addDynamicallyLoadedComponentToState = async () => {
+ try {
+ const { default: component } = await importComponent();
+ setState({ component });
+ if (options.onLoaded && isFunction(options.onLoaded)) {
+ options.onLoaded(component);
+ }
+ } catch (error) {
+ console.error('[AsyncComponent] Failed to import chunk:', error);
+
+ if (options.onError && isFunction(options.onError)) {
+ options.onError(error, setState);
+ return;
+ }
+
+ if (isChunkError(error)) {
+ console.error('[AsyncComponent] Reloading due to chunk error');
+ window.location.reload();
+ }
+ }
+ };
+
+ addDynamicallyLoadedComponentToState();
+ }, []);
+
+ const Component = state.component;
+ return Component ? : null;
+};
+
+export default asyncComponent;
diff --git a/platform/ui/src/utils/asyncComponent/index.js b/platform/ui/src/utils/asyncComponent/index.js
new file mode 100644
index 000000000..abb28f96e
--- /dev/null
+++ b/platform/ui/src/utils/asyncComponent/index.js
@@ -0,0 +1,2 @@
+export { default as asyncComponent } from './asyncComponent';
+export { retryImport } from './asyncComponent';
diff --git a/platform/viewer/src/OHIFStandaloneViewer.js b/platform/viewer/src/OHIFStandaloneViewer.js
index 7bddfb9d6..ae3af51ad 100644
--- a/platform/viewer/src/OHIFStandaloneViewer.js
+++ b/platform/viewer/src/OHIFStandaloneViewer.js
@@ -5,9 +5,8 @@ import { Route, Switch } from 'react-router-dom';
import { NProgress } from '@tanem/react-nprogress';
import { CSSTransition } from 'react-transition-group';
import { connect } from 'react-redux';
-import { ViewerbaseDragDropContext, ErrorBoundary } from '@ohif/ui';
+import { ViewerbaseDragDropContext, ErrorBoundary, asyncComponent, retryImport } from '@ohif/ui';
import { SignoutCallbackComponent } from 'redux-oidc';
-import asyncComponent from './components/AsyncComponent.js';
import * as RoutesUtil from './routes/routesUtil';
import NotFound from './routes/NotFound.js';
@@ -18,7 +17,7 @@ import './theme-tide.css';
// Contexts
import AppContext from './context/AppContext';
const CallbackPage = asyncComponent(() =>
- import(/* webpackChunkName: "CallbackPage" */ './routes/CallbackPage.js')
+ retryImport(() => import(/* webpackChunkName: "CallbackPage" */ './routes/CallbackPage.js'))
);
class OHIFStandaloneViewer extends Component {
@@ -203,10 +202,10 @@ class OHIFStandaloneViewer extends Component {
{match === null ? (
<>>
) : (
-
-
-
- )}
+
+
+
+ )}
)}
diff --git a/platform/viewer/src/components/AsyncComponent.js b/platform/viewer/src/components/AsyncComponent.js
deleted file mode 100644
index e8f344a35..000000000
--- a/platform/viewer/src/components/AsyncComponent.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * We use this component to leverage "Code Splitting"
- *
- * Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
- */
-
-import React, { Component } from 'react';
-
-export default function asyncComponent(importComponent) {
- class AsyncComponent extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- component: null,
- };
- }
-
- async componentDidMount() {
- // Add dynamically loaded component to state
- const { default: component } = await importComponent();
-
- this.setState({
- component: component,
- });
- }
-
- render() {
- const C = this.state.component;
-
- // Render the loaded component, or null
- return C ? : null;
- }
- }
-
- return AsyncComponent;
-}
diff --git a/platform/viewer/src/routes/routesUtil.js b/platform/viewer/src/routes/routesUtil.js
index bea1c110d..8601edf10 100644
--- a/platform/viewer/src/routes/routesUtil.js
+++ b/platform/viewer/src/routes/routesUtil.js
@@ -1,32 +1,32 @@
-import asyncComponent from '../components/AsyncComponent.js';
-
+import { asyncComponent, retryImport } from '@ohif/ui';
import OHIF from '@ohif/core';
+
const { urlUtil: UrlUtil } = OHIF.utils;
// Dynamic Import Routes (CodeSplitting)
const IHEInvokeImageDisplay = asyncComponent(() =>
- import(
- /* webpackChunkName: "IHEInvokeImageDisplay" */ './IHEInvokeImageDisplay.js'
+ retryImport(() =>
+ import(/* webpackChunkName: "IHEInvokeImageDisplay" */ './IHEInvokeImageDisplay.js')
)
);
const ViewerRouting = asyncComponent(() =>
- import(/* webpackChunkName: "ViewerRouting" */ './ViewerRouting.js')
+ retryImport(() => import(/* webpackChunkName: "ViewerRouting" */ './ViewerRouting.js'))
);
const StudyListRouting = asyncComponent(() =>
- import(
+ retryImport(() => import(
/* webpackChunkName: "StudyListRouting" */ '../studylist/StudyListRouting.js'
- )
+ ))
);
const StandaloneRouting = asyncComponent(() =>
- import(
+ retryImport(() => import(
/* webpackChunkName: "ConnectedStandaloneRouting" */ '../connectedComponents/ConnectedStandaloneRouting.js'
- )
+ ))
);
const ViewerLocalFileData = asyncComponent(() =>
- import(
+ retryImport(() => import(
/* webpackChunkName: "ViewerLocalFileData" */ '../connectedComponents/ViewerLocalFileData.js'
- )
+ ))
);
const reload = () => window.location.reload();