Merge branch 'feat/v2-main' into feat/v2-main-docusaurus-netlify-monorepo
This commit is contained in:
commit
de412f2eef
61 files changed
+1980
-629
No files matched your search
@@ -110,6 +110,10 @@ module.exports = (env, argv) => {
|
||||
historyApiFallback: {
|
||||
disableDotRule: true,
|
||||
},
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -40,3 +40,7 @@
|
||||
no-cache,
|
||||
no-store,
|
||||
must-revalidate'''
|
||||
|
||||
# COMMENT: For sharedArrayBuffer, see https://developer.chrome.com/blog/enabling-shared-array-buffer/
|
||||
Cross-Origin-Embedder-Policy = "require-corp"
|
||||
Cross-Origin-Opener-Policy = "same-origin"
|
||||
@@ -62,17 +62,19 @@
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dotenv-webpack": "^1.7.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"history": "5.0.0",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"moment": "^2.24.0",
|
||||
"oidc-client": "1.11.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^6.12.1",
|
||||
"react-dropzone": "^10.1.7",
|
||||
"react-i18next": "^10.11.0",
|
||||
"react-resize-detector": "^4.2.0",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-router": "next",
|
||||
"react-router-dom": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@percy/cypress": "^2.3.0",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 202 B |
Binary file not shown.
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 352 B |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -39,6 +39,13 @@ window.config = {
|
||||
configuration: {},
|
||||
},
|
||||
],
|
||||
httpErrorHandler: error => {
|
||||
// This is 429 when rejected from the public idc sandbox too often.
|
||||
console.warn(error.status);
|
||||
|
||||
// Could use services manager here to bring up a dialog/modal if needed.
|
||||
console.warn('test, navigate to https://ohif.org/');
|
||||
},
|
||||
// whiteLabeling: {
|
||||
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
||||
// createLogoComponentFn: function (React) {
|
||||
|
||||
+42
-31
@@ -3,7 +3,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import Compose from './routes/Mode/Compose.js';
|
||||
|
||||
import {
|
||||
DialogProvider,
|
||||
Modal,
|
||||
@@ -13,22 +15,18 @@ import {
|
||||
ViewportDialogProvider,
|
||||
ViewportGridProvider,
|
||||
CineProvider,
|
||||
UserAuthenticationProvider,
|
||||
} from '@ohif/ui';
|
||||
// Viewer Project
|
||||
// TODO: Should this influence study list?
|
||||
import { AppConfigProvider } from '@state';
|
||||
import createRoutes from './routes';
|
||||
import appInit from './appInit.js';
|
||||
import history from './history'
|
||||
import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes.jsx';
|
||||
|
||||
// TODO: Temporarily for testing
|
||||
import '@ohif/mode-longitudinal';
|
||||
|
||||
/**
|
||||
* ENV Variable to determine routing behavior
|
||||
*/
|
||||
const OHIFRouter = Router
|
||||
|
||||
let commandsManager, extensionManager, servicesManager, hotkeysManager;
|
||||
|
||||
function App({ config, defaultExtensions }) {
|
||||
@@ -42,7 +40,8 @@ function App({ config, defaultExtensions }) {
|
||||
|
||||
// Set appConfig
|
||||
const appConfigState = init.appConfig;
|
||||
const { routerBasename, modes, dataSources } = appConfigState;
|
||||
const { routerBasename, modes, dataSources, oidc } = appConfigState;
|
||||
|
||||
// Use config to create routes
|
||||
const appRoutes = createRoutes({
|
||||
modes,
|
||||
@@ -50,38 +49,50 @@ function App({ config, defaultExtensions }) {
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
hotkeysManager,
|
||||
routerBasename,
|
||||
});
|
||||
const {
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
UINotificationService,
|
||||
UIViewportDialogService,
|
||||
ViewportGridService, // TODO: Should this be a "UI" Service?
|
||||
CineService
|
||||
ViewportGridService,
|
||||
CineService,
|
||||
UserAuthenticationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const providers = [
|
||||
[AppConfigProvider, { value: appConfigState }],
|
||||
[UserAuthenticationProvider, { service: UserAuthenticationService}],
|
||||
[I18nextProvider, { i18n }],
|
||||
[ThemeWrapper],
|
||||
[ViewportGridProvider, {service: ViewportGridService}],
|
||||
[ViewportDialogProvider, {service: UIViewportDialogService}],
|
||||
[CineProvider, {service: CineService}],
|
||||
[SnackbarProvider, {service: UINotificationService}],
|
||||
[DialogProvider, {service: UIDialogService}],
|
||||
[ModalProvider, {service: UIModalService, modal: Modal}],
|
||||
]
|
||||
const CombinedProviders = ({ children }) =>
|
||||
Compose({ components: providers, children });
|
||||
|
||||
let authRoutes = null;
|
||||
|
||||
if (oidc) {
|
||||
authRoutes = (<OpenIdConnectRoutes
|
||||
oidc={oidc}
|
||||
routerBasename={routerBasename}
|
||||
UserAuthenticationService={UserAuthenticationService}
|
||||
/>)
|
||||
}
|
||||
|
||||
return (
|
||||
<AppConfigProvider value={appConfigState}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<OHIFRouter basename={routerBasename} history={history}>
|
||||
<ThemeWrapper>
|
||||
<ViewportGridProvider service={ViewportGridService}>
|
||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
||||
<CineProvider service={CineService}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<DialogProvider service={UIDialogService}>
|
||||
<ModalProvider modal={Modal} service={UIModalService}>
|
||||
{appRoutes}
|
||||
</ModalProvider>
|
||||
</DialogProvider>
|
||||
</SnackbarProvider>
|
||||
</CineProvider>
|
||||
</ViewportDialogProvider>
|
||||
</ViewportGridProvider>
|
||||
</ThemeWrapper>
|
||||
</OHIFRouter>
|
||||
</I18nextProvider>
|
||||
</AppConfigProvider>
|
||||
<CombinedProviders>
|
||||
<BrowserRouter>
|
||||
{authRoutes}
|
||||
{appRoutes}
|
||||
</BrowserRouter>
|
||||
</CombinedProviders>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
CineService,
|
||||
UserAuthenticationService,
|
||||
errorHandler
|
||||
// utils,
|
||||
} from '@ohif/core';
|
||||
|
||||
// TODO -> this feels bad.
|
||||
|
||||
/**
|
||||
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
||||
* @param {object[]} defaultExtensions - array of extension objects
|
||||
@@ -60,8 +60,15 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
CineService,
|
||||
UserAuthenticationService,
|
||||
]);
|
||||
|
||||
errorHandler.getHTTPErrorHandler = () => {
|
||||
if (typeof appConfig.httpErrorHandler === 'function') {
|
||||
return appConfig.httpErrorHandler;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Example: [ext1, ext2, ext3]
|
||||
* Example2: [[ext1, config], ext2, [ext3, config]]
|
||||
@@ -75,13 +82,16 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
||||
// TODO: We no longer init webWorkers at app level
|
||||
// TODO: We no longer init the user Manager
|
||||
|
||||
if (!appConfig.modes) {
|
||||
throw new Error('No modes are defined! Check your app-config.js');
|
||||
}
|
||||
|
||||
// TODO: Remove this
|
||||
if (!appConfig.modes.length) {
|
||||
appConfig.modes.push(window.longitudinalMode);
|
||||
// appConfig.modes.push(window.segmentationMode);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
appConfig,
|
||||
commandsManager,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: replace with HistoryRouter https://github.com/ReactTraining/react-router/pull/7586
|
||||
import { createBrowserHistory, createHashHistory } from 'history';
|
||||
const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER);
|
||||
const router = useHashRouting ? createHashHistory() : createBrowserHistory();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function CallbackPage({ userManager, onRedirectSuccess }) {
|
||||
const onRedirectError = error => {
|
||||
throw new Error(error);
|
||||
};
|
||||
|
||||
userManager
|
||||
.signinRedirectCallback()
|
||||
.then(user => onRedirectSuccess(user))
|
||||
.catch(error => onRedirectError(error));
|
||||
|
||||
// todo: add i18n (or return null?)
|
||||
return <div>Redirecting...</div>;
|
||||
}
|
||||
|
||||
CallbackPage.propTypes = {
|
||||
userManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CallbackPage;
|
||||
@@ -3,8 +3,8 @@ import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
//
|
||||
import { useAppConfig } from '@state';
|
||||
import { extensionManager } from '../App.jsx';
|
||||
import { useParams, useLocation } from 'react-router';
|
||||
|
||||
/**
|
||||
* Uses route properties to determine the data source that should be passed
|
||||
@@ -15,8 +15,10 @@ import { extensionManager } from '../App.jsx';
|
||||
* @param {function} props.children - Layout Template React Component
|
||||
*/
|
||||
function DataSourceWrapper(props) {
|
||||
const [appConfig] = useAppConfig();
|
||||
const { children: LayoutTemplate, history, ...rest } = props;
|
||||
const { children: LayoutTemplate, ...rest } = props;
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
|
||||
// TODO: Fetch by type, name, etc?
|
||||
const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE];
|
||||
// TODO: Good usecase for flatmap?
|
||||
@@ -30,13 +32,10 @@ function DataSourceWrapper(props) {
|
||||
return acc.concat(mods);
|
||||
}, []);
|
||||
|
||||
// Grabbing first for now. This isn't hydrated yet, but we should
|
||||
// hydrate it somewhere based on config...
|
||||
// ~ default.js
|
||||
const firstAppConfigDataSource = appConfig.dataSources[0];
|
||||
const dataSourceConfig = firstAppConfigDataSource.configuration;
|
||||
const firstWebApiDataSource = webApiDataSources[0];
|
||||
const dataSource = firstWebApiDataSource.createDataSource(dataSourceConfig);
|
||||
// Grabbing first for now - should get active?
|
||||
const name = webApiDataSources[0].name;
|
||||
// TODO: Why does this return an array?
|
||||
const dataSource = extensionManager.getDataSources(name)[0]
|
||||
|
||||
// Route props --> studies.mapParams
|
||||
// mapParams --> studies.search
|
||||
@@ -55,19 +54,18 @@ function DataSourceWrapper(props) {
|
||||
|
||||
useEffect(() => {
|
||||
const queryFilterValues = _getQueryFilterValues(
|
||||
history.location.search,
|
||||
location.search,
|
||||
STUDIES_LIMIT
|
||||
);
|
||||
|
||||
// 204: no content
|
||||
async function getData() {
|
||||
setIsLoading(true);
|
||||
|
||||
const studies = await dataSource.query.studies.search(queryFilterValues);
|
||||
|
||||
setIsLoading(false);
|
||||
setData({
|
||||
studies,
|
||||
studies: studies || [],
|
||||
total: studies.length,
|
||||
resultsPerPage: queryFilterValues.resultsPerPage,
|
||||
pageNumber: queryFilterValues.pageNumber,
|
||||
@@ -99,14 +97,13 @@ function DataSourceWrapper(props) {
|
||||
console.warn(ex);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [history.location.search]);
|
||||
}, [location, params]);
|
||||
// queryFilterValues
|
||||
|
||||
// TODO: Better way to pass DataSource?
|
||||
return (
|
||||
<LayoutTemplate
|
||||
{...rest}
|
||||
history={history}
|
||||
data={data.studies}
|
||||
dataTotal={data.total}
|
||||
dataSource={dataSource}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { MODULE_TYPES } from '@ohif/core'
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import filesToStudies from './filesToStudies'
|
||||
|
||||
@@ -39,8 +39,8 @@ const getLoadButton = (onDrop, text, isDir) => {
|
||||
</Dropzone>)
|
||||
}
|
||||
|
||||
function Local(props) {
|
||||
const { history } = props
|
||||
function Local() {
|
||||
const navigate = useNavigate();
|
||||
const dropzoneRef = useRef()
|
||||
|
||||
// Initializing the dicom local dataSource
|
||||
@@ -61,7 +61,7 @@ function Local(props) {
|
||||
const onDrop = async (acceptedFiles) => {
|
||||
const studies = await filesToStudies(acceptedFiles, dataSource)
|
||||
// Todo: navigate to work list and let user select a mode
|
||||
history.push(`/viewer/dicomlocal?StudyInstanceUIDs=${studies[0]}`)
|
||||
navigate(`/viewer/dicomlocal?StudyInstanceUIDs=${studies[0]}`)
|
||||
}
|
||||
|
||||
// Set body style
|
||||
@@ -74,7 +74,7 @@ function Local(props) {
|
||||
|
||||
return (
|
||||
<Dropzone ref={dropzoneRef} onDrop={onDrop} noClick>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
{({ getRootProps }) => (
|
||||
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
|
||||
<div className="h-screen w-screen flex justify-center items-center ">
|
||||
<div className="py-8 px-8 mx-auto bg-secondary-dark shadow-md space-y-2 rounded-lg">
|
||||
@@ -107,10 +107,4 @@ function Local(props) {
|
||||
)
|
||||
}
|
||||
|
||||
Local.propTypes = {
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default Local
|
||||
@@ -10,14 +10,18 @@ export default function Compose(props) {
|
||||
const { components = [], children } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{components.reduceRight((acc, Comp) => {
|
||||
return <Comp>{acc}</Comp>;
|
||||
<React.Fragment>
|
||||
{components.reduceRight((acc, curr) => {
|
||||
const [Comp, props] = Array.isArray(curr)
|
||||
? [curr[0], curr[1]]
|
||||
: [curr, {}];
|
||||
return <Comp {...props}>{acc}</Comp>;
|
||||
}, children)}
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// https://juliuskoronci.medium.com/avoid-a-long-list-of-react-providers-c45a269d80c1
|
||||
Compose.propTypes = {
|
||||
components: PropTypes.array,
|
||||
children: PropTypes.node.isRequired,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useParams, useLocation } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
// TODO: DicomMetadataStore should be injected?
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
@@ -54,7 +54,6 @@ async function defaultRouteInit({
|
||||
}
|
||||
|
||||
export default function ModeRoute({
|
||||
location,
|
||||
mode,
|
||||
dataSourceName,
|
||||
extensionManager,
|
||||
@@ -62,6 +61,7 @@ export default function ModeRoute({
|
||||
hotkeysManager,
|
||||
}) {
|
||||
// Parse route params/querystring
|
||||
const location = useLocation();
|
||||
const query = useQuery();
|
||||
const params = useParams();
|
||||
|
||||
@@ -80,7 +80,9 @@ export default function ModeRoute({
|
||||
const {
|
||||
DisplaySetService,
|
||||
HangingProtocolService,
|
||||
UserAuthenticationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
|
||||
|
||||
if (dataSourceName === undefined) {
|
||||
@@ -96,6 +98,12 @@ export default function ModeRoute({
|
||||
// Only handling one route per mode for now
|
||||
const route = mode.routes[0];
|
||||
|
||||
const layoutTemplateRouteData = route.layoutTemplate({ location });
|
||||
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
|
||||
layoutTemplateRouteData.id
|
||||
);
|
||||
const LayoutComponent = layoutTemplateModuleEntry.component;
|
||||
|
||||
// For each extension, look up their context modules
|
||||
// TODO: move to extension manager.
|
||||
let contextModules = [];
|
||||
@@ -279,14 +287,6 @@ export default function ModeRoute({
|
||||
}
|
||||
|
||||
ModeRoute.propTypes = {
|
||||
// Ref: https://reacttraining.com/react-router/web/api/location
|
||||
location: PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
pathname: PropTypes.string.isRequired,
|
||||
search: PropTypes.string.isRequired,
|
||||
hash: PropTypes.string.isRequired,
|
||||
//state: PropTypes.object.isRequired,
|
||||
}),
|
||||
mode: PropTypes.object.isRequired,
|
||||
dataSourceName: PropTypes.string,
|
||||
extensionManager: PropTypes.object,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { Route } from "react-router-dom";
|
||||
import { useUserAuthentication } from "@ohif/ui";
|
||||
|
||||
export const PrivateRoute = ({ ...rest }) => {
|
||||
const [{ user, enabled }, userAuthenticationService] = useUserAuthentication();
|
||||
|
||||
if (enabled && !user) {
|
||||
return userAuthenticationService.handleUnauthenticated();
|
||||
}
|
||||
|
||||
return <Route {...rest}/>;
|
||||
}
|
||||
|
||||
export default PrivateRoute;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function SignoutCallbackComponent({ userManager }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onRedirectSuccess = (/* user */) => {
|
||||
const { pathname, search = '' } = JSON.parse(
|
||||
sessionStorage.getItem('ohif-redirect-to')
|
||||
);
|
||||
|
||||
navigate(`${pathname}?${search}`);
|
||||
};
|
||||
|
||||
const onRedirectError = error => {
|
||||
throw new Error(error);
|
||||
};
|
||||
|
||||
userManager
|
||||
.signoutRedirectCallback()
|
||||
.then(user => onRedirectSuccess(user))
|
||||
.catch(error => onRedirectError(error));
|
||||
|
||||
// todo: add i18n
|
||||
return <div>Redirecting...</div>;
|
||||
}
|
||||
|
||||
SignoutCallbackComponent.propTypes = {
|
||||
userManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default SignoutCallbackComponent;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
import qs from 'query-string';
|
||||
import isEqual from 'lodash.isequal';
|
||||
@@ -38,7 +38,6 @@ const seriesInStudiesMap = new Map();
|
||||
* - debounce `setFilterValues` (150ms?)
|
||||
*/
|
||||
function WorkList({
|
||||
history,
|
||||
data: studies,
|
||||
dataTotal: studiesTotal,
|
||||
isLoadingData,
|
||||
@@ -52,6 +51,7 @@ function WorkList({
|
||||
const [appConfig] = useAppConfig();
|
||||
// ~ Filters
|
||||
const query = useQuery();
|
||||
const navigate = useNavigate();
|
||||
const STUDIES_LIMIT = 101;
|
||||
const queryFilterValues = _getQueryFilterValues(query);
|
||||
const [filterValues, _setFilterValues] = useState({
|
||||
@@ -175,12 +175,14 @@ function WorkList({
|
||||
}
|
||||
});
|
||||
|
||||
history.push({
|
||||
const search = qs.stringify(queryString, {
|
||||
skipNull: true,
|
||||
skipEmptyString: true,
|
||||
});
|
||||
|
||||
navigate({
|
||||
pathname: '/',
|
||||
search: `?${qs.stringify(queryString, {
|
||||
skipNull: true,
|
||||
skipEmptyString: true,
|
||||
})}`,
|
||||
search: search ? `?${search}` : undefined,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedFilterValues]);
|
||||
@@ -441,9 +443,6 @@ function WorkList({
|
||||
}
|
||||
|
||||
WorkList.propTypes = {
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}).isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
dataSource: PropTypes.shape({
|
||||
query: PropTypes.object.isRequired,
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function buildModeRoutes({
|
||||
dataSources,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
hotkeysManager
|
||||
hotkeysManager,
|
||||
}) {
|
||||
const routes = [];
|
||||
|
||||
@@ -50,21 +50,19 @@ export default function buildModeRoutes({
|
||||
const path = `/${mode.id}/${dataSourceName}`;
|
||||
|
||||
// TODO move up.
|
||||
const component = ({ location }) => (
|
||||
<ModeRoute
|
||||
location={location}
|
||||
mode={mode}
|
||||
dataSourceName={dataSourceName}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
hotkeysManager={hotkeysManager}
|
||||
/>
|
||||
const children = () => (
|
||||
<ModeRoute
|
||||
mode={mode}
|
||||
dataSourceName={dataSourceName}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
hotkeysManager={hotkeysManager}
|
||||
/>
|
||||
);
|
||||
|
||||
routes.push({
|
||||
path,
|
||||
component,
|
||||
exact: true,
|
||||
children,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,21 +72,20 @@ export default function buildModeRoutes({
|
||||
const path = `/${mode.id}`;
|
||||
|
||||
// TODO move up.
|
||||
const component = ({ location }) => (
|
||||
<ModeRoute
|
||||
location={location}
|
||||
mode={mode}
|
||||
dataSourceName={defaultDataSourceName}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
hotkeysManager={hotkeysManager}
|
||||
/>
|
||||
const children = () => (
|
||||
<ModeRoute
|
||||
mode={mode}
|
||||
dataSourceName={defaultDataSourceName}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
hotkeysManager={hotkeysManager}
|
||||
/>
|
||||
);
|
||||
|
||||
routes.push({
|
||||
path,
|
||||
component,
|
||||
exact: true,
|
||||
children,
|
||||
private: true, // todo: all mode routes are private for now
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { ErrorBoundary } from '@ohif/ui';
|
||||
|
||||
// Route Components
|
||||
import DataSourceWrapper from './DataSourceWrapper';
|
||||
import WorkList from './WorkList';
|
||||
import Local from './Local';
|
||||
import NotFound from './NotFound';
|
||||
import buildModeRoutes from './buildModeRoutes';
|
||||
import { ErrorBoundary } from '@ohif/ui';
|
||||
import PrivateRoute from './PrivateRoute';
|
||||
|
||||
// TODO: Make these configurable
|
||||
// TODO: Include "routes" debug route if dev build
|
||||
@@ -14,14 +16,13 @@ const bakedInRoutes = [
|
||||
// WORK LIST
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: DataSourceWrapper,
|
||||
children: DataSourceWrapper,
|
||||
private: true,
|
||||
props: { children: WorkList },
|
||||
},
|
||||
{
|
||||
path: '/local',
|
||||
exact: true,
|
||||
component: Local,
|
||||
children: Local,
|
||||
},
|
||||
// NOT FOUND (404)
|
||||
{ component: NotFound },
|
||||
@@ -33,6 +34,7 @@ const createRoutes = ({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
hotkeysManager,
|
||||
routerBasename,
|
||||
}) => {
|
||||
const routes =
|
||||
buildModeRoutes({
|
||||
@@ -45,31 +47,44 @@ const createRoutes = ({
|
||||
|
||||
const allRoutes = [...routes, ...bakedInRoutes];
|
||||
|
||||
function RouteWithErrorBoundary({ route, ...rest }) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return (
|
||||
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
|
||||
<route.children
|
||||
{...rest}
|
||||
{...route.props}
|
||||
route={route}
|
||||
servicesManager={servicesManager}
|
||||
hotkeysManager={hotkeysManager}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
const { UserAuthenticationService } = servicesManager.services;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Routes basename={routerBasename}>
|
||||
{allRoutes.map((route, i) => {
|
||||
return (
|
||||
return route.private === true ? (
|
||||
<PrivateRoute
|
||||
key={i}
|
||||
path={route.path}
|
||||
handleUnauthenticated={
|
||||
UserAuthenticationService.handleUnauthenticated
|
||||
}
|
||||
element={<RouteWithErrorBoundary route={route} />}
|
||||
/>
|
||||
) : (
|
||||
<Route
|
||||
key={i}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
strict={route.strict}
|
||||
render={props => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
|
||||
<route.component
|
||||
{...props}
|
||||
{...route.props}
|
||||
route={route}
|
||||
servicesManager={servicesManager}
|
||||
hotkeysManager={hotkeysManager}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
element={<RouteWithErrorBoundary route={route} />}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Switch>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router';
|
||||
import CallbackPage from '../routes/CallbackPage';
|
||||
import SignoutCallbackComponent from '../routes/SignoutCallbackComponent';
|
||||
import getUserManagerForOpenIdConnectClient from './getUserManagerForOpenIdConnectClient.js';
|
||||
|
||||
function _isAbsoluteUrl(url) {
|
||||
return url.includes('http://') || url.includes('https://');
|
||||
}
|
||||
|
||||
function _makeAbsoluteIfNecessary(url, base_url) {
|
||||
if (_isAbsoluteUrl(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
/*
|
||||
* Make sure base_url and url are not duplicating slashes.
|
||||
*/
|
||||
if (base_url[base_url.length - 1] === '/') {
|
||||
base_url = base_url.slice(0, base_url.length - 1);
|
||||
}
|
||||
|
||||
return base_url + url;
|
||||
}
|
||||
|
||||
const initUserManager = (oidc, routerBasename) => {
|
||||
if (!oidc || !oidc.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstOpenIdClient = oidc[0];
|
||||
const { protocol, host } = window.location;
|
||||
const baseUri = `${protocol}//${host}${routerBasename}`;
|
||||
|
||||
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
|
||||
const silent_redirect_uri =
|
||||
firstOpenIdClient.silent_redirect_uri || '/silent-refresh.html';
|
||||
const post_logout_redirect_uri =
|
||||
firstOpenIdClient.post_logout_redirect_uri || '/';
|
||||
|
||||
const openIdConnectConfiguration = Object.assign({}, firstOpenIdClient, {
|
||||
redirect_uri: _makeAbsoluteIfNecessary(redirect_uri, baseUri),
|
||||
silent_redirect_uri: _makeAbsoluteIfNecessary(
|
||||
silent_redirect_uri,
|
||||
baseUri
|
||||
),
|
||||
post_logout_redirect_uri: _makeAbsoluteIfNecessary(
|
||||
post_logout_redirect_uri,
|
||||
baseUri
|
||||
),
|
||||
});
|
||||
|
||||
return getUserManagerForOpenIdConnectClient(openIdConnectConfiguration);
|
||||
}
|
||||
|
||||
function LoginComponent(userManager) {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const iss = queryParams.get('iss');
|
||||
const loginHint = queryParams.get('login_hint');
|
||||
const targetLinkUri = queryParams.get('target_link_uri');
|
||||
if (iss !== oidcAuthority) {
|
||||
console.error(
|
||||
'iss of /login does not match the oidc authority'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
userManager.removeUser().then(() => {
|
||||
if (targetLinkUri !== null) {
|
||||
const ohifRedirectTo = {
|
||||
pathname: new URL(targetLinkUri).pathname,
|
||||
};
|
||||
sessionStorage.setItem(
|
||||
'ohif-redirect-to',
|
||||
JSON.stringify(ohifRedirectTo)
|
||||
);
|
||||
} else {
|
||||
const ohifRedirectTo = {
|
||||
pathname: '/',
|
||||
};
|
||||
sessionStorage.setItem(
|
||||
'ohif-redirect-to',
|
||||
JSON.stringify(ohifRedirectTo)
|
||||
);
|
||||
}
|
||||
|
||||
if (loginHint !== null) {
|
||||
userManager.signinRedirect({ login_hint: loginHint });
|
||||
} else {
|
||||
userManager.signinRedirect();
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function OpenIdConnectRoutes({
|
||||
oidc,
|
||||
routerBasename,
|
||||
UserAuthenticationService
|
||||
}) {
|
||||
const userManager = initUserManager(oidc, routerBasename);
|
||||
|
||||
const getAuthorizationHeader = () => {
|
||||
const user = UserAuthenticationService.getUser();
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${user.access_token}`
|
||||
};
|
||||
}
|
||||
|
||||
const handleUnauthenticated = () => {
|
||||
userManager.signinRedirect()
|
||||
|
||||
// return null because this is used in a react component
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
UserAuthenticationService.set({ enabled: true });
|
||||
|
||||
UserAuthenticationService.setServiceImplementation({
|
||||
getAuthorizationHeader,
|
||||
handleUnauthenticated
|
||||
});
|
||||
}, [])
|
||||
|
||||
const oidcAuthority = oidc[0].authority;
|
||||
|
||||
const location = useLocation();
|
||||
const { pathname, search } = location;
|
||||
|
||||
const redirect_uri = new URL(userManager.settings._redirect_uri).pathname//.replace(routerBasename,'')
|
||||
const silent_refresh_uri = new URL(userManager.settings._silent_redirect_uri).pathname//.replace(routerBasename,'')
|
||||
const post_logout_redirect_uri = new URL(userManager.settings._post_logout_redirect_uri).pathname//.replace(routerBasename,'');
|
||||
|
||||
// const pathnameRelative = pathname.replace(routerBasename,'');
|
||||
|
||||
if (pathname !== redirect_uri) {
|
||||
sessionStorage.setItem(
|
||||
'ohif-redirect-to',
|
||||
JSON.stringify({ pathname, search })
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes basename={routerBasename}>
|
||||
<Route
|
||||
path={silent_refresh_uri}
|
||||
onEnter={window.location.reload}
|
||||
/>
|
||||
<Route
|
||||
path={post_logout_redirect_uri}
|
||||
element={
|
||||
<SignoutCallbackComponent
|
||||
userManager={userManager}
|
||||
successCallback={() => console.log('Signout successful')}
|
||||
errorCallback={error => {
|
||||
console.warn(error);
|
||||
console.warn('Signout failed');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={redirect_uri}
|
||||
element={<CallbackPage userManager={userManager} onRedirectSuccess={(user) => {
|
||||
const { pathname, search = '' } = JSON.parse(
|
||||
sessionStorage.getItem('ohif-redirect-to')
|
||||
);
|
||||
|
||||
UserAuthenticationService.setUser(user);
|
||||
|
||||
navigate({
|
||||
pathname,
|
||||
search
|
||||
})
|
||||
}}/>}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={<LoginComponent userManager={userManager} oidcAuthority={oidcAuthority}/>}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default OpenIdConnectRoutes;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { UserManager } from 'oidc-client';
|
||||
|
||||
/**
|
||||
* Creates a userManager from oidcSettings
|
||||
* LINK: https://github.com/IdentityModel/oidc-client-js/wiki#configuration
|
||||
*
|
||||
* @param {Object} oidcSettings
|
||||
* @param {string} oidcSettings.authServerUrl,
|
||||
* @param {string} oidcSettings.clientId,
|
||||
* @param {string} oidcSettings.authRedirectUri,
|
||||
* @param {string} oidcSettings.postLogoutRedirectUri,
|
||||
* @param {string} oidcSettings.responseType,
|
||||
* @param {string} oidcSettings.extraQueryParams,
|
||||
*/
|
||||
export default function getUserManagerForOpenIdConnectClient(oidcSettings) {
|
||||
if (!oidcSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
...oidcSettings,
|
||||
automaticSilentRenew: true,
|
||||
revokeAccessTokenOnSignout: true,
|
||||
filterProtocolClaims: true,
|
||||
loadUserInfo: true,
|
||||
};
|
||||
|
||||
const userManager = new UserManager(settings);
|
||||
|
||||
return userManager;
|
||||
}
|
||||
Reference in new issue
Block a user