Merge branch 'feat/v2-main' into feat/v2-main-docusaurus-netlify-monorepo

This commit is contained in:
Erik Ziegler 2021-07-08 10:41:49 +02:00
commit de412f2eef
61 changed files with 1980 additions and 629 deletions

View File

@ -21,7 +21,7 @@ if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
echo "Updating config..."
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
sed -i -e "s+https://healthcare.googleapis.com/v1beta1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
sed -i -e "s+https://healthcare.googleapis.com/v1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js

View File

@ -26,6 +26,7 @@
"autoFix": true
}
],
"jest.autoRun": "off",
"prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {},

View File

@ -101,7 +101,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
),
}),
new UnusedFilesWebpackPlugin({
failOnUnused: true,
failOnUnused: false,
globOptions: {
ignore: [
"node_modules/**/*",

View File

@ -21,28 +21,37 @@
# Stage 1: Build the application
# docker build -t ohif/viewer:latest .
FROM node:14.3.0-slim as builder
FROM node:14.3.0-slim as json-copier
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
# Copy Files
COPY .docker /usr/src/app/.docker
COPY .webpack /usr/src/app/.webpack
COPY ["package.json", "yarn.lock", "./"]
COPY extensions /usr/src/app/extensions
COPY modes /usr/src/app/modes
COPY platform /usr/src/app/platform
COPY .browserslistrc /usr/src/app/.browserslistrc
COPY aliases.config.js /usr/src/app/aliases.config.js
COPY babel.config.js /usr/src/app/babel.config.js
COPY lerna.json /usr/src/app/lerna.json
COPY package.json /usr/src/app/package.json
COPY postcss.config.js /usr/src/app/postcss.config.js
COPY yarn.lock /usr/src/app/yarn.lock
# Find and remove non-package.json files
RUN find extensions \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
RUN find modes \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
# Copy Files
FROM node:14.3.0-slim as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY --from=json-copier /usr/src/app .
# Run the install before copying the rest of the files
RUN yarn config set workspaces-experimental true
RUN yarn install
RUN yarn install --frozen-lockfile
COPY . .
# To restore workspaces symlinks
RUN yarn install --frozen-lockfile
ENV PATH /usr/src/app/node_modules/.bin:$PATH
ENV QUICK_BUILD true
@ -51,7 +60,7 @@ ENV QUICK_BUILD true
RUN yarn run build
# Stage 2: Bundle the built application into a Docker container
# Stage 3: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginx:1.15.5-alpine
RUN apk add --no-cache bash

View File

@ -57,7 +57,7 @@ function ExampleContextProvider({ children }) {
return (
<ExampleContext.Provider value={{ example: 'value' }}>
{children}
</TrackedMeasurementsContext.Provider>
</ExampleContext.Provider>
);
}

View File

@ -63,9 +63,8 @@ All use the `ACTIVE_VIEWPORT::CORNERSTONE` context.
## Viewport Module
Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
and is connected the redux store. This module is the most prone to change as we
hammer out our Viewport interface.
Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport].
This module is the most prone to change as we hammer out our Viewport interface.
## Tool Configuration
@ -87,10 +86,12 @@ Tools can be configured through extension configuration using the tools key:
## Annotate Tools Configuration
*We currently support one property for annotation tools.*
_We currently support one property for annotation tools._
### Hide handles
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
This extension configuration allows you to toggle on/off handle rendering for
all annotate tools:
```js
...
@ -119,3 +120,4 @@ This extension configuration allows you to toggle on/off handle rendering for al
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
[cornerstone]: https://github.com/cornerstonejs/cornerstone
<!-- prettier-ignore-end -->
```

View File

@ -5,7 +5,7 @@ import csTools from 'cornerstone-tools';
import merge from 'lodash.merge';
import getTools, { toolsGroupedByType } from './utils/getTools.js';
import initCornerstoneTools from './initCornerstoneTools.js';
import './initWADOImageLoader.js';
import initWADOImageLoader from './initWADOImageLoader.js';
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import { setEnabledElement } from './state';
@ -69,6 +69,7 @@ export default function init({
MeasurementService,
DisplaySetService,
ToolBarService,
UserAuthenticationService,
} = servicesManager.services;
const tools = getTools();
@ -275,6 +276,8 @@ export default function init({
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
initWADOImageLoader(UserAuthenticationService);
// ~~
const defaultCsToolsConfig = csToolsConfig || {
globalToolSyncEnabled: false, // hold on to your pants!

View File

@ -1,18 +1,45 @@
import cornerstone from 'cornerstone-core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import dicomParser from 'dicom-parser';
import { errorHandler } from '@ohif/core'
//import { initWebWorkers } from './utils/index.js';
let initialized = false;
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
function initWebWorkers() {
const config = {
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
startWebWorkersOnDemand: true,
taskConfiguration: {
decodeTask: {
initializeCodecsOnStartup: false,
usePDFJS: false,
strict: false,
},
},
};
cornerstoneWADOImageLoader.configure({
beforeSend: function(xhr) {
/*const headers = OHIF.DICOMWeb.getAuthorizationHeader();
if (!initialized) {
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
initialized = true;
}
}
if (headers.Authorization) {
xhr.setRequestHeader('Authorization', headers.Authorization);
}*/
},
});
export default function initWADOImageLoader(UserAuthenticationService) {
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
cornerstoneWADOImageLoader.configure({
beforeSend: function(xhr) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers && headers.Authorization) {
xhr.setRequestHeader('Authorization', headers.Authorization);
}
},
errorInterceptor: error => {
errorHandler.getHTTPErrorHandler(error)
},
});
initWebWorkers();
}

View File

@ -1,22 +0,0 @@
import { redux } from '@ohif/core';
const { setLayout } = redux.actions;
/**
* Update the current layout with a simple Cornerstone one
*
* @return void
*/
const setCornerstoneLayout = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
const action = setLayout(layout);
window.store.dispatch(action);
};
export default setCornerstoneLayout;

View File

@ -7,7 +7,7 @@ import {
processSeriesResults,
} from './qido.js';
import dcm4cheeReject from './dcm4cheeReject';
import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core';
import { DicomMetadataStore, IWebApiDataSource, utils, errorHandler } from '@ohif/core';
import getImageId from './utils/getImageId';
import dcmjs from 'dcmjs';
@ -38,7 +38,7 @@ const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
* @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
*/
function createDicomWebApi(dicomWebConfig) {
function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
const {
qidoRoot,
wadoRoot,
@ -46,15 +46,20 @@ function createDicomWebApi(dicomWebConfig) {
supportsFuzzyMatching,
supportsWildcard,
supportsReject,
requestOptions,
} = dicomWebConfig;
const qidoConfig = {
url: qidoRoot,
// headers: DICOMWeb.getAuthorizationHeader(server),
headers: UserAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
};
const wadoConfig = {
url: wadoRoot,
headers: UserAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
};
// TODO -> Two clients sucks, but its better than 1000.
@ -78,7 +83,12 @@ function createDicomWebApi(dicomWebConfig) {
query: {
studies: {
mapParams: mapParams.bind(),
search: async function (origParams) {
search: async function(origParams) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
mapParams(origParams, {
supportsFuzzyMatching,
@ -98,7 +108,12 @@ function createDicomWebApi(dicomWebConfig) {
},
series: {
// mapParams: mapParams.bind(),
search: async function (studyInstanceUid) {
search: async function(studyInstanceUid) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
const results = await seriesInStudy(
qidoDicomWebClient,
studyInstanceUid
@ -109,14 +124,20 @@ function createDicomWebApi(dicomWebConfig) {
// processResults: processResults.bind(),
},
instances: {
search: (studyInstanceUid, queryParameters) =>
search: (studyInstanceUid, queryParameters) => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
qidoSearch.call(
undefined,
qidoDicomWebClient,
studyInstanceUid,
null,
queryParameters
),
);
},
},
},
retrieve: {
@ -125,6 +146,11 @@ function createDicomWebApi(dicomWebConfig) {
// Conduct query, return a promise like others
// Await this call and add to DicomMetadataStore after receiving result
metadata: (queryParams, callback) => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true);
StudyInstanceUIDs = urlUtil.paramString.parseParam(StudyInstanceUIDs);
@ -164,6 +190,11 @@ function createDicomWebApi(dicomWebConfig) {
},
store: {
dicom: async dataset => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
const meta = {
FileMetaInformationVersion:
dataset._meta.FileMetaInformationVersion.Value,
@ -196,6 +227,11 @@ function createDicomWebApi(dicomWebConfig) {
sortFunction,
madeInClient = false,
} = {}) => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
if (!StudyInstanceUID) {
throw new Error(
'Unable to query for SeriesMetadata without StudyInstanceUID'

View File

@ -2,7 +2,9 @@ async function getStudiesForPatientByStudyInstanceUID(
dataSource,
StudyInstanceUID
) {
if (StudyInstanceUID === undefined) return;
if (StudyInstanceUID === undefined) {
return;
}
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore?

View File

@ -11,6 +11,8 @@ import {
} from '@ohif/ui';
import i18n from '@ohif/i18n';
import { hotkeys } from '@ohif/core';
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
@ -115,7 +117,11 @@ function ViewerLayout({
currentLanguage: currentLanguage(),
availableLanguages,
defaultLanguage,
onCancel: hide,
onCancel: () => {
hotkeys.stopRecord();
hotkeys.unpause();
hide();
},
onSubmit: ({ hotkeyDefinitions, language }) => {
i18n.changeLanguage(language.value);
hotkeysManager.setHotkeys(hotkeyDefinitions);

View File

@ -1,37 +0,0 @@
import React, { useState } from 'react';
const HelloWorldContext = React.createContext({
message: 'HelloWorldContextTesting',
setMessage: () => { },
});
HelloWorldContext.displayName = 'HelloWorldContext';
function HelloWorldContextProvider({ children }) {
const [message, setMessage] = useState('HelloWorldContextTesting');
return (
<HelloWorldContext.Provider
value={{
message,
setMessage,
}}
>
{children}
</HelloWorldContext.Provider>
);
}
function getContextModule() {
return [
{
name: 'HelloWorldContext',
context: HelloWorldContext,
provider: HelloWorldContextProvider,
},
];
}
export { HelloWorldContext };
export default getContextModule;

View File

@ -1,4 +1,3 @@
import getContextModule from './getContextModule.js';
import getDataSourcesModule from './getDataSourcesModule.js';
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
import getPanelModule from './getPanelModule.js';
@ -13,7 +12,6 @@ export default {
* Only required property. Should be a unique value across all extensions.
*/
id,
getContextModule,
getDataSourcesModule,
getHangingProtocolModule,
getLayoutTemplateModule,

View File

@ -2,7 +2,9 @@ async function getStudiesForPatientByStudyInstanceUID(
dataSource,
StudyInstanceUID
) {
if (StudyInstanceUID === undefined) return;
if (StudyInstanceUID === undefined) {
return;
}
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore?

View File

@ -41,6 +41,7 @@
"dicomweb-client": "^0.6.0",
"isomorphic-base64": "^1.0.2",
"lodash.merge": "^4.6.1",
"lodash.clonedeep": "^4.5.0",
"moment": "^2.24.0",
"mousetrap": "^1.6.3",
"query-string": "^6.14.0",

View File

@ -10,7 +10,7 @@ import user from '../user';
* @param {string|function} [server.requestOptions.auth]
* @returns {Object} { Authorization }
*/
export default function getAuthorizationHeader({ requestOptions } = {}) {
export default function getAuthorizationHeader({ requestOptions } = {}, user) {
const headers = {};
// Check for OHIF.user since this can also be run on the server

View File

@ -64,11 +64,11 @@ describe('getAuthorizationHeader', () => {
it('should return an Authorization with accessToken when server is not defined and there is an accessToken', () => {
user.getAccessToken.mockImplementationOnce(() => 'MOCKED_TOKEN');
const authentication = getAuthorizationHeader({});
const exptecteHeaderBasedOnUserAccessToekn = {
const authentication = getAuthorizationHeader({}, user);
const expectedHeaderBasedOnUserAccessToken = {
Authorization: 'Bearer MOCKED_TOKEN',
};
expect(authentication).toEqual(exptecteHeaderBasedOnUserAccessToekn);
expect(authentication).toEqual(expectedHeaderBasedOnUserAccessToken);
});
});

View File

@ -439,7 +439,7 @@ class MetadataProvider {
_getUIDsFromImageID(imageId) {
if (imageId.includes('wadors:')) {
const strippedImageId = imageId.split('studies/')[1];
const strippedImageId = imageId.split('/studies/')[1];
const splitImageId = strippedImageId.split('/');
return {

View File

@ -186,6 +186,7 @@ export default class ExtensionManager {
case MODULE_TYPES.SOP_CLASS_HANDLER:
case MODULE_TYPES.CONTEXT:
case MODULE_TYPES.LAYOUT_TEMPLATE:
case MODULE_TYPES.HANGING_PROTOCOL:
// Default for most extension points,
// Just adds each entry ready for consumption by mode.
extensionModule.forEach(element => {
@ -194,13 +195,6 @@ export default class ExtensionManager {
] = element;
});
break;
case MODULE_TYPES.HANGING_PROTOCOL:
extensionModule.forEach(element => {
this.modulesMap[
`${extensionId}.${moduleType}.${element.name}`
] = element;
});
break;
default:
throw new Error(`Module type invalid: ${moduleType}`);
}
@ -277,13 +271,16 @@ export default class ExtensionManager {
};
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
const { UserAuthenticationService } = this._servicesManager.services;
extensionModule.forEach(element => {
const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`;
dataSources.forEach(dataSource => {
if (dataSource.namespace === namespace) {
const dataSourceInstance = element.createDataSource(
dataSource.configuration
dataSource.configuration,
UserAuthenticationService
);
if (this.dataSourceMap[dataSource.sourceName]) {
@ -303,27 +300,9 @@ export default class ExtensionManager {
}
_initHangingProtocolModule(extensionModule, extensionId) {
extensionModule.forEach(element => {
const namespace = `${extensionId}.${MODULE_TYPES.HANGING_PROTOCOL}.${element.name}`;
dataSources.forEach(dataSource => {
if (dataSource.namespace === namespace) {
const dataSourceInstance = element.createDataSource(
dataSource.configuration
);
if (this.dataSourceMap[dataSource.sourceName]) {
this.dataSourceMap[dataSource.sourceName].push(dataSourceInstance);
} else {
this.dataSourceMap[dataSource.sourceName] = [dataSourceInstance];
}
}
});
});
extensionModule.forEach(element => {
this.modulesMap[
`${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`
`${extensionId}.${MODULE_TYPES.HANGING_PROTOCOL}.${element.name}`
] = element;
});
}

View File

@ -15,6 +15,10 @@ describe('ExtensionManager.js', () => {
};
servicesManager = {
registerService: jest.fn(),
services: {
// Required for DataSource Module initiation
UserAuthenticationService: jest.fn(),
},
};
appConfig = {
testing: true,
@ -100,7 +104,6 @@ describe('ExtensionManager.js', () => {
extensionManager.registerExtension(undefinedExtension);
}).toThrow('Attempting to register a null/undefined extension.');
expect(() => {
extensionManager.registerExtension(nullExtension);
}).toThrow('Attempting to register a null/undefined extension.');
@ -185,7 +188,7 @@ describe('ExtensionManager.js', () => {
hotkeysManager: undefined,
appConfig,
configuration: extensionConfiguration,
extensionManager
extensionManager,
});
}
});
@ -220,7 +223,7 @@ describe('ExtensionManager.js', () => {
},
getContextModule: () => {
return [{}];
}
},
};
extensionManager.registerExtension(extension);

View File

@ -26,6 +26,7 @@ import {
ViewportGridService,
HangingProtocolService,
pubSubServiceInterface,
UserAuthenticationService,
} from './services';
import IWebApiDataSource from './DataSources/IWebApiDataSource';
@ -66,6 +67,7 @@ const OHIF = {
ToolBarService, // TODO: TYPO
ViewportGridService,
HangingProtocolService,
UserAuthenticationService,
IWebApiDataSource,
DicomMetadataStore,
pubSubServiceInterface,
@ -101,6 +103,7 @@ export {
ToolBarService,
ViewportGridService,
HangingProtocolService,
UserAuthenticationService,
IWebApiDataSource,
DicomMetadataStore,
pubSubServiceInterface,

View File

@ -34,9 +34,10 @@ describe('Top level exports', () => {
'ToolBarService',
'ViewportGridService',
'HangingProtocolService',
'UserAuthenticationService',
'IWebApiDataSource',
'DicomMetadataStore',
'pubSubServiceInterface'
'pubSubServiceInterface',
].sort();
const exports = Object.keys(OHIF).sort();

View File

@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash';
import cloneDeep from 'lodash.clonedeep';
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
import sortBy from '../../utils/sortBy.js';
import ProtocolEngine from './ProtocolEngine';

View File

@ -56,7 +56,7 @@ export default class ToolBarService {
*/
recordInteraction(interaction) {
const commandsManager = this._commandsManager;
const { groupId, itemId, interactionType } = interaction;
const { groupId, itemId, interactionType, commandName, commandOptions } = interaction;
switch (interactionType) {
case 'action': {
@ -66,7 +66,7 @@ export default class ToolBarService {
this.state.primaryToolId = itemId;
// TODO: Force run this for all contexts? Even inactive?
// or... They'll just detect primaryToolId when they spin up and apply...
commandsManager.runCommand('setToolActive', interaction.commandOptions);
commandsManager.runCommand('setToolActive', commandOptions);
break;
}
case 'toggle': {
@ -74,7 +74,9 @@ export default class ToolBarService {
this.state.toggles[itemId] === undefined
? true
: !this.state.toggles[itemId];
interaction.commandOptions.toggledState = this.state.toggles[itemId];
if (commandOptions) {
commandOptions.toggledState = this.state.toggles[itemId];
}
break;
}
default:
@ -86,11 +88,8 @@ export default class ToolBarService {
// NOTE: Should probably just do this for tools as well?
// But would be nice if we could enforce at least the command name?
let unsubscribe;
if (interaction.commandName) {
unsubscribe = commandsManager.runCommand(
interaction.commandName,
interaction.commandOptions
);
if (commandName) {
unsubscribe = commandsManager.runCommand(commandName, commandOptions);
}
// Storing the unsubscribe for later reseting

View File

@ -0,0 +1,92 @@
const name = 'UserAuthenticationService';
const publicAPI = {
name,
getState: _getState,
setUser: _setUser,
getUser: _getUser,
getAuthorizationHeader: _getAuthorizationHeader,
handleUnauthenticated: _handleUnauthenticated,
setServiceImplementation,
reset: _reset,
set: _set,
};
const serviceImplementation = {
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
_setUser: () => console.warn('_setUser() NOT IMPLEMENTED'),
_getUser: () => console.warn('_setUser() NOT IMPLEMENTED'),
_getAuthorizationHeader: () =>
console.warn('_getAuthorizationHeader() NOT IMPLEMENTED'),
_handleUnauthenticated: () =>
console.warn('_handleUnauthenticated() NOT IMPLEMENTED'),
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
_set: () => console.warn('set() NOT IMPLEMENTED'),
};
function _getState() {
return serviceImplementation._getState();
}
function _setUser(user) {
return serviceImplementation._setUser(user);
}
function _getUser() {
return serviceImplementation._getUser();
}
function _getAuthorizationHeader() {
return serviceImplementation._getAuthorizationHeader();
}
function _handleUnauthenticated() {
return serviceImplementation._handleUnauthenticated();
}
function _set(state) {
return serviceImplementation._set(state);
}
function _reset() {
return serviceImplementation._reset({});
}
function setServiceImplementation({
getState: getStateImplementation,
setUser: setUserImplementation,
getUser: getUserImplementation,
getAuthorizationHeader: getAuthorizationHeaderImplementation,
handleUnauthenticated: handleUnauthenticatedImplementation,
reset: resetImplementation,
set: setImplementation,
}) {
if (getStateImplementation) {
serviceImplementation._getState = getStateImplementation;
}
if (setUserImplementation) {
serviceImplementation._setUser = setUserImplementation;
}
if (getUserImplementation) {
serviceImplementation._getUser = getUserImplementation;
}
if (getAuthorizationHeaderImplementation) {
serviceImplementation._getAuthorizationHeader = getAuthorizationHeaderImplementation;
}
if (handleUnauthenticatedImplementation) {
serviceImplementation._handleUnauthenticated = handleUnauthenticatedImplementation;
}
if (resetImplementation) {
serviceImplementation._reset = resetImplementation;
}
if (setImplementation) {
serviceImplementation._set = setImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};

View File

@ -0,0 +1,3 @@
import UserAuthenticationService from './UserAuthenticationService';
export default UserAuthenticationService;

View File

@ -11,6 +11,7 @@ import ViewportGridService from './ViewportGridService';
import CineService from './CineService';
import HangingProtocolService from './HangingProtocolService';
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
import UserAuthenticationService from './UserAuthenticationService';
export {
MeasurementService,
@ -26,4 +27,5 @@ export {
HangingProtocolService,
CineService,
pubSubServiceInterface,
UserAuthenticationService,
};

View File

@ -1,41 +1,28 @@
"use strict";
/**
* This extension allows you to record a sequence using Mousetrap.
* {@link https://craig.is/killing/mice}
*
* @author Dan Tao <daniel.tao@gmail.com>
*/
module.exports = function (Mousetrap, options) {
/**
* the default configurations, we merge with user options
*
* @type {Array}
*/
var _config = Object.assign({
timeout: 1000
}, options);
export default function (Mousetrap) {
/**
* the sequence currently being recorded
*
* @type {Array}
*/
var _recordedSequence = [],
/**
* a callback to invoke after recording a sequence
*
* @type {Function|null}
*/
_recordedSequenceCallback = null,
/**
* a list of all of the keys currently held down
*
* @type {Array}
*/
_currentRecordedKeys = [],
/**
* temporary state where we remember if we've already captured a
* character key in the current combo
@ -43,28 +30,19 @@ module.exports = function (Mousetrap, options) {
* @type {boolean}
*/
_recordedCharacterKey = false,
/**
* a handle for the timer of the current recording
*
* @type {null|number}
*/
_recordTimer = null,
/**
* the original handleKey method to override when Mousetrap.record() is
* called
*
* @type {Function}
*/
_origHandleKey = Mousetrap.prototype.handleKey,
/**
* the timeout that timer will wait for a key
*
* @type {number}
*/
_recordTimeout = _config.timeout;
_origHandleKey = Mousetrap.prototype.handleKey;
/**
* handles a character key event
@ -83,19 +61,19 @@ module.exports = function (Mousetrap, options) {
}
// remember this character if we're currently recording a sequence
if (e.type === 'keydown') {
if (e.type == 'keydown') {
if (character.length === 1 && _recordedCharacterKey) {
_recordCurrentCombo();
}
for (var i = 0; i < modifiers.length; ++i) {
for (let i = 0; i < modifiers.length; ++i) {
_recordKey(modifiers[i]);
}
_recordKey(character);
// once a key is released, all keys that were held down at the time
// count as a keypress
} else if (e.type === 'keyup' && _currentRecordedKeys.length > 0) {
} else if (e.type == 'keyup' && _currentRecordedKeys.length > 0) {
_recordCurrentCombo();
}
}
@ -107,10 +85,8 @@ module.exports = function (Mousetrap, options) {
* @returns void
*/
function _recordKey(key) {
var i;
// one-off implementation of Array.indexOf, since IE6-9 don't support it
for (i = 0; i < _currentRecordedKeys.length; ++i) {
for (let i = 0; i < _currentRecordedKeys.length; ++i) {
if (_currentRecordedKeys[i] === key) {
return;
}
@ -133,7 +109,7 @@ module.exports = function (Mousetrap, options) {
_recordedSequence.push(_currentRecordedKeys);
_currentRecordedKeys = [];
_recordedCharacterKey = false;
_restartRecordTimer();
_finishRecording();
}
/**
@ -146,10 +122,8 @@ module.exports = function (Mousetrap, options) {
* @returns void
*/
function _normalizeSequence(sequence) {
var i;
for (i = 0; i < sequence.length; ++i) {
sequence[i].sort(function(x, y) {
for (let i = 0; i < sequence.length; ++i) {
sequence[i].sort(function (x, y) {
// modifier keys always come first, in alphabetical order
if (x.length > 1 && y.length === 1) {
return -1;
@ -194,7 +168,7 @@ module.exports = function (Mousetrap, options) {
*/
function _restartRecordTimer() {
clearTimeout(_recordTimer);
_recordTimer = setTimeout(_finishRecording, _recordTimeout);
_recordTimer = setTimeout(_finishRecording, 1000);
}
/**
@ -204,27 +178,41 @@ module.exports = function (Mousetrap, options) {
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.record = function(callback, timeout) {
Mousetrap.prototype.record = function (callback) {
var self = this;
self.recording = true;
// if the user doesn't want to change the timeout
// we still need to guarantee that it gets the default timeout
_recordTimeout = timeout || _config.timeout;
_recordedSequenceCallback = function() {
_recordedSequenceCallback = function () {
self.recording = false;
callback.apply(self, arguments);
};
};
Mousetrap.prototype.handleKey = function() {
/**
* stop recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.stopRecord = function () {
var self = this;
self.recording = false;
};
/**
* start recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.startRecording = function () {
var self = this;
self.recording = true;
};
Mousetrap.prototype.handleKey = function () {
var self = this;
_handleKey.apply(self, arguments);
};
Mousetrap.init();
return Mousetrap;
};
}

View File

@ -260,8 +260,7 @@ following resources helpful:
We chose to use a generic OpenID Connect library on the client, but it's worth
noting that Keycloak comes packaged with its own:
- [redux-oidc](https://github.com/maxmantz/redux-oidc) (Which wraps
[oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki))
- [oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki)
- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
If you're not already drowning in links, here are some good security resources

View File

@ -25,6 +25,9 @@ export {
ViewportGridContext,
ViewportGridProvider,
useViewportGrid,
UserAuthenticationContext,
UserAuthenticationProvider,
useUserAuthentication,
} from './src/contextProviders';
/** COMPONENTS */
@ -92,7 +95,7 @@ export {
ViewportDownloadForm,
ViewportGrid,
ViewportPane,
WindowLevelMenuItem
WindowLevelMenuItem,
} from './src/components';
/** VIEWS */

View File

@ -38,7 +38,7 @@
"react-dnd-html5-backend": "10.0.2",
"react-dom": "16.11.0",
"react-draggable": "4.4.3",
"react-error-boundary": "2.2.x",
"react-error-boundary": "^3.1.3",
"react-modal": "3.11.2",
"react-outside-click-handler": "^1.3.0",
"react-select": "3.0.8",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
// TODO: This may fail if package is split from PWA build
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
function Header({ children, menuOptions, isReturnEnabled, isSticky, WhiteLabeling }) {
const { t } = useTranslation('Header');
const history = useHistory();
const navigate = useNavigate();
// TODO: this should be passed in as a prop instead and the react-router-dom
// dependency should be dropped
const onReturnHandler = () => {
if (isReturnEnabled) {
history.push('/');
navigate('/');
}
};

View File

@ -1,11 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
//import { hotkeys } from '@ohif/core';
import { hotkeys } from '@ohif/core';
import Input from '../Input';
//import { getKeys, formatKeysForInput } from './utils';
import { formatKeysForInput } from './utils';
import { getKeys, formatKeysForInput } from './utils';
/**
* HotkeyField
@ -25,16 +27,16 @@ const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys }) => {
event.stopPropagation();
event.preventDefault();
/*hotkeys.record(sequence => {
hotkeys.record(sequence => {
const keys = getKeys({ sequence, modifierKeys });
hotkeys.unpause();
onChange(keys);
});*/
});
};
const onFocus = () => {
/*hotkeys.pause();
hotkeys.startRecording();*/
hotkeys.pause();
hotkeys.startRecording();
};
return (

View File

@ -35,9 +35,10 @@ const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErr
setErrors(prevState => {
const errors = { ...prevState, [id]: error };
onChange(id, definition, errors);
return errors;
});
onChange(id, definition, { ...errors, [id]: error });
};
return (
@ -84,7 +85,7 @@ const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErr
onChange={onChangeHandler}
className='text-lg h-8'
/>
{error && <span className='p-2 text-red-600 text-sm'>{error}</span>}
{error && <span className='p-2 text-left text-red-600 text-sm'>{error}</span>}
</div>
</div>
</div>

View File

@ -0,0 +1,147 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useReducer,
} from 'react';
import PropTypes from 'prop-types';
const DEFAULT_STATE = {
user: null,
enabled: false,
};
export const UserAuthenticationContext = createContext(DEFAULT_STATE);
export function UserAuthenticationProvider({ children, service }) {
const userAuthenticationReducer = (state, action) => {
switch (action.type) {
case 'SET_USER': {
return {
...state,
...{ user: action.payload.user },
};
}
case 'RESET': {
return {
user: null,
};
}
case 'SET': {
return {
...state,
...action.payload,
};
}
default:
return action.payload;
}
};
const [userAuthenticationState, dispatch] = useReducer(
userAuthenticationReducer,
DEFAULT_STATE
);
const getState = useCallback(() => userAuthenticationState, [
userAuthenticationState,
]);
const setUser = useCallback(
user =>
dispatch({
type: 'SET_USER',
payload: {
user,
},
}),
[dispatch]
);
const getUser = useCallback(() => userAuthenticationState.user, [
userAuthenticationState,
]);
const reset = useCallback(
() =>
dispatch({
type: 'RESET',
payload: {},
}),
[dispatch]
);
const set = useCallback(
payload =>
dispatch({
type: 'SET',
payload,
}),
[dispatch]
);
/**
* Sets the implementation of the UserAuthenticationService that can be used by extensions.
*
* @returns void
*/
// TODO: should this be a useEffect or not?
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
setUser,
getUser,
reset,
set,
});
}
}, [getState, service, setUser, getUser, reset, set]);
// TODO: This may not be correct, but I think we need to set the implementation for the service
// immediately when this runs, since otherwise the authentication redirects will fail.
// (useEffect only runs after the child components - in this case, routing logic - has failed)
if (service) {
service.setServiceImplementation({
getState,
setUser,
getUser,
reset,
set,
});
}
const api = {
getState,
setUser,
getUser,
getAuthorizationHeader: service.getAuthorizationHeader,
handleUnauthenticated: service.handleUnauthenticated,
reset,
set,
};
return (
<UserAuthenticationContext.Provider value={[userAuthenticationState, api]}>
{children}
</UserAuthenticationContext.Provider>
);
}
export default UserAuthenticationProvider;
const UserAuthenticationConsumer = UserAuthenticationContext.Consumer;
export { UserAuthenticationConsumer };
UserAuthenticationProvider.propTypes = {
children: PropTypes.any,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}).isRequired,
};
export const useUserAuthentication = () =>
useContext(UserAuthenticationContext);

View File

@ -2,7 +2,7 @@ export {
default as DialogProvider,
useDialog,
withDialog,
} from './DialogProvider'
} from './DialogProvider';
export { default as DragAndDropProvider } from './DragAndDropProvider';
@ -19,17 +19,13 @@ export {
useImageViewer,
} from './ImageViewerProvider';
export {
CineContext,
default as CineProvider,
useCine,
} from './CineProvider';
export { CineContext, default as CineProvider, useCine } from './CineProvider';
export {
default as SnackbarProvider,
useSnackbar,
withSnackbar,
} from './SnackbarProvider'
} from './SnackbarProvider';
export {
default as ViewportDialogProvider,
@ -41,3 +37,9 @@ export {
ViewportGridProvider,
useViewportGrid,
} from './ViewportGridProvider';
export {
UserAuthenticationContext,
UserAuthenticationProvider,
useUserAuthentication,
} from './UserAuthenticationProvider';

View File

@ -110,6 +110,10 @@ module.exports = (env, argv) => {
historyApiFallback: {
disableDotRule: true,
},
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
});

View File

@ -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"

View File

@ -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: 32 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -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) {

View File

@ -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>
);
}

View File

@ -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,

View File

@ -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();

View File

@ -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;

View File

@ -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}

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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
});
});

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;
}

1238
yarn.lock

File diff suppressed because it is too large Load Diff