feat: 🎸 Improve usability of Google Cloud adapter, including direct routes to studies (#989)
This commit is contained in:
parent
c6f306c0de
commit
2bc361cdca
@ -1,11 +1,13 @@
|
|||||||
import actions from './actions.js';
|
import actions from './actions.js';
|
||||||
import reducers from './reducers';
|
import reducers from './reducers';
|
||||||
import localStorage from './localStorage.js';
|
import localStorage from './localStorage.js';
|
||||||
|
import sessionStorage from './sessionStorage.js';
|
||||||
|
|
||||||
const redux = {
|
const redux = {
|
||||||
reducers,
|
reducers,
|
||||||
actions,
|
actions,
|
||||||
localStorage,
|
localStorage,
|
||||||
|
sessionStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default redux;
|
export default redux;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import redux from './index.js';
|
|||||||
|
|
||||||
describe('redux exports', () => {
|
describe('redux exports', () => {
|
||||||
test('have not changed', () => {
|
test('have not changed', () => {
|
||||||
const expectedExports = ['actions', 'reducers', 'localStorage'].sort();
|
const expectedExports = ['actions', 'reducers', 'localStorage', 'sessionStorage'].sort();
|
||||||
|
|
||||||
const exports = Object.keys(redux).sort();
|
const exports = Object.keys(redux).sort();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
const LocalStorageApi = window.localStorage;
|
||||||
|
const localStorageKey = 'state';
|
||||||
export const loadState = () => {
|
export const loadState = () => {
|
||||||
try {
|
try {
|
||||||
const serializedState = window.localStorage.getItem('state');
|
const serializedState = LocalStorageApi.getItem(localStorageKey);
|
||||||
if (!serializedState) {
|
if (!serializedState) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -14,7 +16,7 @@ export const loadState = () => {
|
|||||||
export const saveState = state => {
|
export const saveState = state => {
|
||||||
try {
|
try {
|
||||||
const serializedState = JSON.stringify(state);
|
const serializedState = JSON.stringify(state);
|
||||||
localStorage.setItem('state', serializedState);
|
LocalStorageApi.setItem(localStorageKey, serializedState);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
28
platform/core/src/redux/sessionStorage.js
Normal file
28
platform/core/src/redux/sessionStorage.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const SessionStorageApi = window.sessionStorage;
|
||||||
|
const sessionStorageKey = 'state';
|
||||||
|
export const loadState = () => {
|
||||||
|
try {
|
||||||
|
const serializedState = SessionStorageApi.getItem(sessionStorageKey);
|
||||||
|
if (!serializedState) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(serializedState);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveState = state => {
|
||||||
|
try {
|
||||||
|
const serializedState = JSON.stringify(state);
|
||||||
|
SessionStorageApi.setItem(sessionStorageKey, serializedState);
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionStorage = {
|
||||||
|
saveState,
|
||||||
|
loadState,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sessionStorage;
|
||||||
@ -9,6 +9,7 @@ import studyMetadataManager from './studyMetadataManager';
|
|||||||
import updateMetaDataManager from './updateMetaDataManager.js';
|
import updateMetaDataManager from './updateMetaDataManager.js';
|
||||||
import writeScript from './writeScript.js';
|
import writeScript from './writeScript.js';
|
||||||
import DicomLoaderService from './dicomLoaderService.js';
|
import DicomLoaderService from './dicomLoaderService.js';
|
||||||
|
import * as urlUtil from './urlUtil';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
guid,
|
guid,
|
||||||
@ -23,6 +24,7 @@ const utils = {
|
|||||||
updateMetaDataManager,
|
updateMetaDataManager,
|
||||||
DICOMTagDescriptions,
|
DICOMTagDescriptions,
|
||||||
DicomLoaderService,
|
DicomLoaderService,
|
||||||
|
urlUtil,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -38,6 +40,7 @@ export {
|
|||||||
updateMetaDataManager,
|
updateMetaDataManager,
|
||||||
DICOMTagDescriptions,
|
DICOMTagDescriptions,
|
||||||
DicomLoaderService,
|
DicomLoaderService,
|
||||||
|
urlUtil,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ describe('Top level exports', () => {
|
|||||||
'updateMetaDataManager',
|
'updateMetaDataManager',
|
||||||
'DICOMTagDescriptions',
|
'DICOMTagDescriptions',
|
||||||
'DicomLoaderService',
|
'DicomLoaderService',
|
||||||
|
'urlUtil'
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(utils.default).sort();
|
const exports = Object.keys(utils.default).sort();
|
||||||
|
|||||||
67
platform/core/src/utils/urlUtil.js
Normal file
67
platform/core/src/utils/urlUtil.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import lib from 'query-string';
|
||||||
|
|
||||||
|
const PARAM_SEPARATOR = ';';
|
||||||
|
const PARAM_PATTERN_IDENTIFIER = ':';
|
||||||
|
|
||||||
|
function toLowerCaseFirstLetter(word) {
|
||||||
|
return word[0].toLowerCase() + word.slice(1);
|
||||||
|
}
|
||||||
|
const getFilters = (location = {}) => {
|
||||||
|
const { search } = location;
|
||||||
|
|
||||||
|
if (!search) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParameters = parse(search);
|
||||||
|
const filters = {};
|
||||||
|
|
||||||
|
Object.entries(searchParameters).forEach(([key, value]) => {
|
||||||
|
filters[toLowerCaseFirstLetter(key)] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decode = (strToDecode = '') => {
|
||||||
|
try {
|
||||||
|
const decoded = window.atob(strToDecode);
|
||||||
|
return decoded;
|
||||||
|
} catch (e) {
|
||||||
|
return strToDecode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parse = toParse => {
|
||||||
|
if (toParse) {
|
||||||
|
return lib.parse(toParse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
const parseParam = paramStr => {
|
||||||
|
const _paramDecoded = decode(paramStr);
|
||||||
|
if (_paramDecoded && typeof _paramDecoded === 'string') {
|
||||||
|
return _paramDecoded.split(PARAM_SEPARATOR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceParam = (path = '', paramKey, paramValue) => {
|
||||||
|
const paramPattern = `${PARAM_PATTERN_IDENTIFIER}${paramKey}`;
|
||||||
|
if (paramValue) {
|
||||||
|
return path.replace(paramPattern, paramValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryString = {
|
||||||
|
getQueryFilters: getFilters,
|
||||||
|
};
|
||||||
|
|
||||||
|
const paramString = {
|
||||||
|
parseParam: parseParam,
|
||||||
|
replaceParam: replaceParam,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { parse, queryString, paramString };
|
||||||
@ -8,44 +8,18 @@ import { connect } from 'react-redux';
|
|||||||
import { ViewerbaseDragDropContext } from '@ohif/ui';
|
import { ViewerbaseDragDropContext } from '@ohif/ui';
|
||||||
import { SignoutCallbackComponent } from 'redux-oidc';
|
import { SignoutCallbackComponent } from 'redux-oidc';
|
||||||
import asyncComponent from './components/AsyncComponent.js';
|
import asyncComponent from './components/AsyncComponent.js';
|
||||||
|
import * as RoutesUtil from './routes/routesUtil';
|
||||||
|
|
||||||
import NotFound from './routes/NotFound.js';
|
import NotFound from './routes/NotFound.js';
|
||||||
import { Bar, Container } from './components/LoadingBar/';
|
import { Bar, Container } from './components/LoadingBar/';
|
||||||
import './OHIFStandaloneViewer.css';
|
import './OHIFStandaloneViewer.css';
|
||||||
import './variables.css';
|
import './variables.css';
|
||||||
import './theme-tide.css';
|
import './theme-tide.css';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import AppContext from './context/AppContext';
|
import AppContext from './context/AppContext';
|
||||||
|
|
||||||
// Dynamic Import Routes (CodeSplitting)
|
|
||||||
const IHEInvokeImageDisplay = asyncComponent(() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "IHEInvokeImageDisplay" */ './routes/IHEInvokeImageDisplay.js'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const ViewerRouting = asyncComponent(() =>
|
|
||||||
import(/* webpackChunkName: "ViewerRouting" */ './routes/ViewerRouting.js')
|
|
||||||
);
|
|
||||||
const StudyListRouting = asyncComponent(() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "StudyListRouting" */ './studylist/StudyListRouting.js'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const StandaloneRouting = asyncComponent(() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "StandaloneRouting" */ './routes/StandaloneRouting.js'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const CallbackPage = asyncComponent(() =>
|
const CallbackPage = asyncComponent(() =>
|
||||||
import(/* webpackChunkName: "CallbackPage" */ './routes/CallbackPage.js')
|
import(/* webpackChunkName: "CallbackPage" */ './routes/CallbackPage.js')
|
||||||
);
|
);
|
||||||
const ViewerLocalFileData = asyncComponent(() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "ViewerLocalFileData" */ './connectedComponents/ViewerLocalFileData.js'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const reload = () => window.location.reload();
|
|
||||||
|
|
||||||
class OHIFStandaloneViewer extends Component {
|
class OHIFStandaloneViewer extends Component {
|
||||||
static contextType = AppContext;
|
static contextType = AppContext;
|
||||||
@ -86,7 +60,11 @@ class OHIFStandaloneViewer extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/silent-refresh.html" onEnter={reload} />
|
<Route
|
||||||
|
exact
|
||||||
|
path="/silent-refresh.html"
|
||||||
|
onEnter={RoutesUtil.reload}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/logout-redirect"
|
path="/logout-redirect"
|
||||||
@ -128,41 +106,7 @@ class OHIFStandaloneViewer extends Component {
|
|||||||
*
|
*
|
||||||
* See http://reactcommunity.org/react-transition-group/with-react-router/
|
* See http://reactcommunity.org/react-transition-group/with-react-router/
|
||||||
*/
|
*/
|
||||||
const routes = [
|
const routes = RoutesUtil.getRoutes(appConfig);
|
||||||
{
|
|
||||||
path: '/local',
|
|
||||||
Component: ViewerLocalFileData,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/viewer',
|
|
||||||
Component: StandaloneRouting,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/viewer/:studyInstanceUids',
|
|
||||||
Component: ViewerRouting,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/study/:studyInstanceUids/series/:seriesInstanceUids',
|
|
||||||
Component: ViewerRouting,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/IHEInvokeImageDisplay',
|
|
||||||
Component: IHEInvokeImageDisplay,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const showStudyList =
|
|
||||||
appConfig.showStudyList !== undefined ? appConfig.showStudyList : true;
|
|
||||||
if (showStudyList) {
|
|
||||||
routes.push({
|
|
||||||
path: '/studylist',
|
|
||||||
Component: StudyListRouting,
|
|
||||||
});
|
|
||||||
routes.push({
|
|
||||||
path: '/',
|
|
||||||
Component: StudyListRouting,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPath = this.props.location.pathname;
|
const currentPath = this.props.location.pathname;
|
||||||
const noMatchingRoutes = !routes.find(r =>
|
const noMatchingRoutes = !routes.find(r =>
|
||||||
@ -184,8 +128,8 @@ class OHIFStandaloneViewer extends Component {
|
|||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
</NProgress>
|
</NProgress>
|
||||||
<Route exact path="/silent-refresh.html" onEnter={reload} />
|
<Route exact path="/silent-refresh.html" onEnter={RoutesUtil.reload} />
|
||||||
<Route exact path="/logout-redirect.html" onEnter={reload} />
|
<Route exact path="/logout-redirect.html" onEnter={RoutesUtil.reload} />
|
||||||
{!noMatchingRoutes &&
|
{!noMatchingRoutes &&
|
||||||
routes.map(({ path, Component }) => (
|
routes.map(({ path, Component }) => (
|
||||||
<Route key={path} exact path={path}>
|
<Route key={path} exact path={path}>
|
||||||
|
|||||||
77
platform/viewer/src/customHooks/useServer.js
Normal file
77
platform/viewer/src/customHooks/useServer.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import GoogleCloudApi from '../googleCloud/api/GoogleCloudApi';
|
||||||
|
|
||||||
|
import * as GoogleCloudUtilServers from '../googleCloud/utils/getServers';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
// Contexts
|
||||||
|
import AppContext from '../context/AppContext';
|
||||||
|
|
||||||
|
const getActiveServer = servers => {
|
||||||
|
const isActive = a => a.active === true;
|
||||||
|
|
||||||
|
return servers && servers.servers && servers.servers.find(isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServers = (appConfig, project, location, dataset, dicomStore) => {
|
||||||
|
let servers = [];
|
||||||
|
if (appConfig.enableGoogleCloudAdapter) {
|
||||||
|
const pathUrl = GoogleCloudApi.getUrlBaseDicomWeb(
|
||||||
|
project,
|
||||||
|
location,
|
||||||
|
dataset,
|
||||||
|
dicomStore
|
||||||
|
);
|
||||||
|
const data = {
|
||||||
|
project,
|
||||||
|
location,
|
||||||
|
dataset,
|
||||||
|
dicomStore,
|
||||||
|
wadoUriRoot: pathUrl,
|
||||||
|
qidoRoot: pathUrl,
|
||||||
|
wadoRoot: pathUrl,
|
||||||
|
};
|
||||||
|
servers = GoogleCloudUtilServers.getServers(data, dicomStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateServer = (
|
||||||
|
appConfig,
|
||||||
|
dispatch,
|
||||||
|
project,
|
||||||
|
location,
|
||||||
|
dataset,
|
||||||
|
dicomStore
|
||||||
|
) => {
|
||||||
|
const servers = getServers(appConfig, project, location, dataset, dicomStore);
|
||||||
|
|
||||||
|
if (servers && servers.length) {
|
||||||
|
const action = {
|
||||||
|
type: 'SET_SERVERS',
|
||||||
|
servers,
|
||||||
|
};
|
||||||
|
dispatch(action);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useServer({
|
||||||
|
project,
|
||||||
|
location,
|
||||||
|
dataset,
|
||||||
|
dicomStore,
|
||||||
|
} = {}) {
|
||||||
|
// Hooks
|
||||||
|
const servers = useSelector(state => state && state.servers);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { appConfig = {} } = useContext(AppContext);
|
||||||
|
|
||||||
|
const server = getActiveServer(servers);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
updateServer(appConfig, dispatch, project, location, dataset, dicomStore);
|
||||||
|
} else {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import Modal from 'react-bootstrap-modal';
|
|||||||
import DatasetSelector from './DatasetSelector';
|
import DatasetSelector from './DatasetSelector';
|
||||||
import './googleCloud.css';
|
import './googleCloud.css';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import * as GoogleCloudUtilServers from './utils/getServers';
|
||||||
|
|
||||||
class DicomStorePickerModal extends Component {
|
class DicomStorePickerModal extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -19,21 +20,7 @@ class DicomStorePickerModal extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleEvent = data => {
|
handleEvent = data => {
|
||||||
const servers = [
|
const servers = GoogleCloudUtilServers.getServers(data, data.dicomstore);
|
||||||
{
|
|
||||||
name: data.dicomStore,
|
|
||||||
imageRendering: 'wadors',
|
|
||||||
thumbnailRendering: 'wadors',
|
|
||||||
qidoSupportsIncludeField: false,
|
|
||||||
type: 'dicomWeb',
|
|
||||||
qidoRoot: data.qidoRoot,
|
|
||||||
wadoRoot: data.wadoRoot,
|
|
||||||
wadoUriRoot: data.wadoUriRoot,
|
|
||||||
active: true,
|
|
||||||
supportsFuzzyMatching: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
this.props.setServers(servers);
|
this.props.setServers(servers);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,13 @@ class GoogleCloudApi {
|
|||||||
return this.urlBase + `/projects`;
|
return this.urlBase + `/projects`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUrlBaseDicomWeb(project, location, dataset, dicomStore) {
|
||||||
|
return (
|
||||||
|
this.urlBase +
|
||||||
|
`/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}/dicomWeb`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async doRequest(urlStr, config = {}, params = {}) {
|
async doRequest(urlStr, config = {}, params = {}) {
|
||||||
const url = new URL(urlStr);
|
const url = new URL(urlStr);
|
||||||
let data = null;
|
let data = null;
|
||||||
|
|||||||
32
platform/viewer/src/googleCloud/utils/getServers.js
Normal file
32
platform/viewer/src/googleCloud/utils/getServers.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const getServers = (data, name) => {
|
||||||
|
const {
|
||||||
|
wadoUriRoot,
|
||||||
|
qidoRoot,
|
||||||
|
wadoRoot,
|
||||||
|
dataset = '',
|
||||||
|
dicomStore = '',
|
||||||
|
location = '',
|
||||||
|
project = '',
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
dataset,
|
||||||
|
dicomStore,
|
||||||
|
location,
|
||||||
|
project,
|
||||||
|
imageRendering: 'wadors',
|
||||||
|
thumbnailRendering: 'wadors',
|
||||||
|
type: 'dicomWeb',
|
||||||
|
active: true,
|
||||||
|
wadoUriRoot,
|
||||||
|
qidoRoot,
|
||||||
|
wadoRoot,
|
||||||
|
supportsFuzzyMatching: false,
|
||||||
|
qidoSupportsIncludeField: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getServers };
|
||||||
@ -1,29 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import queryString from 'query-string';
|
|
||||||
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData.js';
|
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData.js';
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
function decodeStudyUids(studyUids) {
|
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||||
const decodedData = window.atob(studyUids);
|
|
||||||
|
|
||||||
return decodedData.split(';');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQueryParameters(location) {
|
|
||||||
if (location) {
|
|
||||||
return queryString.parse(location.search);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function IHEInvokeImageDisplay({ location }) {
|
function IHEInvokeImageDisplay({ location }) {
|
||||||
const {
|
const {
|
||||||
// patientID,
|
// patientID,
|
||||||
requestType,
|
requestType,
|
||||||
studyUID,
|
studyUID,
|
||||||
} = getQueryParameters(location);
|
} = UrlUtil.parse(location.search);
|
||||||
|
|
||||||
switch (requestType) {
|
switch (requestType) {
|
||||||
case 'STUDY':
|
case 'STUDY':
|
||||||
@ -36,7 +23,7 @@ function IHEInvokeImageDisplay({ location }) {
|
|||||||
case 'STUDYBASE64':
|
case 'STUDYBASE64':
|
||||||
return (
|
return (
|
||||||
<ConnectedViewerRetrieveStudyData
|
<ConnectedViewerRetrieveStudyData
|
||||||
studyInstanceUids={decodeStudyUids(studyUID)}
|
studyInstanceUids={UrlUtil.paramString.parseParam(studyUID)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +1,47 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData';
|
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData';
|
||||||
|
import useServer from '../customHooks/useServer';
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||||
|
|
||||||
function ViewerRouting({ match }) {
|
/**
|
||||||
const { studyInstanceUids, seriesInstanceUids } = match.params;
|
* Get array of seriesUIDs from param or from queryString
|
||||||
|
* @param {*} seriesInstanceUIDs
|
||||||
|
* @param {*} location
|
||||||
|
*/
|
||||||
|
const getSeriesInstanceUIDs = (seriesInstanceUIDs, routeLocation) => {
|
||||||
|
const queryFilters = UrlUtil.queryString.getQueryFilters(routeLocation);
|
||||||
|
const querySeriesUIDs = queryFilters && queryFilters['SeriesInstanceUID'];
|
||||||
|
const _seriesInstanceUIDs = seriesInstanceUIDs || querySeriesUIDs;
|
||||||
|
|
||||||
let studyUIDs;
|
return UrlUtil.paramString.parseParam(_seriesInstanceUIDs);
|
||||||
let seriesUIDs;
|
};
|
||||||
|
|
||||||
if (studyInstanceUids && !seriesInstanceUids) {
|
function ViewerRouting({ match: routeMatch, location: routeLocation }) {
|
||||||
studyUIDs = studyInstanceUids.split(';');
|
const {
|
||||||
} else if (studyInstanceUids && seriesInstanceUids) {
|
project,
|
||||||
studyUIDs = [studyInstanceUids];
|
location,
|
||||||
seriesUIDs = match.params.seriesInstanceUids.split(';');
|
dataset,
|
||||||
|
dicomStore,
|
||||||
|
studyInstanceUids,
|
||||||
|
seriesInstanceUids,
|
||||||
|
} = routeMatch.params;
|
||||||
|
const server = useServer({ project, location, dataset, dicomStore });
|
||||||
|
|
||||||
|
const studyUIDs = UrlUtil.paramString.parseParam(studyInstanceUids);
|
||||||
|
const seriesUIDs = getSeriesInstanceUIDs(seriesInstanceUids, routeLocation);
|
||||||
|
|
||||||
|
if (server && studyUIDs) {
|
||||||
|
return (
|
||||||
|
<ConnectedViewerRetrieveStudyData
|
||||||
|
studyInstanceUids={studyUIDs}
|
||||||
|
seriesInstanceUids={seriesUIDs}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<ConnectedViewerRetrieveStudyData
|
|
||||||
studyInstanceUids={studyUIDs}
|
|
||||||
seriesInstanceUids={seriesUIDs}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewerRouting.propTypes = {
|
ViewerRouting.propTypes = {
|
||||||
@ -28,6 +49,10 @@ ViewerRouting.propTypes = {
|
|||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
studyInstanceUids: PropTypes.string.isRequired,
|
studyInstanceUids: PropTypes.string.isRequired,
|
||||||
seriesInstanceUids: PropTypes.string,
|
seriesInstanceUids: PropTypes.string,
|
||||||
|
dataset: PropTypes.string,
|
||||||
|
dicomStore: PropTypes.string,
|
||||||
|
location: PropTypes.string,
|
||||||
|
project: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
114
platform/viewer/src/routes/routesUtil.js
Normal file
114
platform/viewer/src/routes/routesUtil.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import asyncComponent from '../components/AsyncComponent.js';
|
||||||
|
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||||
|
|
||||||
|
// Dynamic Import Routes (CodeSplitting)
|
||||||
|
const IHEInvokeImageDisplay = asyncComponent(() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "IHEInvokeImageDisplay" */ './IHEInvokeImageDisplay.js'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const ViewerRouting = asyncComponent(() =>
|
||||||
|
import(/* webpackChunkName: "ViewerRouting" */ './ViewerRouting.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
const StudyListRouting = asyncComponent(() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "StudyListRouting" */ '../studylist/StudyListRouting.js'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const StandaloneRouting = asyncComponent(() =>
|
||||||
|
import(/* webpackChunkName: "StandaloneRouting" */ './StandaloneRouting.js')
|
||||||
|
);
|
||||||
|
const ViewerLocalFileData = asyncComponent(() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "ViewerLocalFileData" */ '../connectedComponents/ViewerLocalFileData.js'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const reload = () => window.location.reload();
|
||||||
|
|
||||||
|
const ROUTES_DEF = {
|
||||||
|
default: {
|
||||||
|
viewer: {
|
||||||
|
path: '/viewer/:studyInstanceUids',
|
||||||
|
component: ViewerRouting,
|
||||||
|
},
|
||||||
|
standaloneViewer: {
|
||||||
|
path: '/viewer',
|
||||||
|
component: StandaloneRouting,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
path: ['/studylist', '/'],
|
||||||
|
component: StudyListRouting,
|
||||||
|
condition: appConfig => {
|
||||||
|
return appConfig.showStudyList !== undefined
|
||||||
|
? appConfig.showStudyList
|
||||||
|
: true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
path: '/local',
|
||||||
|
component: ViewerLocalFileData,
|
||||||
|
},
|
||||||
|
IHEInvokeImageDisplay: {
|
||||||
|
path: '/IHEInvokeImageDisplay',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gcloud: {
|
||||||
|
viewer: {
|
||||||
|
path:
|
||||||
|
'/projects/:project/locations/:location/datasets/:dataset/dicomStores/:dicomStore/study/:studyInstanceUids',
|
||||||
|
component: ViewerRouting,
|
||||||
|
condition: appConfig => {
|
||||||
|
return !!appConfig.enableGoogleCloudAdapter;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoutes = appConfig => {
|
||||||
|
const routes = [];
|
||||||
|
for (let keyConfig in ROUTES_DEF) {
|
||||||
|
const routesConfig = ROUTES_DEF[keyConfig];
|
||||||
|
|
||||||
|
for (let routeKey in routesConfig) {
|
||||||
|
const route = routesConfig[routeKey];
|
||||||
|
const validRoute =
|
||||||
|
typeof route.condition === 'function'
|
||||||
|
? route.condition(appConfig)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (validRoute) {
|
||||||
|
routes.push({
|
||||||
|
path: route.path,
|
||||||
|
Component: route.component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseViewerPath = (appConfig = {}, server = {}, params) => {
|
||||||
|
let viewerPath = ROUTES_DEF.default.viewer.path;
|
||||||
|
if (appConfig.enableGoogleCloudAdapter) {
|
||||||
|
viewerPath = ROUTES_DEF.gcloud.viewer.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _paramsCopy = Object.assign({}, server, params);
|
||||||
|
|
||||||
|
for (let key in _paramsCopy) {
|
||||||
|
viewerPath = UrlUtil.paramString.replaceParam(
|
||||||
|
viewerPath,
|
||||||
|
key,
|
||||||
|
_paramsCopy[key]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewerPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getRoutes, parseViewerPath, reload };
|
||||||
@ -13,7 +13,7 @@ import thunkMiddleware from 'redux-thunk';
|
|||||||
|
|
||||||
// Combine our @ohif/core, ui, and oidc reducers
|
// Combine our @ohif/core, ui, and oidc reducers
|
||||||
// Set init data, using values found in localStorage
|
// Set init data, using values found in localStorage
|
||||||
const { reducers, localStorage } = redux;
|
const { reducers, localStorage, sessionStorage } = redux;
|
||||||
const middleware = [thunkMiddleware];
|
const middleware = [thunkMiddleware];
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
|
||||||
@ -21,9 +21,14 @@ reducers.ui = layoutReducers;
|
|||||||
reducers.oidc = oidcReducer;
|
reducers.oidc = oidcReducer;
|
||||||
|
|
||||||
const rootReducer = combineReducers(reducers);
|
const rootReducer = combineReducers(reducers);
|
||||||
|
const preloadedState = {
|
||||||
|
...localStorage.loadState(),
|
||||||
|
...sessionStorage.loadState(),
|
||||||
|
};
|
||||||
|
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
rootReducer,
|
rootReducer,
|
||||||
localStorage.loadState(), // preloadedState
|
preloadedState,
|
||||||
composeEnhancers(applyMiddleware(...middleware))
|
composeEnhancers(applyMiddleware(...middleware))
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -33,6 +38,9 @@ store.subscribe(() => {
|
|||||||
localStorage.saveState({
|
localStorage.saveState({
|
||||||
preferences: store.getState().preferences,
|
preferences: store.getState().preferences,
|
||||||
});
|
});
|
||||||
|
sessionStorage.saveState({
|
||||||
|
servers: store.getState().servers,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
@ -1,33 +1,18 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import queryString from 'query-string';
|
|
||||||
import ConnectedStudyList from './ConnectedStudyList';
|
import ConnectedStudyList from './ConnectedStudyList';
|
||||||
|
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import AppContext from '../context/AppContext';
|
import AppContext from '../context/AppContext';
|
||||||
|
|
||||||
// TODO: Move to @ohif/ui
|
|
||||||
|
|
||||||
function toLowerCaseFirstLetter(word) {
|
|
||||||
return word[0].toLowerCase() + word.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilters({ search }) {
|
|
||||||
const searchParameters = queryString.parse(search);
|
|
||||||
const filters = {};
|
|
||||||
|
|
||||||
Object.entries(searchParameters).forEach(([key, value]) => {
|
|
||||||
filters[toLowerCaseFirstLetter(key)] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
function StudyListRouting({ location }) {
|
function StudyListRouting({ location }) {
|
||||||
const { appConfig = {} } = useContext(AppContext);
|
const { appConfig = {} } = useContext(AppContext);
|
||||||
|
|
||||||
const filters = location ? getFilters(location) : undefined;
|
const filters = UrlUtil.queryString.getQueryFilters(location);
|
||||||
|
|
||||||
let studyListFunctionsEnabled = false;
|
let studyListFunctionsEnabled = false;
|
||||||
if (appConfig.studyListFunctionsEnabled) {
|
if (appConfig.studyListFunctionsEnabled) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
|
|||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import { StudyList } from '@ohif/ui';
|
import { StudyList } from '@ohif/ui';
|
||||||
import ConnectedHeader from '../connectedComponents/ConnectedHeader.js';
|
import ConnectedHeader from '../connectedComponents/ConnectedHeader.js';
|
||||||
|
import * as RoutesUtil from '../routes/routesUtil';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
|
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
|
||||||
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
|
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
|
||||||
@ -175,7 +176,12 @@ class StudyListWithData extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSelectItem = studyInstanceUID => {
|
onSelectItem = studyInstanceUID => {
|
||||||
this.props.history.push(`/viewer/${studyInstanceUID}`);
|
const { appConfig = {} } = this.context;
|
||||||
|
const { server } = this.props;
|
||||||
|
const viewerPath = RoutesUtil.parseViewerPath(appConfig, server, {
|
||||||
|
studyInstanceUids: studyInstanceUID,
|
||||||
|
});
|
||||||
|
this.props.history.push(viewerPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearch = searchData => {
|
onSearch = searchData => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user