From 7bbc213f58a1be3560c0414242549007885d0f13 Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> Date: Thu, 20 Jul 2023 11:43:05 -0400 Subject: [PATCH] feat(DataSource): Allow for dynamic data source configuration and creation (#3543) --- .../default/src/DicomJSONDataSource/index.js | 28 +-- .../default/src/DicomLocalDataSource/index.js | 43 +++-- .../default/src/DicomWebDataSource/index.js | 178 ++++++++++-------- .../src/DicomWebProxyDataSource/index.js | 26 +-- .../default/src/Panels/DataSourceSelector.tsx | 4 +- platform/app/public/config/aws.js | 9 +- platform/app/public/config/default.js | 11 +- platform/app/public/config/demo.js | 2 +- platform/app/public/config/dicomweb-server.js | 9 +- .../app/public/config/dicomweb_relative.js | 9 +- .../app/public/config/docker_nginx-orthanc.js | 9 +- .../public/config/docker_openresty-orthanc.js | 9 +- platform/app/public/config/e2e.js | 13 +- platform/app/public/config/google.js | 9 +- platform/app/public/config/local_dcm4chee.js | 9 +- platform/app/public/config/local_orthanc.js | 9 +- platform/app/public/config/local_static.js | 9 +- platform/app/public/config/multiple.js | 15 +- platform/app/public/config/netlify.js | 9 +- platform/app/src/hooks/index.js | 5 +- platform/app/src/hooks/useQuery.js | 5 - platform/app/src/hooks/useSearchParams.js | 17 -- platform/app/src/hooks/useSearchParams.ts | 24 +++ platform/app/src/routes/DataSourceWrapper.tsx | 161 ++++++++++++---- platform/app/src/routes/Mode/Mode.tsx | 27 +-- platform/app/src/routes/WorkList/WorkList.tsx | 2 +- platform/app/src/routes/buildModeRoutes.tsx | 6 +- platform/app/src/routes/index.tsx | 2 +- .../core/src/DataSources/IWebApiDataSource.js | 2 + .../core/src/extensions/ExtensionManager.ts | 146 +++++++++++--- platform/core/src/types/DataSource.ts | 7 + platform/core/src/types/index.ts | 10 +- .../docs/configuration/configurationFiles.md | 37 +++- .../configuration/dataSources/dicom-web.md | 2 +- .../extensions/modules/data-source.md | 63 +++++++ .../docs/docs/platform/managers/extension.md | 12 +- 36 files changed, 640 insertions(+), 298 deletions(-) delete mode 100644 platform/app/src/hooks/useQuery.js delete mode 100644 platform/app/src/hooks/useSearchParams.js create mode 100644 platform/app/src/hooks/useSearchParams.ts create mode 100644 platform/core/src/types/DataSource.ts diff --git a/extensions/default/src/DicomJSONDataSource/index.js b/extensions/default/src/DicomJSONDataSource/index.js index 241d430af..d412a57a1 100644 --- a/extensions/default/src/DicomJSONDataSource/index.js +++ b/extensions/default/src/DicomJSONDataSource/index.js @@ -13,6 +13,7 @@ const mappings = { let _store = { urls: [], + studyInstanceUIDMap: new Map(), // map of urls to array of study instance UIDs // { // url: url1 // studies: [Study1, Study2], // if multiple studies @@ -41,10 +42,10 @@ const findStudies = (key, value) => { }; function createDicomJSONApi(dicomJsonConfig) { - const { name, wadoRoot } = dicomJsonConfig; + const { wadoRoot } = dicomJsonConfig; const implementation = { - initialize: async ({ params, query, url }) => { + initialize: async ({ query, url }) => { if (!url) url = query.get('url'); let metaData = getMetaDataByURL(url); @@ -58,11 +59,7 @@ function createDicomJSONApi(dicomJsonConfig) { } const response = await fetch(url); - let data = await response.json(); - - const studyInstanceUIDs = data.studies.map( - study => study.StudyInstanceUID - ); + const data = await response.json(); let StudyInstanceUID; let SeriesInstanceUID; @@ -89,12 +86,14 @@ function createDicomJSONApi(dicomJsonConfig) { url, studies: [...data.studies], }); - - return studyInstanceUIDs; + _store.studyInstanceUIDMap.set( + url, + data.studies.map(study => study.StudyInstanceUID) + ); }, query: { studies: { - mapParams: () => { }, + mapParams: () => {}, search: async param => { const [key, value] = Object.entries(param)[0]; const mappedParam = mappings[key]; @@ -252,12 +251,13 @@ function createDicomJSONApi(dicomJsonConfig) { return imageIds; }, getImageIdsForInstance({ instance, frame }) { - const imageIds = getImageId({ - instance, - frame, - }); + const imageIds = getImageId({ instance, frame }); return imageIds; }, + getStudyInstanceUIDs: ({ params, query }) => { + const url = query.get('url'); + return _store.studyInstanceUIDMap.get(url); + }, }; return IWebApiDataSource.create(implementation); } diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index fc8554b92..3de0cc663 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -41,25 +41,7 @@ function createDicomLocalApi(dicomLocalConfig) { const { name } = dicomLocalConfig; const implementation = { - initialize: ({ params, query }) => { - const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; - const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs'); - - const StudyInstanceUIDs = - queryStudyInstanceUIDs || paramsStudyInstanceUIDs; - const StudyInstanceUIDsAsArray = - StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) - ? StudyInstanceUIDs - : [StudyInstanceUIDs]; - - // Put SRs at the end of series list to make sure images are loaded first - StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => { - const study = DicomMetadataStore.getStudy(StudyInstanceUID); - study.series = study.series.sort(customSort); - }); - - return StudyInstanceUIDsAsArray; - }, + initialize: ({ params, query }) => {}, query: { studies: { mapParams: () => {}, @@ -246,6 +228,29 @@ function createDicomLocalApi(dicomLocalConfig) { deleteStudyMetadataPromise() { console.log('deleteStudyMetadataPromise not implemented'); }, + getStudyInstanceUIDs: ({ params, query }) => { + const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; + const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs'); + + const StudyInstanceUIDs = + queryStudyInstanceUIDs || paramsStudyInstanceUIDs; + const StudyInstanceUIDsAsArray = + StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) + ? StudyInstanceUIDs + : [StudyInstanceUIDs]; + + // Put SRs at the end of series list to make sure images are loaded first + let isStudyInCache = false; + StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => { + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + if (study) { + study.series = study.series.sort(customSort); + isStudyInCache = true; + } + }); + + return isStudyInCache ? StudyInstanceUIDsAsArray : []; + }, }; return IWebApiDataSource.create(implementation); } diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 34c8fc196..efbcf944b 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -51,87 +51,77 @@ const metadataProvider = classes.MetadataProvider; * @param {string|bool} singlepart - indicates of the retrieves can fetch singlepart. Options are bulkdata, video, image or boolean true */ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { - const { - qidoRoot, - wadoRoot, - enableStudyLazyLoad, - supportsFuzzyMatching, - supportsWildcard, - supportsReject, - staticWado, - singlepart, - omitQuotationForMultipartRequest, - acceptHeader, - requestTransferSyntaxUID, - } = dicomWebConfig; - - const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig)); - - const getAuthrorizationHeader = () => { - const xhrRequestHeaders = {}; - const authHeaders = userAuthenticationService.getAuthorizationHeader(); - if (authHeaders && authHeaders.Authorization) { - xhrRequestHeaders.Authorization = authHeaders.Authorization; - } - return xhrRequestHeaders; - }; - - const generateWadoHeader = () => { - let authorizationHeader = getAuthrorizationHeader(); - //Generate accept header depending on config params - let formattedAcceptHeader = utils.generateAcceptHeader( - acceptHeader, - requestTransferSyntaxUID, - omitQuotationForMultipartRequest - ); - - return { - ...authorizationHeader, - Accept: formattedAcceptHeader, - }; - }; - - const qidoConfig = { - url: qidoRoot, - staticWado, - singlepart, - headers: getAuthrorizationHeader(), - errorInterceptor: errorHandler.getHTTPErrorHandler(), - }; - - const wadoConfig = { - url: wadoRoot, - staticWado, - singlepart, - headers: generateWadoHeader(), - errorInterceptor: errorHandler.getHTTPErrorHandler(), - }; - - // TODO -> Two clients sucks, but its better than 1000. - // TODO -> We'll need to merge auth later. - const qidoDicomWebClient = staticWado - ? new StaticWadoClient(qidoConfig) - : new api.DICOMwebClient(qidoConfig); - - const wadoDicomWebClient = staticWado - ? new StaticWadoClient(wadoConfig) - : new api.DICOMwebClient(wadoConfig); + let dicomWebConfigCopy, + qidoConfig, + wadoConfig, + qidoDicomWebClient, + wadoDicomWebClient, + getAuthrorizationHeader, + generateWadoHeader; const implementation = { initialize: ({ params, query }) => { - const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; - const queryStudyInstanceUIDs = utils.splitComma( - query.getAll('StudyInstanceUIDs') - ); + if ( + dicomWebConfig.onConfiguration && + typeof dicomWebConfig.onConfiguration === 'function' + ) { + dicomWebConfig = dicomWebConfig.onConfiguration(dicomWebConfig, { + params, + query, + }); + } - const StudyInstanceUIDs = - (queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || - paramsStudyInstanceUIDs; - const StudyInstanceUIDsAsArray = - StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) - ? StudyInstanceUIDs - : [StudyInstanceUIDs]; - return StudyInstanceUIDsAsArray; + dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig)); + + getAuthrorizationHeader = () => { + const xhrRequestHeaders = {}; + const authHeaders = userAuthenticationService.getAuthorizationHeader(); + if (authHeaders && authHeaders.Authorization) { + xhrRequestHeaders.Authorization = authHeaders.Authorization; + } + return xhrRequestHeaders; + }; + + generateWadoHeader = () => { + let authorizationHeader = getAuthrorizationHeader(); + //Generate accept header depending on config params + let formattedAcceptHeader = utils.generateAcceptHeader( + dicomWebConfig.acceptHeader, + dicomWebConfig.requestTransferSyntaxUID, + dicomWebConfig.omitQuotationForMultipartRequest + ); + + return { + ...authorizationHeader, + Accept: formattedAcceptHeader, + }; + }; + + qidoConfig = { + url: dicomWebConfig.qidoRoot, + staticWado: dicomWebConfig.staticWado, + singlepart: dicomWebConfig.singlepart, + headers: userAuthenticationService.getAuthorizationHeader(), + errorInterceptor: errorHandler.getHTTPErrorHandler(), + }; + + wadoConfig = { + url: dicomWebConfig.wadoRoot, + staticWado: dicomWebConfig.staticWado, + singlepart: dicomWebConfig.singlepart, + headers: userAuthenticationService.getAuthorizationHeader(), + errorInterceptor: errorHandler.getHTTPErrorHandler(), + }; + + // TODO -> Two clients sucks, but its better than 1000. + // TODO -> We'll need to merge auth later. + qidoDicomWebClient = dicomWebConfig.staticWado + ? new StaticWadoClient(qidoConfig) + : new api.DICOMwebClient(qidoConfig); + + wadoDicomWebClient = dicomWebConfig.staticWado + ? new StaticWadoClient(wadoConfig) + : new api.DICOMwebClient(wadoConfig); }, query: { studies: { @@ -140,8 +130,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { qidoDicomWebClient.headers = getAuthrorizationHeader(); const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = mapParams(origParams, { - supportsFuzzyMatching, - supportsWildcard, + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, + supportsWildcard: dicomWebConfig.supportsWildcard, }) || {}; const results = await qidoSearch( @@ -194,7 +184,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { * or is already retrieved, or a promise to a URL for such use if a BulkDataURI */ directURL: params => { - return getDirectURL({ wadoRoot, singlepart }, params); + return getDirectURL( + { + wadoRoot: dicomWebConfig.wadoRoot, + singlepart: dicomWebConfig.singlepart, + }, + params + ); }, bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => { qidoDicomWebClient.headers = getAuthrorizationHeader(); @@ -222,7 +218,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { ); } - if (enableStudyLazyLoad) { + if (dicomWebConfig.enableStudyLazyLoad) { return implementation._retrieveSeriesMetadataAsync( StudyInstanceUID, filters, @@ -522,10 +518,26 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { getConfig() { return dicomWebConfigCopy; }, + getStudyInstanceUIDs({ params, query }) { + const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; + const queryStudyInstanceUIDs = utils.splitComma( + query.getAll('StudyInstanceUIDs') + ); + + const StudyInstanceUIDs = + (queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || + paramsStudyInstanceUIDs; + const StudyInstanceUIDsAsArray = + StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) + ? StudyInstanceUIDs + : [StudyInstanceUIDs]; + + return StudyInstanceUIDsAsArray; + }, }; - if (supportsReject) { - implementation.reject = dcm4cheeReject(wadoRoot); + if (dicomWebConfig.supportsReject) { + implementation.reject = dcm4cheeReject(dicomWebConfig.wadoRoot); } return IWebApiDataSource.create(implementation); diff --git a/extensions/default/src/DicomWebProxyDataSource/index.js b/extensions/default/src/DicomWebProxyDataSource/index.js index e873237dd..326c13144 100644 --- a/extensions/default/src/DicomWebProxyDataSource/index.js +++ b/extensions/default/src/DicomWebProxyDataSource/index.js @@ -18,15 +18,6 @@ function createDicomWebProxyApi( const implementation = { initialize: async ({ params, query }) => { - let studyInstanceUIDs = []; - - // there seem to be a couple of variations of the case for this parameter - const queryStudyInstanceUIDs = - query.get('studyInstanceUIDs') || query.get('studyInstanceUids'); - if (!queryStudyInstanceUIDs) { - throw new Error(`No studyInstanceUids in request for '${name}'`); - } - const url = query.get('url'); if (!url) { @@ -39,12 +30,11 @@ function createDicomWebProxyApi( } dicomWebDelegate = createDicomWebApi( - data.servers.dicomWeb[0], + data.servers.dicomWeb[0].configuration, UserAuthenticationService ); - studyInstanceUIDs = queryStudyInstanceUIDs.split(';'); + dicomWebDelegate.initialize({ params, query }); } - return studyInstanceUIDs; }, query: { studies: { @@ -77,6 +67,18 @@ function createDicomWebProxyApi( dicomWebDelegate.getImageIdsForDisplaySet(...args), getImageIdsForInstance: (...args) => dicomWebDelegate.getImageIdsForInstance(...args), + getStudyInstanceUIDs({ params, query }) { + let studyInstanceUIDs = []; + + // there seem to be a couple of variations of the case for this parameter + const queryStudyInstanceUIDs = + query.get('studyInstanceUIDs') || query.get('studyInstanceUids'); + if (!queryStudyInstanceUIDs) { + throw new Error(`No studyInstanceUids in request for '${name}'`); + } + studyInstanceUIDs = queryStudyInstanceUIDs.split(';'); + return studyInstanceUIDs; + }, }; return IWebApiDataSource.create(implementation); } diff --git a/extensions/default/src/Panels/DataSourceSelector.tsx b/extensions/default/src/Panels/DataSourceSelector.tsx index 06ed18909..1aa0fbd03 100644 --- a/extensions/default/src/Panels/DataSourceSelector.tsx +++ b/extensions/default/src/Panels/DataSourceSelector.tsx @@ -31,7 +31,9 @@ function DataSourceSelector() { ) .map(ds => (
-

{ds.friendlyName}

+

+ {ds.configuration?.friendlyName || ds.friendlyName} +