feat: 🎸 Improve usability of Google Cloud adapter, including direct routes to studies (#989)
This commit is contained in:
1 parent
c6f306c0de
commit
2bc361cdca
18 files changed
+413
-138
No files matched your search
@@ -8,44 +8,18 @@ import { connect } from 'react-redux';
|
||||
import { ViewerbaseDragDropContext } from '@ohif/ui';
|
||||
import { SignoutCallbackComponent } from 'redux-oidc';
|
||||
import asyncComponent from './components/AsyncComponent.js';
|
||||
import * as RoutesUtil from './routes/routesUtil';
|
||||
|
||||
import NotFound from './routes/NotFound.js';
|
||||
import { Bar, Container } from './components/LoadingBar/';
|
||||
import './OHIFStandaloneViewer.css';
|
||||
import './variables.css';
|
||||
import './theme-tide.css';
|
||||
|
||||
// Contexts
|
||||
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(() =>
|
||||
import(/* webpackChunkName: "CallbackPage" */ './routes/CallbackPage.js')
|
||||
);
|
||||
const ViewerLocalFileData = asyncComponent(() =>
|
||||
import(
|
||||
/* webpackChunkName: "ViewerLocalFileData" */ './connectedComponents/ViewerLocalFileData.js'
|
||||
)
|
||||
);
|
||||
|
||||
const reload = () => window.location.reload();
|
||||
|
||||
class OHIFStandaloneViewer extends Component {
|
||||
static contextType = AppContext;
|
||||
@@ -86,7 +60,11 @@ class OHIFStandaloneViewer extends Component {
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/silent-refresh.html" onEnter={reload} />
|
||||
<Route
|
||||
exact
|
||||
path="/silent-refresh.html"
|
||||
onEnter={RoutesUtil.reload}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/logout-redirect"
|
||||
@@ -128,41 +106,7 @@ class OHIFStandaloneViewer extends Component {
|
||||
*
|
||||
* See http://reactcommunity.org/react-transition-group/with-react-router/
|
||||
*/
|
||||
const routes = [
|
||||
{
|
||||
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 routes = RoutesUtil.getRoutes(appConfig);
|
||||
|
||||
const currentPath = this.props.location.pathname;
|
||||
const noMatchingRoutes = !routes.find(r =>
|
||||
@@ -184,8 +128,8 @@ class OHIFStandaloneViewer extends Component {
|
||||
</Container>
|
||||
)}
|
||||
</NProgress>
|
||||
<Route exact path="/silent-refresh.html" onEnter={reload} />
|
||||
<Route exact path="/logout-redirect.html" onEnter={reload} />
|
||||
<Route exact path="/silent-refresh.html" onEnter={RoutesUtil.reload} />
|
||||
<Route exact path="/logout-redirect.html" onEnter={RoutesUtil.reload} />
|
||||
{!noMatchingRoutes &&
|
||||
routes.map(({ path, Component }) => (
|
||||
<Route key={path} exact path={path}>
|
||||
|
||||
@@ -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 './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import * as GoogleCloudUtilServers from './utils/getServers';
|
||||
|
||||
class DicomStorePickerModal extends Component {
|
||||
static propTypes = {
|
||||
@@ -19,21 +20,7 @@ class DicomStorePickerModal extends Component {
|
||||
};
|
||||
|
||||
handleEvent = data => {
|
||||
const servers = [
|
||||
{
|
||||
name: data.dicomStore,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
qidoSupportsIncludeField: false,
|
||||
type: 'dicomWeb',
|
||||
qidoRoot: data.qidoRoot,
|
||||
wadoRoot: data.wadoRoot,
|
||||
wadoUriRoot: data.wadoUriRoot,
|
||||
active: true,
|
||||
supportsFuzzyMatching: false,
|
||||
},
|
||||
];
|
||||
|
||||
const servers = GoogleCloudUtilServers.getServers(data, data.dicomstore);
|
||||
this.props.setServers(servers);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ class GoogleCloudApi {
|
||||
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 = {}) {
|
||||
const url = new URL(urlStr);
|
||||
let data = null;
|
||||
|
||||
@@ -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 PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData.js';
|
||||
|
||||
function decodeStudyUids(studyUids) {
|
||||
const decodedData = window.atob(studyUids);
|
||||
|
||||
return decodedData.split(';');
|
||||
}
|
||||
|
||||
function getQueryParameters(location) {
|
||||
if (location) {
|
||||
return queryString.parse(location.search);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
import OHIF from '@ohif/core';
|
||||
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||
|
||||
function IHEInvokeImageDisplay({ location }) {
|
||||
const {
|
||||
// patientID,
|
||||
requestType,
|
||||
studyUID,
|
||||
} = getQueryParameters(location);
|
||||
} = UrlUtil.parse(location.search);
|
||||
|
||||
switch (requestType) {
|
||||
case 'STUDY':
|
||||
@@ -36,7 +23,7 @@ function IHEInvokeImageDisplay({ location }) {
|
||||
case 'STUDYBASE64':
|
||||
return (
|
||||
<ConnectedViewerRetrieveStudyData
|
||||
studyInstanceUids={decodeStudyUids(studyUID)}
|
||||
studyInstanceUids={UrlUtil.paramString.parseParam(studyUID)}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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;
|
||||
let seriesUIDs;
|
||||
return UrlUtil.paramString.parseParam(_seriesInstanceUIDs);
|
||||
};
|
||||
|
||||
if (studyInstanceUids && !seriesInstanceUids) {
|
||||
studyUIDs = studyInstanceUids.split(';');
|
||||
} else if (studyInstanceUids && seriesInstanceUids) {
|
||||
studyUIDs = [studyInstanceUids];
|
||||
seriesUIDs = match.params.seriesInstanceUids.split(';');
|
||||
function ViewerRouting({ match: routeMatch, location: routeLocation }) {
|
||||
const {
|
||||
project,
|
||||
location,
|
||||
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 (
|
||||
<ConnectedViewerRetrieveStudyData
|
||||
studyInstanceUids={studyUIDs}
|
||||
seriesInstanceUids={seriesUIDs}
|
||||
/>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
ViewerRouting.propTypes = {
|
||||
@@ -28,6 +49,10 @@ ViewerRouting.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
studyInstanceUids: PropTypes.string.isRequired,
|
||||
seriesInstanceUids: PropTypes.string,
|
||||
dataset: PropTypes.string,
|
||||
dicomStore: PropTypes.string,
|
||||
location: PropTypes.string,
|
||||
project: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
// Set init data, using values found in localStorage
|
||||
const { reducers, localStorage } = redux;
|
||||
const { reducers, localStorage, sessionStorage } = redux;
|
||||
const middleware = [thunkMiddleware];
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
@@ -21,9 +21,14 @@ reducers.ui = layoutReducers;
|
||||
reducers.oidc = oidcReducer;
|
||||
|
||||
const rootReducer = combineReducers(reducers);
|
||||
const preloadedState = {
|
||||
...localStorage.loadState(),
|
||||
...sessionStorage.loadState(),
|
||||
};
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
localStorage.loadState(), // preloadedState
|
||||
preloadedState,
|
||||
composeEnhancers(applyMiddleware(...middleware))
|
||||
);
|
||||
|
||||
@@ -33,6 +38,9 @@ store.subscribe(() => {
|
||||
localStorage.saveState({
|
||||
preferences: store.getState().preferences,
|
||||
});
|
||||
sessionStorage.saveState({
|
||||
servers: store.getState().servers,
|
||||
});
|
||||
});
|
||||
|
||||
export default store;
|
||||
@@ -1,33 +1,18 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
import ConnectedStudyList from './ConnectedStudyList';
|
||||
|
||||
import OHIF from '@ohif/core';
|
||||
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||
|
||||
// Contexts
|
||||
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 }) {
|
||||
const { appConfig = {} } = useContext(AppContext);
|
||||
|
||||
const filters = location ? getFilters(location) : undefined;
|
||||
const filters = UrlUtil.queryString.getQueryFilters(location);
|
||||
|
||||
let studyListFunctionsEnabled = false;
|
||||
if (appConfig.studyListFunctionsEnabled) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { StudyList } from '@ohif/ui';
|
||||
import ConnectedHeader from '../connectedComponents/ConnectedHeader.js';
|
||||
import * as RoutesUtil from '../routes/routesUtil';
|
||||
import moment from 'moment';
|
||||
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
|
||||
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
|
||||
@@ -175,7 +176,12 @@ class StudyListWithData extends Component {
|
||||
};
|
||||
|
||||
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 => {
|
||||
|
||||
Reference in new issue
Block a user