feat: New @ohif/viewer project structure + routes (#1674)

* fix pagination size

* clean up, start a very basic route configuration

* quick navigation between routes

* configure webpack to read .env variables

* Move container components to Routes directory

* Fix naming of recipe file

* Clean up source folder

* update yarn.lock

* ci: update netlify deploy preview to build viewer project

* update PWA entrypoint to pass configuration on window to App as props

* fix: for missing key

* Pass routerBasename to our React Router component

* Simplify config for meow

Co-authored-by: dannyrb <danny.ri.brown@gmail.com>
This commit is contained in:
Rodrigo Antinarellianddannyrb authored and James A. Petts committed 2020-05-15 13:57:23 +02:00
1 parent 9a8bede80e
commit 01b314522d
116 files changed
+22080 -4971

No files matched your search

+37 -5
View File
@@ -1,14 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter, HashRouter } from 'react-router-dom';
import { ThemeWrapper } from '@ohif/ui';
import ConnectedStudyList from './connectedComponents/ConnectedStudyList';
import routes from './routes';
function App({ config }) {
const { routerBasename } = config;
const Router = JSON.parse(process.env.USE_HASH_ROUTER)
? HashRouter
: BrowserRouter;
const App = () => {
return (
<ThemeWrapper>
<ConnectedStudyList />
</ThemeWrapper>
<Router basename={routerBasename}>
<ThemeWrapper>{routes()}</ThemeWrapper>
</Router>
);
}
App.propTypes = {
config: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({
routerBasename: PropTypes.string.isRequired,
oidc: PropTypes.array,
whiteLabeling: PropTypes.shape({
createLogoComponentFn: PropTypes.func,
}),
extensions: PropTypes.array,
}),
]).isRequired,
/* Extensions that are "bundled" or "baked-in" to the application */
defaultExtensions: PropTypes.array,
};
App.defaultProps = {
config: {
showStudyList: true,
oidc: [],
extensions: [],
},
defaultExtensions: [],
};
export default App;
@@ -1,37 +0,0 @@
html,
body,
#root {
background-color: black;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light,
Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif;
}
body {
font-size: 14px;
line-height: 1.42857143;
}
#root .fade-enter {
opacity: 0;
}
#root .fade-enter-active {
opacity: 1;
transition: opacity 200ms;
}
#root .fade-exit {
opacity: 1;
}
#root .fade-exit-active {
opacity: 0;
transition: opacity 200ms;
}
@@ -1,8 +0,0 @@
import commandsModule from './commandsModule.js';
export default {
id: 'generic-viewer-commands',
getCommandsModule({ commandsManager }) {
return commandsModule({ commandsManager });
},
};
@@ -1,420 +0,0 @@
import { connect } from 'react-redux';
import { MeasurementTable } from '@ohif/ui';
import OHIF, { DICOMSR } from '@ohif/core';
import moment from 'moment';
import cornerstone from 'cornerstone-core';
import jumpToRowItem from './jumpToRowItem.js';
const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements;
/**
* Takes a list of objects and a property and return the list grouped by the property
*
* @param {Array} list - The objects to be grouped by
* @param {string} props - The property to group the objects
* @returns {Object}
*/
function groupBy(list, props) {
return list.reduce((a, b) => {
(a[b[props]] = a[b[props]] || []).push(b);
return a;
}, {});
}
/**
* Takes a list of tools grouped and return all tools separately
*
* @param {Array} [toolGroups=[]] - The grouped tools
* @returns {Array} - The list of all tools on all groups
*/
function getAllTools(toolGroups = []) {
let tools = [];
toolGroups.forEach(toolGroup => (tools = tools.concat(toolGroup.childTools)));
return tools;
}
/**
* Takes measurementData and build the measurement text to be used into the table
*
* @param {Object} [measurementData={}]
* @param {string} measurementData.location - The measurement location
* @param {string} measurementData.description - The measurement description
* @returns {string}
*/
function getMeasurementText(measurementData = {}) {
const defaultText = '...';
const { location = '', description = '' } = measurementData;
const result = location + (description ? ` (${description})` : '');
return result || defaultText;
}
/**
* Takes a list of measurements grouped by measurement numbers and return each measurement data by available timepoint
*
* @param {Array} measurementNumberList - The list of measurements
* @param {Array} timepoints - The list of available timepoints
* @param {Function} displayFunction - The function that builds the display text by each tool
* @returns
*/
function getDataForEachMeasurementNumber(
measurementNumberList,
timepoints,
displayFunction
) {
const data = [];
// on each measurement number we should get each measurement data by available timepoint
measurementNumberList.forEach(measurement => {
timepoints.forEach(timepoint => {
const eachData = {
displayText: '...',
};
if (measurement.timepointId === timepoint.timepointId) {
eachData.displayText = displayFunction(measurement);
}
data.push(eachData);
});
});
return data;
}
/**
* Take a measurement toolName and return if any warnings
*
* @param {string} toolName - The tool name
* @returns {string}
*/
function getWarningsForMeasurement(toolName) {
const isToolSupported = DICOMSR.isToolSupported(toolName);
return {
hasWarnings: !isToolSupported,
warningTitle: isToolSupported ? '' : 'Unsupported Tool',
warningList: isToolSupported
? []
: [`${toolName} cannot be persisted at this time`],
};
}
/**
* Take measurements from MeasurementAPI structure and convert into a measurementTable structure
*
* @param {Object} toolCollections - The list of all measurement grouped by groupTool and toolName
* @param {Array} timepoints - The list of available timepoints
* @returns
*/
function convertMeasurementsToTableData(toolCollections, timepoints) {
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroups = config.measurementTools;
const tools = getAllTools(toolGroups);
const tableMeasurements = toolGroups.map(toolGroup => {
return {
groupName: toolGroup.name,
groupId: toolGroup.id,
measurements: [],
};
});
Object.keys(toolCollections).forEach(toolId => {
const toolMeasurements = toolCollections[toolId];
const tool = tools.find(tool => tool.id === toolId);
const { displayFunction } = tool.options.measurementTable;
// Group by measurementNumber so we can display then all in the same line
const groupedMeasurements = groupBy(toolMeasurements, 'measurementNumber');
Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => {
const measurementNumberList =
groupedMeasurements[groupedMeasurementsIndex];
const measurementData = measurementNumberList[0];
const {
measurementNumber,
lesionNamingNumber,
toolType,
} = measurementData;
const measurementId = measurementData._id;
const {
hasWarnings,
warningTitle,
warningList,
} = getWarningsForMeasurement(toolType);
//check if all measurements with same measurementNumber will have same LABEL
const tableMeasurement = {
itemNumber: lesionNamingNumber,
label: getMeasurementText(measurementData),
measurementId,
measurementNumber,
lesionNamingNumber,
toolType,
hasWarnings,
warningTitle,
warningList,
isSplitLesion: false, //TODO
data: getDataForEachMeasurementNumber(
measurementNumberList,
timepoints,
displayFunction
),
};
// find the group object for the tool
const toolGroupMeasurements = tableMeasurements.find(group => {
return group.groupId === tool.toolGroup;
});
// inject the new measurement for this measurementNumer
toolGroupMeasurements.measurements.push(tableMeasurement);
});
});
// Sort measurements by lesion naming number
tableMeasurements.forEach(tm => {
tm.measurements.sort((m1, m2) =>
m1.lesionNamingNumber > m2.lesionNamingNumber ? 1 : -1
);
});
return tableMeasurements;
}
/**
* Take a list of available timepoints and return a list header information for each timepoint
*
* @param {Array} timepoints - The list of available timepoints
* @param {string} timepoints[].latestDate - The date of the last study taken on the timepoint
* @returns {{label: string, key: string, date: string}[]}
*/
function convertTimepointsToTableData(timepoints) {
if (!timepoints || !timepoints.length) {
return [];
}
return [
{
label: 'Study date:',
key: 'StudyDate',
date: moment(timepoints[0].latestDate).format('DD-MMM-YY'),
},
];
}
/**
* Takes server type and return a function or undefined
*
* @param {string} serverType - The server type
* @returns {undefined|Function}
*/
function getSaveFunction(serverType) {
if (serverType === 'dicomWeb') {
return () => {
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const promise = measurementApi.storeMeasurements();
return promise;
};
}
}
const mapStateToProps = state => {
const { timepointManager, servers } = state;
const { timepoints, measurements } = timepointManager;
const activeServer = servers.servers.find(a => a.active === true);
const saveFunction = getSaveFunction(activeServer.type);
return {
timepoints: convertTimepointsToTableData(timepoints),
measurementCollection: convertMeasurementsToTableData(
measurements,
timepoints
),
timepointManager: state.timepointManager,
viewports: state.viewports,
saveFunction,
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
dispatchRelabel: (event, measurementData, viewportsState) => {
event.persist();
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0;
const enabledElements = cornerstone.getEnabledElements();
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
OHIF.log.error('Failed to find the enabled element');
return;
}
const { toolType, measurementId } = measurementData;
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId;
});
// Clone the tool not to set empty location initially
const toolForLocation = Object.assign({}, tool, { location: null });
if (ownProps.onRelabel) {
ownProps.onRelabel(toolForLocation);
}
},
dispatchEditDescription: (event, measurementData, viewportsState) => {
event.persist();
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0;
const enabledElements = cornerstone.getEnabledElements();
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
OHIF.log.error('Failed to find the enabled element');
return;
}
const { toolType, measurementId } = measurementData;
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId;
});
if (ownProps.onEditDescription) {
ownProps.onEditDescription(tool);
}
},
dispatchJumpToRowItem: (
measurementData,
viewportsState,
timepointManagerState,
options
) => {
const actionData = jumpToRowItem(
measurementData,
viewportsState,
timepointManagerState,
dispatch,
options
);
actionData.viewportSpecificData.forEach(viewportSpecificData => {
const { viewportIndex, displaySet } = viewportSpecificData;
dispatch(setViewportSpecificData(viewportIndex, displaySet));
});
const { toolType, measurementNumber } = measurementData;
const measurementApi = MeasurementApi.Instance;
Object.keys(measurementApi.tools).forEach(toolType => {
const measurements = measurementApi.tools[toolType];
measurements.forEach(measurement => {
measurement.active = false;
});
});
const measurementsToActive = measurementApi.tools[toolType].filter(
measurement => {
return measurement.measurementNumber === measurementNumber;
}
);
measurementsToActive.forEach(measurementToActive => {
measurementToActive.active = true;
});
measurementApi.syncMeasurementsAndToolData();
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
// Needs to update viewports.layout state to set layout
//const layout = actionData.layout;
//dispatch(setLayout(layout))
// Needs to update viewports.activeViewportIndex to the first updated viewport
//const viewportIndex = actionData.viewportIndex;
//dispatch(setViewportActive(viewportIndex));
// Needs to update timepointsManager.measurements state to set active measurementId
// TODO: Not yet implemented
//dispatch(setActiveMeasurement(measurementData.measurementId))
// (later): Needs to set some property on state.extensions.cornerstone to synchronize viewport scrolling
},
};
};
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const { timepoints, saveFunction, measurementCollection } = propsFromState;
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
return {
timepoints,
saveFunction,
measurementCollection,
onSaveComplete,
selectedMeasurementNumber,
...propsFromDispatch,
onItemClick: (event, measurementData) => {
// TODO: Add timepointId to .data for measurementData?
// TODO: Tooltype should be on the level below? This should
// provide the entire row item?
const viewportsState = propsFromState.viewports;
const timepointManagerState = propsFromState.timepointManager;
// TODO: invertViewportTimepointsOrder should be stored in / read from user preferences
// TODO: childToolKey should come from the measurement table when it supports child tools
const options = {
invertViewportTimepointsOrder: false,
childToolKey: null,
};
propsFromDispatch.dispatchJumpToRowItem(
measurementData,
viewportsState,
timepointManagerState,
options
);
},
onRelabelClick: (event, measurementData) => {
const viewportsState = propsFromState.viewports;
propsFromDispatch.dispatchRelabel(event, measurementData, viewportsState);
},
onEditDescriptionClick: (event, measurementData) => {
const viewportsState = propsFromState.viewports;
propsFromDispatch.dispatchEditDescription(
event,
measurementData,
viewportsState
);
},
onDeleteClick: (event, measurementData) => {
const { MeasurementHandlers } = OHIF.measurements;
MeasurementHandlers.onRemoved({
detail: {
toolType: measurementData.toolType,
measurementData: {
_id: measurementData.measurementId,
lesionNamingNumber: measurementData.lesionNamingNumber,
measurementNumber: measurementData.measurementNumber,
},
},
});
},
};
};
const ConnectedMeasurementTable = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(MeasurementTable);
export default ConnectedMeasurementTable;
@@ -1,87 +0,0 @@
import React from 'react';
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
import init from './init.js';
import LabellingFlow from '../../components/Labelling/LabellingFlow';
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'measurements-table',
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration });
},
getPanelModule({ servicesManager, commandsManager }) {
const { UINotificationService, UIDialogService } = servicesManager.services;
const showLabellingDialog = (props, measurementData) => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.dismiss({ id: 'labelling' });
UIDialogService.create({
id: 'labelling',
centralize: true,
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
contentProps: {
measurementData,
labellingDoneCallback: () =>
UIDialogService.dismiss({ id: 'labelling' }),
updateLabelling: ({ location, description, response }) => {
measurementData.location = location || measurementData.location;
measurementData.description = description || '';
measurementData.response = response || measurementData.response;
commandsManager.runCommand(
'updateTableWithNewMeasurementData',
measurementData
);
},
...props,
},
});
};
const ExtendedConnectedMeasurementTable = () => (
<ConnectedMeasurementTable
onRelabel={tool =>
showLabellingDialog(
{ editLocation: true, skipAddLabelButton: true },
tool
)
}
onEditDescription={tool =>
showLabellingDialog({ editDescriptionOnDialog: true }, tool)
}
onSaveComplete={message => {
if (UINotificationService) {
UINotificationService.show(message);
}
}}
/>
);
return {
menuOptions: [
{
icon: 'list',
label: 'Measurements',
target: 'measurement-panel',
},
],
components: [
{
id: 'measurement-panel',
component: ExtendedConnectedMeasurementTable,
},
],
defaultContext: ['VIEWER'],
};
},
};
@@ -1,251 +0,0 @@
import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import throttle from 'lodash.throttle';
import LabellingFlow from '../../components/Labelling/LabellingFlow';
import ToolContextMenu from '../../connectedComponents/ToolContextMenu';
const {
onAdded,
onRemoved,
onModified,
} = OHIF.measurements.MeasurementHandlers;
const MEASUREMENT_ACTION_MAP = {
added: onAdded,
removed: onRemoved,
modified: throttle(event => {
return onModified(event);
}, 300),
};
/**
*
*
* @export
* @param {Object} servicesManager
* @param {Object} configuration
*/
export default function init({
servicesManager,
commandsManager,
configuration,
}) {
const { UIDialogService } = servicesManager.services;
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
const onMeasurementsChanged = (action, event) => {
return MEASUREMENT_ACTION_MAP[action](event);
};
const onMeasurementAdded = onMeasurementsChanged.bind(this, 'added');
const onMeasurementRemoved = onMeasurementsChanged.bind(this, 'removed');
const onMeasurementModified = onMeasurementsChanged.bind(this, 'modified');
const onLabelmapModified = onMeasurementsChanged.bind(
this,
'labelmapModified'
);
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client.x) || 0,
y: (event && event.currentPoints.client.y) || 0,
});
const _updateLabellingHandler = (labellingData, measurementData) => {
const { location, description, response } = labellingData;
if (location) {
measurementData.location = location;
}
measurementData.description = description || '';
if (response) {
measurementData.response = response;
}
commandsManager.runCommand(
'updateTableWithNewMeasurementData',
measurementData
);
};
const showLabellingDialog = (props, contentProps, measurementData) => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.create({
id: 'labelling',
isDraggable: false,
showOverlay: true,
centralize: true,
content: LabellingFlow,
contentProps: {
measurementData,
labellingDoneCallback: () =>
UIDialogService.dismiss({ id: 'labelling' }),
updateLabelling: labellingData =>
_updateLabellingHandler(labellingData, measurementData),
...contentProps,
},
...props,
});
};
const onRightClick = event => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.dismiss({ id: 'context-menu' });
UIDialogService.create({
id: 'context-menu',
isDraggable: false,
preservePosition: false,
defaultPosition: _getDefaultPosition(event.detail),
content: ToolContextMenu,
contentProps: {
eventData: event.detail,
onDelete: (nearbyToolData, eventData) => {
const element = eventData.element;
commandsManager.runCommand('removeToolState', {
element,
toolType: nearbyToolData.toolType,
tool: nearbyToolData.tool,
});
},
onClose: () => UIDialogService.dismiss({ id: 'context-menu' }),
onSetLabel: (eventData, measurementData) => {
showLabellingDialog(
{ centralize: true, isDraggable: false },
{ skipAddLabelButton: true, editLocation: true },
measurementData
);
},
onSetDescription: (eventData, measurementData) => {
showLabellingDialog(
{ defaultPosition: _getDefaultPosition(eventData) },
{ editDescriptionOnDialog: true },
measurementData
);
},
},
});
};
const onTouchPress = event => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.create({
eventData: event.detail,
content: ToolContextMenu,
contentProps: {
isTouchEvent: true,
},
});
};
const onTouchStart = () => resetLabelligAndContextMenu();
const onMouseClick = () => resetLabelligAndContextMenu();
const resetLabelligAndContextMenu = () => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.dismiss({ id: 'context-menu' });
UIDialogService.dismiss({ id: 'labelling' });
};
// TODO: This makes scrolling painfully slow
// const onNewImage = ...
/*
* Because click gives us the native "mouse up", buttons will always be `0`
* Need to fallback to event.which;
*
*/
const handleClick = cornerstoneMouseClickEvent => {
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
const isRightClick = mouseUpEvent.which === 3;
if (isRightClick) {
onRightClick(cornerstoneMouseClickEvent);
} else {
onMouseClick(cornerstoneMouseClickEvent);
}
};
function elementEnabledHandler(evt) {
const element = evt.detail.element;
element.addEventListener(
csTools.EVENTS.MEASUREMENT_ADDED,
onMeasurementAdded
);
element.addEventListener(
csTools.EVENTS.MEASUREMENT_REMOVED,
onMeasurementRemoved
);
element.addEventListener(
csTools.EVENTS.MEASUREMENT_MODIFIED,
onMeasurementModified
);
element.addEventListener(
csTools.EVENTS.LABELMAP_MODIFIED,
onLabelmapModified
);
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
// TODO: This makes scrolling painfully slow
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
}
function elementDisabledHandler(evt) {
const element = evt.detail.element;
element.removeEventListener(
csTools.EVENTS.MEASUREMENT_ADDED,
onMeasurementAdded
);
element.removeEventListener(
csTools.EVENTS.MEASUREMENT_REMOVED,
onMeasurementRemoved
);
element.removeEventListener(
csTools.EVENTS.MEASUREMENT_MODIFIED,
onMeasurementModified
);
element.removeEventListener(
csTools.EVENTS.LABELMAP_MODIFIED,
onLabelmapModified
);
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
// TODO: This makes scrolling painfully slow
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
}
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
elementEnabledHandler
);
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_DISABLED,
elementDisabledHandler
);
}
@@ -1,4 +0,0 @@
import GenericViewerCommands from './GenericViewerCommands/index.js';
import MeasurementsPanel from './MeasurementsPanel/index.js';
export { GenericViewerCommands, MeasurementsPanel };
Whitespace-only changes.
@@ -1,37 +0,0 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
@@ -1,6 +0,0 @@
.editDescriptionDialog {
position: relative;
z-index: 300;
width: 320px;
transition: all 300ms linear;
}
@@ -1,64 +0,0 @@
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import SimpleDialog from '../SimpleDialog/SimpleDialog.js';
import './EditDescriptionDialog.css';
export default class EditDescriptionDialog extends Component {
static propTypes = {
description: PropTypes.string,
measurementData: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
description: props.measurementData.description || '',
};
}
componentDidUpdate(prevProps) {
if (this.props.description !== prevProps.description) {
this.setState({
description: this.props.description,
});
}
}
render() {
return (
<SimpleDialog
headerTitle="Edit Description"
onClose={this.onClose}
onConfirm={this.onConfirm}
rootClass="editDescriptionDialog"
>
<input
value={this.state.description}
className="simpleDialogInput"
id="description"
autoComplete="off"
autoFocus
onChange={this.handleChange}
/>
</SimpleDialog>
);
}
onClose = () => {
this.props.onCancel();
};
onConfirm = e => {
e.preventDefault();
this.props.onUpdate(this.state.description);
};
handleChange = event => {
this.setState({ description: event.target.value });
};
}
@@ -1,3 +0,0 @@
import Header from './Header';
export default Header;
@@ -1,126 +0,0 @@
.labellingComponent {
position: relative;
text-align: center;
z-index: 999;
transition: all 200ms linear;
max-height: 500px;
}
.labellingComponent .selectedLabel, .labellingComponent .selectedDescription {
padding: 5px;
background-color: white;
width: 150px;
}
.labellingComponent .addLabelButton {
color: #000000;
background-color: #20a5d6;
border: 2px solid #44626f;
border-radius: 14px;
cursor: pointer;
font-weight: bold;
font-size: 13px;
line-height: 24px;
opacity: 1;
padding: 0 14px;
transition: opacity 0.3s ease;
outline: none;
cursor: pointer;
}
.labellingComponent .commonButton {
border: 1px solid #44626f;
color: #ffffff;
background-color: #000000;
border-radius: 16px;
font-weight: bold;
font-size: 13px;
line-height: 26px;
padding: 0 12px;
margin: 10px 5px 0 0;
outline: none;
cursor: pointer;
}
.labellingComponent .locationDescriptionWrapper {
background-color: #ffffff;
line-height: 46px;
height: 46px;
font-size: 13px;
position: relative;
width: 290px;
min-width: 260px;
padding: 0 12px;
margin: 0 auto;
display: inline-block;
}
.labellingComponent .locationDescriptionWrapper .location {
transition: all 300ms linear;
position: absolute;
white-space: nowrap;
bottom: 0;
}
.labellingComponent.editDescription .locationDescriptionWrapper .location {
bottom: 38px;
}
.labellingComponent .locationDescriptionWrapper #descriptionInput {
transition-delay: all 300ms linear;
visibility: hidden;
outline: none;
height: 46px;
width: 100%;
line-height: 20px;
font-size: 13px;
border: none;
}
.labellingComponent.editDescription .locationDescriptionWrapper #descriptionInput {
visibility: visible;
}
.labellingComponent.editDescription .location {
color: #337ab7;
}
.labellingComponent .commonButtons, .labellingComponent.editDescription .editDescriptionButtons {
display: block;
margin-left: 55px;
}
.labellingComponent.editDescription .commonButtons, .labellingComponent .editDescriptionButtons {
display: none;
}
.labellingComponent .commonButtons {
text-align: center;
margin-left: 55px;
}
.labellingComponent .commonButton.left {
float: left;
}
.labellingComponent .commonButton.right {
float: right;
}
.labellingComponent .checkIconWrapper {
display: inline-block;
background-color: #337ab7;
border-radius: 46px;
width: 46px;
height: 46px;
margin-right: 10px;
vertical-align: bottom;
cursor: pointer;
}
.labellingComponent .checkIcon {
width: 20px;
height: 20px;
margin: 13px;
fill: black;
}
@@ -1,263 +0,0 @@
import { Icon, SelectTree } from '@ohif/ui';
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import LabellingTransition from './LabellingTransition.js';
import OHIFLabellingData from './OHIFLabellingData.js';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import './LabellingFlow.css';
const LabellingFlow = ({
measurementData,
editLocation,
editDescription,
skipAddLabelButton,
updateLabelling,
labellingDoneCallback,
editDescriptionOnDialog,
}) => {
const [fadeOutTimer, setFadeOutTimer] = useState();
const [showComponent, setShowComponent] = useState(true);
const descriptionInput = useRef();
const [state, setState] = useState({
measurementData,
editLocation,
editDescription,
skipAddLabelButton,
});
useEffect(() => {
const newMeasurementData = cloneDeep(measurementData);
if (editDescription) {
newMeasurementData.description = undefined;
}
if (editLocation) {
newMeasurementData.location = undefined;
}
let newEditLocation = editLocation;
if (!editDescription && !editLocation) {
newEditLocation = true;
}
setState(state => ({
...state,
editLocation: newEditLocation,
measurementData: newMeasurementData,
}));
}, [editDescription, editLocation, measurementData]);
useEffect(() => {
if (descriptionInput.current) {
descriptionInput.current.focus();
}
}, [state]);
const relabel = event =>
setState(state => ({ ...state, editLocation: true }));
const setDescriptionUpdateMode = () => {
descriptionInput.current.focus();
setState(state => ({ ...state, editDescription: true }));
};
const descriptionCancel = () => {
const { description = '' } = cloneDeep(state);
descriptionInput.current.value = description;
setState(state => ({ ...state, editDescription: false }));
};
const handleKeyPress = event => {
if (event.key === 'Enter') {
descriptionSave();
}
};
const descriptionSave = () => {
const description = descriptionInput.current.value;
updateLabelling({ description });
setState(state => ({
...state,
description,
editDescription: false,
}));
};
const selectTreeSelectCallback = (event, itemSelected) => {
const location = itemSelected.value;
const locationLabel = itemSelected.label;
updateLabelling({ location });
setState(state => ({
...state,
editLocation: false,
measurementData: {
...state.measurementData,
location,
locationLabel,
},
}));
};
const showLabelling = () => {
setState(state => ({
...state,
skipAddLabelButton: true,
editLocation: false,
}));
};
/*
* Waits for 1 sec to dismiss the labelling component.
*
*/
const fadeOutAndLeave = () =>
setFadeOutTimer(setTimeout(fadeOutAndLeaveFast, 1000));
const fadeOutAndLeaveFast = () => setShowComponent(false);
const clearFadeOutTimer = () => {
if (fadeOutTimer) {
clearTimeout(fadeOutTimer);
setFadeOutTimer(null);
}
};
const descriptionDialogUpdate = description => {
updateLabelling({ description });
labellingDoneCallback();
};
const labellingStateFragment = () => {
const { skipAddLabelButton, editLocation, measurementData } = state;
const { description, locationLabel, location } = measurementData;
if (!skipAddLabelButton) {
return (
<button
type="button"
className="addLabelButton"
onClick={showLabelling}
>
{location ? 'Edit' : 'Add'} Label
</button>
);
} else {
if (editLocation) {
return (
<SelectTree
items={OHIFLabellingData}
columns={1}
onSelected={selectTreeSelectCallback}
selectTreeFirstTitle="Assign Label"
/>
);
} else {
return (
<>
<div className="checkIconWrapper" onClick={fadeOutAndLeaveFast}>
<Icon name="check" className="checkIcon" />
</div>
<div className="locationDescriptionWrapper">
<div className="location">{locationLabel}</div>
<div className="description">
<input
id="descriptionInput"
ref={descriptionInput}
defaultValue={description || ''}
autoComplete="off"
onKeyPress={handleKeyPress}
/>
</div>
</div>
<div className="commonButtons">
<button
type="button"
className="commonButton left"
onClick={relabel}
>
Relabel
</button>
<button
type="button"
className="commonButton right"
onClick={setDescriptionUpdateMode}
>
{description ? 'Edit ' : 'Add '}
Description
</button>
</div>
<div className="editDescriptionButtons">
<button
type="button"
className="commonButton left"
onClick={descriptionCancel}
>
Cancel
</button>
<button
type="button"
className="commonButton right"
onClick={descriptionSave}
>
Save
</button>
</div>
</>
);
}
}
};
if (editDescriptionOnDialog) {
return (
<EditDescriptionDialog
onCancel={labellingDoneCallback}
onUpdate={descriptionDialogUpdate}
measurementData={state.measurementData}
/>
);
}
return (
<LabellingTransition
displayComponent={showComponent}
onTransitionExit={labellingDoneCallback}
>
<>
<div
className={`labellingComponent ${state.editDescription &&
'editDescription'}`}
onMouseLeave={fadeOutAndLeave}
onMouseEnter={clearFadeOutTimer}
>
{labellingStateFragment()}
</div>
</>
</LabellingTransition>
);
};
LabellingFlow.propTypes = {
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
initialTopDistance: PropTypes.number,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
editDescriptionOnDialog: PropTypes.bool,
};
LabellingFlow.defaultProps = {
skipAddLabelButton: false,
editLocation: false,
editDescription: false,
editDescriptionOnDialog: false,
};
export default LabellingFlow;
@@ -1,21 +0,0 @@
.labelling-appear {
opacity: 0;
}
.labelling-appear.labelling-appear-active {
opacity: 1;
transition: opacity 500ms linear;
}
.labelling-exit {
opacity: 1;
}
.labelling-exit.labelling-exit-active {
opacity: 0;
transition: opacity 500ms linear;
}
.labelling-exit-done {
opacity: 0;
}
@@ -1,31 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { CSSTransition } from 'react-transition-group';
import './LabellingTransition.css';
// If these variables changes, CSS must be updated
const transitionDuration = 500;
const transitionClassName = 'labelling';
const transitionOnAppear = true;
export default class LabellingTransition extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
displayComponent: PropTypes.bool.isRequired,
onTransitionExit: PropTypes.func.isRequired,
};
render() {
return (
<CSSTransition
in={this.props.displayComponent}
appear={transitionOnAppear}
timeout={transitionDuration}
classNames={transitionClassName}
onExited={this.props.onTransitionExit}
>
{this.props.children}
</CSSTransition>
);
}
}
@@ -1,39 +0,0 @@
const items = [
'Abdomen/Chest Wall',
'Adrenal',
'Bladder',
'Bone',
'Brain',
'Breast',
'Colon',
'Esophagus',
'Extremities',
'Gallbladder',
'Kidney',
'Liver',
'Lung',
'Lymph Node',
'Mediastinum/Hilum',
'Muscle',
'Neck',
'Other Soft Tissue',
'Ovary',
'Pancreas',
'Pelvis',
'Peritoneum/Omentum',
'Prostate',
'Retroperitoneum',
'Small Bowel',
'Spleen',
'Stomach',
'Subcutaneous',
];
const OHIFLabellingData = items.map(item => {
return {
label: item,
value: item,
};
});
export default OHIFLabellingData;
@@ -1,32 +0,0 @@
import React from 'react';
const Bar = ({ progress, animationDuration }) => (
<div
style={{
background: '#29d',
height: 2,
left: 0,
marginLeft: `${(-1 + progress) * 100}%`,
position: 'fixed',
top: 0,
transition: `margin-left ${animationDuration}ms linear`,
width: '100%',
zIndex: 1031,
}}
>
<div
style={{
boxShadow: '0 0 10px #29d, 0 0 5px #29d',
display: 'block',
height: '100%',
opacity: 1,
position: 'absolute',
right: 0,
transform: 'rotate(3deg) translate(0px, -4px)',
width: 100,
}}
/>
</div>
);
export default Bar;
@@ -1,15 +0,0 @@
import React from 'react';
const Container = ({ children, isFinished, animationDuration }) => (
<div
style={{
opacity: isFinished ? 0 : 1,
pointerEvents: 'none',
transition: `opacity ${animationDuration}ms linear`,
}}
>
{children}
</div>
);
export default Container;
@@ -1,4 +0,0 @@
import Bar from './Bar.js';
import Container from './Container.js';
export { Bar, Container };
@@ -1,33 +0,0 @@
.header-brand {
height: 30px;
text-decoration: none;
text-decoration-color: none;
display: flex;
align-items: center;
color: var(--text-primary-color);
}
.header-brand:hover,
.header-brand:active,
.header-band:visited {
color: var(--text-primary-color);
text-decoration: none;
}
.header-logo-text {
width: 224px;
height: auto;
margin-left: 8px;
}
.header-logo-image {
height: 30px;
width: 30px;
font-size: 30px;
}
@media only screen and (max-width: 768px) {
.header-logo-text {
display: none;
}
}
@@ -1,27 +0,0 @@
import './OHIFLogo.css';
import { Icon } from '@ohif/ui';
import React from 'react';
function OHIFLogo() {
return (
<a
target="_blank"
rel="noopener noreferrer"
className="header-brand"
href="http://ohif.org"
>
<Icon name="ohif-logo" className="header-logo-image" />
{/* Logo text would fit smaller displays at two lines:
*
* Open Health
* Imaging Foundation
*
* Or as `OHIF` on really small displays
*/}
<Icon name="ohif-text-logo" className="header-logo-text" />
</a>
);
}
export default OHIFLogo;
@@ -1,41 +0,0 @@
import './SidePanel.css';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
class SidePanel extends Component {
static propTypes = {
from: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
children: PropTypes.node,
width: PropTypes.string,
};
render() {
const fromSideClass =
this.props.from === 'right' ? 'from-right' : 'from-left';
const styles = this.props.width
? {
maxWidth: this.props.width,
marginRight: this.props.isOpen
? '0'
: Number.parseInt(this.props.width) * -1,
}
: {};
return (
<section
style={styles}
className={classNames('sidepanel', fromSideClass, {
'is-open': this.props.isOpen,
})}
>
{this.props.children}
</section>
);
}
}
export default SidePanel;
@@ -1,133 +0,0 @@
.simpleDialog {
position: fixed;
border: 0;
border-radius: 6px;
background-color: #151a1f;
}
.simpleDialog .header {
border-bottom-width: 3px;
border-bottom-style: solid;
border-bottom-color: #000;
padding: 19px 22px 17px;
position: relative;
}
.simpleDialog .header .title {
font-size: 20px;
font-weight: 600;
line-height: 24px;
padding-right: 40px;
color: #91b9cd;
margin: 0;
}
.simpleDialog .header .closeBtn {
height: 20px;
opacity: 1;
overflow: hidden;
padding: 2px;
text-align: center;
text-shadow: none;
width: 20px;
color: #91b9cd;
cursor: pointer;
position: absolute;
right: 21px;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
transition: color 0.3s ease;
}
.simpleDialog .header .closeIcon {
color: transparent;
display: block;
font-size: 0;
height: 100%;
line-height: 0;
overflow: hidden;
position: relative;
width: 100%;
}
.simpleDialog .header .closeIcon:after,
.simpleDialog .header .closeIcon:before {
content: ' ';
display: block;
height: 2px;
transition: background-color 0.3s ease;
width: 19px;
background-color: #91b9cd;
}
.simpleDialog .header .closeIcon:before {
left: 1px;
position: absolute;
top: 1px;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-transform-origin: 1px 50%;
transform-origin: 1px 50%;
}
.simpleDialog .header .closeIcon:after {
right: 1px;
position: absolute;
top: 1px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
-webkit-transform-origin: calc(100% - 1px) 50%;
transform-origin: calc(100% - 1px) 50%;
}
.simpleDialog .content {
padding: 16px 22px 25px;
position: relative;
color: #fff;
}
.simpleDialog .content .simpleDialogSelect,
.simpleDialog .content .simpleDialogInput {
background-color: #fff;
color: #000;
border: 0;
border-radius: 2px;
font-size: 14px;
height: 30px;
width: 100%;
line-height: 16px;
padding: 8px 9px 6px;
margin-top: 10px;
display: block;
}
.simpleDialog .content .simpleDialogInputLabel {
font-size: 14px;
font-weight: 700;
line-height: 16px;
color: #fff;
}
.simpleDialog .footer {
padding: 15px;
text-align: right;
}
.simpleDialog .footer .btn {
transition: background-color 0.3s ease;
color: #000;
border: 0;
border-radius: 4px;
font-size: 15px;
font-weight: 400;
height: 37px;
line-height: 37px;
padding: 0 12px;
margin-bottom: 0;
margin-left: 5px;
}
.simpleDialog .footer .btn-confirm {
color: #fff;
background-color: #337ab7;
}
@@ -1,56 +0,0 @@
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import './SimpleDialog.css';
class SimpleDialog extends Component {
static defaultProps = {
componentStyle: {},
rootClass: '',
};
render() {
return (
<div
className={`simpleDialog ${this.props.rootClass}`}
ref={this.props.componentRef}
style={this.props.componentStyle}
>
<form onSubmit={this.props.onConfirm}>
<div className="header">
<span className="closeBtn" onClick={this.props.onClose}>
<span className="closeIcon">x</span>
</span>
<h4 className="title">{this.props.headerTitle}</h4>
</div>
<div className="content">{this.props.children}</div>
<div className="footer">
<button
type="button"
className="btn btn-cancel"
onClick={this.props.onClose}
>
Cancel
</button>
<button
type="button"
className="btn btn-confirm"
onClick={this.props.onConfirm}
>
Confirm
</button>
</div>
</form>
</div>
);
}
}
SimpleDialog.propTypes = {
headerTitle: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
};
export default SimpleDialog;
@@ -1,42 +0,0 @@
import { Component } from "react";
import PropTypes from "prop-types";
import OHIF from "@ohif/core";
class StudyLoadingMonitor extends Component {
static propTypes = {
studies: PropTypes.array.isRequired,
setStudyLoadingProgress: PropTypes.func.isRequired,
clearStudyLoadingProgress: PropTypes.func.isRequired
};
componentDidMount() {
// TODO: This is pretty ugly. The thing is that the StudyLoadingListener
// needs to update the Redux store, but shouldn't know that it exists.
// I am therefore passing in some functions to update the store instead,
// but this should definitely be cleaned up somehow.
const options = {
_setProgressData: (progressId, progressData) => {
this.props.setStudyLoadingProgress(progressId, progressData);
},
_clearProgressById: progressId => {
this.props.clearStudyLoadingProgress(progressId);
}
};
const { StudyLoadingListener } = OHIF.classes;
this.studyLoadingListener = StudyLoadingListener.getInstance(options);
this.studyLoadingListener.clear();
this.studyLoadingListener.addStudies(this.props.studies);
}
render() {
return null;
}
componentWillUnmount() {
// Destroy stack loading listeners when we close the viewer
this.studyLoadingListener.clear();
}
}
export default StudyLoadingMonitor;
@@ -1,29 +0,0 @@
import { Component } from "react";
import PropTypes from "prop-types";
import OHIF from "@ohif/core";
const { StudyPrefetcher } = OHIF.classes;
class StudyPrefetcherComponent extends Component {
static propTypes = {
studies: PropTypes.array
};
componentDidMount() {
const { studies } = this.props;
this.studyPrefetcher = StudyPrefetcher.getInstance();
this.studyPrefetcher.setStudies(studies);
}
render() {
return null;
}
componentWillUnmount() {
// Stop prefetching when we close the viewer
this.studyPrefetcher.destroy();
}
}
export default StudyPrefetcherComponent;
@@ -1,72 +0,0 @@
import React, { useState, useSelector } from 'react';
import PropTypes from 'prop-types';
import i18n from '@ohif/i18n';
import { TabFooter, LanguageSwitcher, useSnackbarContext } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import './GeneralPreferences.styl';
/**
* General Preferences tab
* It renders the General Preferences content
*
* @param {object} props component props
* @param {function} props.onClose
*/
function GeneralPreferences({ onClose }) {
const { t } = useTranslation('UserPreferencesModal');
const snackbar = useSnackbarContext();
const currentLanguage = i18n.language;
const { availableLanguages } = i18n;
const [language, setLanguage] = useState(currentLanguage);
const onResetPreferences = () => {
setLanguage(i18n.defaultLanguage);
};
const onSave = () => {
i18n.changeLanguage(language);
onClose();
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
const hasErrors = false;
return (
<React.Fragment>
<div className="GeneralPreferences">
<div className="language">
<label htmlFor="language-select" className="languageLabel">
Language
</label>
<LanguageSwitcher
language={language}
onLanguageChange={setLanguage}
languages={availableLanguages}
/>
</div>
</div>
<TabFooter
onResetPreferences={onResetPreferences}
onSave={onSave}
onCancel={onClose}
hasErrors={hasErrors}
t={t}
/>
</React.Fragment>
);
}
GeneralPreferences.propTypes = {
onClose: PropTypes.func,
};
export { GeneralPreferences };
@@ -1,18 +0,0 @@
.GeneralPreferences
display: flex;
padding: 20px 0;
.language
display: flex;
width: 50%;
.languageLabel
display: flex;
flex-basis: 0;
flex-grow: 1.5;
justify-content: flex-end;
padding-right: 20px;
.language-select
flex-grow: 1.5;
flex-basis: 0;
@@ -1,203 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useSnackbarContext, TabFooter, HotkeyField } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import { hotkeysValidators } from './hotkeysValidators';
import { MODIFIER_KEYS } from './hotkeysConfig';
import { hotkeysManager } from '../../App';
import './HotkeysPreferences.styl';
/**
* Take hotkeyDefenintions and build an initialState to be used into the component state
*
* @param {Object} hotkeyDefinitions
* @returns {Object} initialState
*/
const initialState = hotkeyDefinitions => ({
hotkeys: { ...hotkeyDefinitions },
errors: {},
});
/**
* Take the updated command and keys and validate the changes with all validators
*
* @param {Object} arguments
* @param {string} arguments.commandName command name string to be updated
* @param {array} arguments.pressedKeys new array of keys to be added for the commandName
* @param {array} arguments.hotkeys all hotkeys currently into the app
* @returns {Object} {errorMessage} errorMessage coming from any of the validator or undefined if none
*/
const validateCommandKey = ({ commandName, pressedKeys, hotkeys }) => {
for (const validator of hotkeysValidators) {
const validation = validator({
commandName,
pressedKeys,
hotkeys,
});
if (validation && validation.hasError) {
return validation;
}
}
return {
errorMessage: undefined,
};
};
/**
* Take all hotkeys and split the list into two lists
*
* @param {array} hotkeys list of all hotkeys
* @returns {array} array containing two arrays of keys
*/
const splitHotkeys = hotkeys => {
const splitedHotkeys = [];
const arrayHotkeys = Object.entries(hotkeys);
if (arrayHotkeys.length) {
const halfwayThrough = Math.ceil(arrayHotkeys.length / 2);
splitedHotkeys.push(arrayHotkeys.slice(0, halfwayThrough));
splitedHotkeys.push(
arrayHotkeys.slice(halfwayThrough, arrayHotkeys.length)
);
}
return splitedHotkeys;
};
/**
* HotkeysPreferences tab
* It renders all hotkeys displayed into columns/rows
*
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
* @param {object} props component props
* @param {string} props.onClose
*/
function HotkeysPreferences({ onClose }) {
const { t } = useTranslation('UserPreferencesModal');
const { hotkeyDefaults, hotkeyDefinitions } = hotkeysManager;
const [state, setState] = useState(initialState(hotkeyDefinitions));
const snackbar = useSnackbarContext();
const onResetPreferences = () => {
const defaultHotKeyDefinitions = {};
hotkeyDefaults.map(item => {
const { commandName, ...values } = item;
defaultHotKeyDefinitions[commandName] = { ...values };
});
setState(initialState(defaultHotKeyDefinitions));
};
const onSave = () => {
const { hotkeys } = state;
hotkeysManager.setHotkeys(hotkeys);
localStorage.setItem('hotkey-definitions', JSON.stringify(hotkeys));
onClose();
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
const onHotkeyChanged = (commandName, hotkeyDefinition, keys) => {
const { errorMessage } = validateCommandKey({
commandName,
pressedKeys: keys,
hotkeys: state.hotkeys,
});
setState(prevState => ({
hotkeys: {
...prevState.hotkeys,
[commandName]: { ...hotkeyDefinition, keys },
},
errors: {
...prevState.errors,
[commandName]: errorMessage,
},
}));
};
const hasErrors = Object.keys(state.errors).some(key => !!state.errors[key]);
const hasHotkeys = Object.keys(state.hotkeys).length;
const splitedHotkeys = splitHotkeys(state.hotkeys);
return (
<React.Fragment>
<div className="HotkeysPreferences">
{hasHotkeys ? (
<div className="hotkeyTable">
{splitedHotkeys.map((hotkeys, index) => {
return (
<div className="hotkeyColumn" key={index}>
<div className="hotkeyHeader">
<div className="headerItemText text-right">Function</div>
<div className="headerItemText text-center">Shortcut</div>
</div>
{hotkeys.map(hotkey => {
const commandName = hotkey[0];
const hotkeyDefinition = hotkey[1];
const { keys, label } = hotkeyDefinition;
const errorMessage = state.errors[hotkey[0]];
const handleChange = keys => {
onHotkeyChanged(commandName, hotkeyDefinition, keys);
};
return (
<div key={commandName} className="hotkeyRow">
<div className="hotkeyLabel">{label}</div>
<div
data-key="defaultTool"
className={classnames(
'wrapperHotkeyInput',
errorMessage ? 'stateError' : ''
)}
>
<HotkeyField
keys={keys}
modifier_keys={MODIFIER_KEYS}
handleChange={handleChange}
classNames={'preferencesInput'}
></HotkeyField>
<span className="preferencesInputErrorMessage">
{errorMessage}
</span>
</div>
</div>
);
})}
</div>
);
})}
</div>
) : (
'Hotkeys definitions is empty'
)}
</div>
<TabFooter
onResetPreferences={onResetPreferences}
onSave={onSave}
onCancel={onClose}
hasErrors={hasErrors}
t={t}
/>
</React.Fragment>
);
}
HotkeysPreferences.propTypes = {
onClose: PropTypes.func,
};
export { HotkeysPreferences };
@@ -1,46 +0,0 @@
.HotkeysPreferences
display: flex
padding: 20px
.hotkeyTable
display: flex
flex-direction: row
flex-basis: 0
flex-grow: 1.5
.hotkeyColumn
display: flex
flex-direction: column
flex-basis: 0
flex-grow: 1.5
.hotkeyHeader
display: flex
flex-direction: row
margin-bottom: 10px
.headerItemText
flex-basis: 0
flex-grow: 1.5
padding: 5px 15px 5px 0
.hotkeyRow
display: flex
.wrapperHotkeyInput
margin-bottom: 5px
cursor: pointer
display: flex
flex-direction: column
flex-basis: 0
flex-grow: 1.5
.hotkeyLabel
padding: 5px 15px 5px 0
text-align: right
flex-basis: 0
flex-grow: 1.5
.stateError
.preferencesInput
background-color: var(--state-error)
@@ -1,42 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TabComponents } from '@ohif/ui';
// Tabs
import { HotkeysPreferences } from './HotkeysPreferences';
import { WindowLevelPreferences } from './WindowLevelPreferences';
import { GeneralPreferences } from './GeneralPreferences';
import './UserPreferences.styl';
const tabs = [
{
name: 'Hotkeys',
Component: HotkeysPreferences,
customProps: {},
},
{
name: 'General',
Component: GeneralPreferences,
customProps: {},
},
{
name: 'Window Level',
Component: WindowLevelPreferences,
customProps: {},
},
];
function UserPreferences({ hide }) {
const customProps = {
onClose: hide,
};
return <TabComponents tabs={tabs} customProps={customProps} />;
}
UserPreferences.propTypes = {
hide: PropTypes.func,
};
export { UserPreferences };
@@ -1,26 +0,0 @@
.preferencesInputErrorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase
.preferencesInput
font-weight: 400
cursor: pointer
transition: background-color .3s ease,border-color .3s ease
background-color: var(--ui-gray)
color: var(--text-primary-color)
border-color: var(--ui-border-coolor)
border: 0
border-radius: 2px
font-size: 14px
height: 30px
width: 100%
line-height: 16px
padding: 8px 9px 6px
text-align: center
.preferencesInput:focus
border-color: var(--active-color)
background-color: var(--ui-gray-dark)
box-shadow: 0 0 0 2px var(--active-color) !important
outline: 0
@@ -1,128 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { redux } from '@ohif/core';
import { TabFooter, useSnackbarContext } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
const { actions } = redux;
import './WindowLevelPreferences.styl';
function WindowLevelPreferences({ onClose }) {
const dispatch = useDispatch();
const windowLevelData = useSelector(state => {
const { preferences = {} } = state;
const { windowLevelData } = preferences;
return windowLevelData;
});
const [state, setState] = useState({
values: { ...windowLevelData },
});
const { t } = useTranslation('UserPreferencesModal');
const onResetPreferences = () => {};
const hasErrors = false;
const onSave = () => {
dispatch(actions.setUserPreferences({ windowLevelData: state.values }));
onClose();
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
const snackbar = useSnackbarContext();
const handleInputChange = event => {
const $target = event.target;
const { key, inputname } = $target.dataset;
const inputValue = $target.value;
if (!state.values[key] || !state.values[key][inputname]) {
return;
}
setState(prevState => ({
...prevState,
values: {
...prevState.values,
[key]: {
...prevState.values[key],
[inputname]: inputValue,
},
},
}));
};
return (
<React.Fragment>
<div className="WindowLevelPreferences">
<div className="wlColumn">
<div className="wlRow header">
<div className="wlColumn preset">Preset</div>
<div className="wlColumn description">Description</div>
<div className="wlColumn window">Window</div>
<div className="wlColumn level">Level</div>
</div>
{Object.keys(state.values).map((key, index) => {
return (
<div className="wlRow" key={key}>
<div className="wlColumn preset">{key}</div>
<div className="wlColumn description">
<input
type="text"
className="preferencesInput"
value={state.values[key].description}
data-key={key}
data-inputname="description"
onChange={handleInputChange}
/>
</div>
<div className="wlColumn window">
<input
type="number"
className="preferencesInput"
value={state.values[key].window}
data-key={key}
data-inputname="window"
onChange={handleInputChange}
/>
</div>
<div className="wlColumn level">
<input
type="number"
className="preferencesInput"
value={state.values[key].level}
data-key={key}
data-inputname="level"
onChange={handleInputChange}
/>
</div>
</div>
);
})}
</div>
</div>
<TabFooter
onResetPreferences={onResetPreferences}
onSave={onSave}
onCancel={onClose}
hasErrors={hasErrors}
t={t}
/>
</React.Fragment>
);
}
WindowLevelPreferences.propTypes = {
onClose: PropTypes.func,
};
export { WindowLevelPreferences };
@@ -1,28 +0,0 @@
.WindowLevelPreferences
display: flex
padding: 20px 0
text-align: center
.wlColumn
display: flex
flex-direction: column
flex-basis: 0
flex-grow: 1.5
margin: 0 5px
.wlRow
display: flex
flex-direction: row
margin: 5px 0
.header
.preset
flex-grow: 0.5
.window,
.level
flex-grow: 1
.description
flex-grow: 2
@@ -1,91 +0,0 @@
const range = (start, end) => {
return new Array(end - start).fill().map((d, i) => i + start);
};
export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
export const DISALLOWED_COMBINATIONS = {
'': [],
alt: ['space'],
shift: [],
ctrl: [
'f4',
'f5',
'f11',
'w',
'r',
't',
'o',
'p',
'a',
'd',
'f',
'g',
'h',
'j',
'l',
'z',
'x',
'c',
'v',
'b',
'n',
'pagedown',
'pageup',
],
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
};
export const SPECIAL_KEYS = {
8: 'backspace',
9: 'tab',
13: 'return',
16: 'shift',
17: 'ctrl',
18: 'alt',
19: 'pause',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'insert',
46: 'del',
96: '0',
97: '1',
98: '2',
99: '3',
100: '4',
101: '5',
102: '6',
103: '7',
104: '8',
105: '9',
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
144: 'numlock',
145: 'scroll',
191: '/',
224: 'meta',
};
@@ -1,97 +0,0 @@
import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
let firstConflictingCommand = undefined;
const formatedPressedHotkeys = formatPressedKeys(pressedKeys);
for (const commandName in hotkeys) {
const toolHotkeys = hotkeys[commandName].keys;
const formatedToolHotkeys = formatPressedKeys(toolHotkeys);
if (
formatedPressedHotkeys === formatedToolHotkeys &&
commandName !== currentCommandName
) {
firstConflictingCommand = hotkeys[commandName];
break;
}
}
return firstConflictingCommand;
};
const ERROR_MESSAGES = {
MODIFIER:
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut",
EMPTY: "Field can't be empty.",
};
// VALIDATORS
const modifierValidator = ({ pressedKeys }) => {
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
// Check if it has a valid modifier
const isModifier = MODIFIER_KEYS.includes(lastPressedKey);
if (isModifier) {
return {
hasError: true,
errorMessage: ERROR_MESSAGES.MODIFIER,
};
}
};
const emptyValidator = ({ pressedKeys = [] }) => {
if (!pressedKeys.length) {
return {
hasError: true,
errorMessage: ERROR_MESSAGES.EMPTY,
};
}
};
const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
const conflictingCommand = findConflictingCommand(
hotkeys,
commandName,
pressedKeys
);
if (conflictingCommand) {
return {
hasError: true,
errorMessage: `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`,
};
}
};
const disallowedValidator = ({ pressedKeys = [] }) => {
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
const modifierCommand = formatPressedKeys(
pressedKeys.slice(0, pressedKeys.length - 1)
);
const disallowedCombination = DISALLOWED_COMBINATIONS[modifierCommand];
const hasDisallowedCombinations = disallowedCombination
? disallowedCombination.includes(lastPressedKey)
: false;
if (hasDisallowedCombinations) {
return {
hasError: true,
errorMessage: `"${formatPressedKeys(
pressedKeys
)}" shortcut combination is not allowed`,
};
}
};
const hotkeysValidators = [
emptyValidator,
modifierValidator,
conflictingValidator,
disallowedValidator,
];
export { hotkeysValidators };
@@ -1 +0,0 @@
export { UserPreferences } from './UserPreferences';
@@ -1,10 +0,0 @@
/**
*
*
* @export
* @param {*} props
* @returns
*/
export default function DefaultViewport(props) {
return <div>{JSON.stringify(props)}</div>;
}
@@ -1,7 +0,0 @@
.empty-viewport {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--text-secondary-color);
}
@@ -1,12 +0,0 @@
/**
*
*
* @returns
*/
function EmptyViewport() {
return (
<div className="empty-viewport">
<p>Please drag a stack here to view images.</p>
</div>
);
}
@@ -1,9 +0,0 @@
.viewport-container {
float: left;
position: relative;
border: var(--viewport-border-thickness) solid var(--ui-border-color);
}
.viewport-container.active {
border: var(--viewport-border-thickness) solid var(--active-color);
}
@@ -1,10 +0,0 @@
.viewport-drop-target {
opacity: 1;
position: relative; /* Locks in Scrollbar */
transition: 0.3s all ease;
}
.viewport-drop-target.hovered {
opacity: 0.5;
cursor: copy;
}
@@ -1,5 +0,0 @@
import ConnectedViewportGrid from './ConnectedViewportGrid.js';
import ViewportGrid from './ViewportGrid.js';
export default ViewportGrid;
export { ConnectedViewportGrid, ViewportGrid };
@@ -1,104 +0,0 @@
import { connect } from 'react-redux';
import { CineDialog } from '@ohif/ui';
import OHIF from '@ohif/core';
import csTools from 'cornerstone-tools';
import { commandsManager } from './../App.js';
// Our target output kills the `as` and "import" throws a keyword error
// import { import as toolImport, getToolState } from 'cornerstone-tools';
import cloneDeep from 'lodash.clonedeep';
const toolImport = csTools.import;
const scrollToIndex = toolImport('util/scrollToIndex');
const { setViewportSpecificData } = OHIF.redux.actions;
// Why do I need or care about any of this info?
// A dispatch action should be able to pull this at the time of an event?
// `isPlaying` and `cineFrameRate` might matter, but I think we can prop pass for those.
const mapStateToProps = state => {
// Get activeViewport's `cine` and `stack`
const { viewportSpecificData, activeViewportIndex } = state.viewports;
const { cine } = viewportSpecificData[activeViewportIndex] || {};
const dom = commandsManager.runCommand('getActiveViewportEnabledElement');
const cineData = cine || {
isPlaying: false,
cineFrameRate: 24,
};
// New props we're creating?
return {
activeEnabledElement: dom,
activeViewportCineData: cineData,
activeViewportIndex: state.viewports.activeViewportIndex,
};
};
const mapDispatchToProps = dispatch => {
return {
dispatchSetViewportSpecificData: (viewportIndex, data) => {
dispatch(setViewportSpecificData(viewportIndex, data));
},
};
};
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const {
activeEnabledElement,
activeViewportCineData,
activeViewportIndex,
} = propsFromState;
return {
cineFrameRate: activeViewportCineData.cineFrameRate,
isPlaying: activeViewportCineData.isPlaying,
onPlayPauseChanged: isPlaying => {
const cine = cloneDeep(activeViewportCineData);
cine.isPlaying = !cine.isPlaying;
propsFromDispatch.dispatchSetViewportSpecificData(activeViewportIndex, {
cine,
});
},
onFrameRateChanged: frameRate => {
const cine = cloneDeep(activeViewportCineData);
cine.cineFrameRate = frameRate;
propsFromDispatch.dispatchSetViewportSpecificData(activeViewportIndex, {
cine,
});
},
onClickNextButton: () => {
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
if (!stackData || !stackData.data || !stackData.data.length) return;
const { currentImageIdIndex, imageIds } = stackData.data[0];
if (currentImageIdIndex >= imageIds.length - 1) return;
scrollToIndex(activeEnabledElement, currentImageIdIndex + 1);
},
onClickBackButton: () => {
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
if (!stackData || !stackData.data || !stackData.data.length) return;
const { currentImageIdIndex } = stackData.data[0];
if (currentImageIdIndex === 0) return;
scrollToIndex(activeEnabledElement, currentImageIdIndex - 1);
},
onClickSkipToStart: () => {
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
if (!stackData || !stackData.data || !stackData.data.length) return;
scrollToIndex(activeEnabledElement, 0);
},
onClickSkipToEnd: () => {
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
if (!stackData || !stackData.data || !stackData.data.length) return;
const lastIndex = stackData.data[0].imageIds.length - 1;
scrollToIndex(activeEnabledElement, lastIndex);
},
};
};
const ConnectedCineDialog = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(CineDialog);
export default ConnectedCineDialog;
@@ -1,12 +0,0 @@
import Header from '../components/Header/Header.js';
import { connect } from 'react-redux';
const mapStateToProps = state => {
return {
user: state.oidc && state.oidc.user,
};
};
const ConnectedHeader = connect(mapStateToProps)(Header);
export default ConnectedHeader;
@@ -1,67 +0,0 @@
import { LayoutButton } from '@ohif/ui';
import OHIF from '@ohif/core';
import { connect } from 'react-redux';
const { setLayout, setViewportActive } = OHIF.redux.actions;
const mapStateToProps = state => {
return {
currentLayout: state.viewports.layout,
activeViewportIndex: state.viewports.activeViewportIndex,
};
};
const mapDispatchToProps = dispatch => {
return {
// TODO: Change if layout switched becomes more complex
onChange: (selectedCell, currentLayout, activeViewportIndex) => {
const viewports = [];
const numRows = selectedCell.row + 1;
const numColumns = selectedCell.col + 1;
const numViewports = numRows * numColumns;
for (let i = 0; i < numViewports; i++) {
// Hacky way to allow users to exit MPR "mode"
const viewport = currentLayout.viewports[i];
let plugin = viewport && viewport.plugin;
if (viewport && viewport.vtk) {
plugin = 'cornerstone';
}
viewports.push({
plugin,
});
}
const layout = {
numRows,
numColumns,
viewports,
};
const maxActiveIndex = numViewports - 1;
if (activeViewportIndex > maxActiveIndex) {
dispatch(setViewportActive(0));
}
dispatch(setLayout(layout));
},
};
};
const mergeProps = (propsFromState, propsFromDispatch) => {
const onChangeFromDispatch = propsFromDispatch.onChange;
const { currentLayout, activeViewportIndex } = propsFromState;
return {
onChange: selectedCell =>
onChangeFromDispatch(selectedCell, currentLayout, activeViewportIndex),
};
};
const ConnectedLayoutButton = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(LayoutButton);
export default ConnectedLayoutButton;
@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import StandaloneRouting from '../routes/StandaloneRouting';
const mapDispatchToProps = dispatch => {
return {
activateServer: server => {
const action = {
type: 'ACTIVATE_SERVER',
server,
};
dispatch(action);
},
};
};
const ConnectedStandaloneRouting = connect(
null,
mapDispatchToProps
)(StandaloneRouting);
export default ConnectedStandaloneRouting;
@@ -1,14 +0,0 @@
import React from 'react';
import { StudyList } from '@ohif/ui';
const ConnectedStudyList = () => {
return (
/** Temporary using StudyList -- We should mount it by using the created
* components such as NavBar, InputGroup, StudyListTable, etc...
* Check "Views > StudyList" in @ohif/ui for reference
*/
<StudyList />
);
};
export default ConnectedStudyList;
@@ -1,26 +0,0 @@
import { connect } from "react-redux";
import StudyLoadingMonitor from "../components/StudyLoadingMonitor.js";
import OHIF from "@ohif/core";
const {
setStudyLoadingProgress,
clearStudyLoadingProgress
} = OHIF.redux.actions;
const mapDispatchToProps = dispatch => {
return {
setStudyLoadingProgress: (progressId, progressData) => {
dispatch(setStudyLoadingProgress(progressId, progressData));
},
clearStudyLoadingProgress: progressId => {
dispatch(clearStudyLoadingProgress(progressId));
}
};
};
const ConnectedStudyLoadingMonitor = connect(
null,
mapDispatchToProps
)(StudyLoadingMonitor);
export default ConnectedStudyLoadingMonitor;
@@ -1,37 +0,0 @@
import { connect } from 'react-redux';
import Viewer from './Viewer.js';
import OHIF from '@ohif/core';
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
const getActiveServer = servers => {
const isActive = a => a.active === true;
return servers.servers.find(isActive);
};
const mapStateToProps = state => {
const { viewports, servers } = state;
return {
viewports: viewports.viewportSpecificData,
activeViewportIndex: viewports.activeViewportIndex,
activeServer: getActiveServer(servers),
};
};
const mapDispatchToProps = dispatch => {
return {
onTimepointsUpdated: timepoints => {
dispatch(setTimepoints(timepoints));
},
onMeasurementsUpdated: measurements => {
dispatch(setMeasurements(measurements));
},
};
};
const ConnectedViewer = connect(
mapStateToProps,
mapDispatchToProps
)(Viewer);
export default ConnectedViewer;
@@ -1,17 +0,0 @@
.ToolbarRow {
border-bottom: var(--ui-border-thickness) solid var(--ui-border-color);
flex: 0 0 auto;
height: var(--toolbar-height);
padding-left: 1rem;
padding-right: 1rem;
position: relative;
transition: height 300ms ease;
width: 100%;
display: flex;
align-items: center;
}
ToolbarRow.expanded {
height: calc(var(--toolbar-height) + var(--toolbar-drawer-height));
}
@@ -1,10 +0,0 @@
#viewer {
background-color: black;
width: 100%;
}
.loadingTextDiv {
color: var(--text-secondary-color);
font-size: 30px;
}
@@ -1,20 +0,0 @@
.drag-drop-instructions {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.drag-drop-instructions h3 {
color: var(--active-color);
}
.drag-drop-instructions h4 {
color: var(--text-secondary-color);
}
.link-dialog {
color: var(--active-color);
cursor: pointer;
}
@@ -1,21 +0,0 @@
#viewer {
height: calc(100% - var(--top-bar-height));
}
#viewer > .loadingTextDiv {
color: var(--text-secondary-color);
font-size: 30px;
height: 100%;
line-height: calc(100% - var(--top-bar-height));
}
.ViewerMain {
width: 100%;
height: 100%;
transition: all 0.3s ease;
}
.ViewerMain > div {
width: 100%;
height: 100%;
}
@@ -1,5 +0,0 @@
import React from 'react';
const UserManagerContext = React.createContext();
export default UserManagerContext;
@@ -1,9 +0,0 @@
import React, { useEffect, useRef } from 'react';
export default function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
@@ -1,135 +0,0 @@
import React, { useContext } from 'react';
import GoogleCloudApi from '../googleCloud/api/GoogleCloudApi';
import usePrevious from './usePrevious';
import * as GoogleCloudUtilServers from '../googleCloud/utils/getServers';
import { useSelector, useDispatch } from 'react-redux';
import isEqual from 'lodash.isequal';
// 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) {
GoogleCloudApi.urlBase = appConfig.healthcareApiEndpoint;
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);
if (!isValidServer(servers[0], appConfig)) {
return;
}
}
return servers;
};
const isValidServer = (server, appConfig) => {
if (appConfig.enableGoogleCloudAdapter) {
return GoogleCloudUtilServers.isValidServer(server);
}
return !!server;
};
const setServers = (dispatch, servers) => {
const action = {
type: 'SET_SERVERS',
servers,
};
dispatch(action);
};
const useServerFromUrl = (
servers = [],
previousServers,
activeServer,
urlBasedServers,
appConfig,
project,
location,
dataset,
dicomStore
) => {
// update state from url available only when gcloud on
if (!appConfig.enableGoogleCloudAdapter) {
return false;
}
const serverHasChanged = previousServers !== servers && previousServers;
// do not update from url. use state instead.
if (serverHasChanged) {
return false;
}
// if no valid urlbased servers
if (!urlBasedServers || !urlBasedServers.length) {
return false;
} else if (!servers.length || !activeServer) {
// no current valid server
return true;
}
const newServer = urlBasedServers[0];
let exists = servers.some(
GoogleCloudUtilServers.isEqualServer.bind(undefined, newServer)
);
return !exists;
};
export default function useServer({
project,
location,
dataset,
dicomStore,
} = {}) {
// Hooks
const servers = useSelector(state => state && state.servers);
const previousServers = usePrevious(servers);
const dispatch = useDispatch();
const { appConfig = {} } = useContext(AppContext);
const activeServer = getActiveServer(servers);
const urlBasedServers =
getServers(appConfig, project, location, dataset, dicomStore) || [];
const shouldUpdateServer = useServerFromUrl(
servers.servers,
previousServers,
activeServer,
urlBasedServers,
appConfig,
project,
location,
dataset,
dicomStore
);
if (shouldUpdateServer) {
setServers(dispatch, urlBasedServers);
} else if (isValidServer(activeServer, appConfig)) {
return activeServer;
}
}
@@ -1,26 +0,0 @@
import { connect } from 'react-redux';
import DicomFileUploaderModal from './DicomFileUploaderModal.js';
import OHIF from '@ohif/core';
const isActive = a => a.active === true;
const mapStateToProps = state => {
const activeServer = state.servers.servers.find(isActive);
// TODO: Not sure I like this approach since it means we are recreating
// this function every time redux changes
const retrieveAuthHeaderFunction = () =>
OHIF.DICOMWeb.getAuthorizationHeader(activeServer);
return {
retrieveAuthHeaderFunction,
url: activeServer && activeServer.qidoRoot,
};
};
const ConnectedDicomFileUploader = connect(
mapStateToProps,
null
)(DicomFileUploaderModal);
export default ConnectedDicomFileUploader;
@@ -1,32 +0,0 @@
import { connect } from 'react-redux';
import DicomStorePickerModal from './DicomStorePickerModal.js';
const isActive = a => a.active === true;
const mapStateToProps = state => {
const activeServer = state.servers.servers.find(isActive);
return {
user: state.oidc && state.oidc.user,
url: activeServer && activeServer.qidoRoot,
};
};
const mapDispatchToProps = dispatch => {
return {
setServers: servers => {
const action = {
type: 'SET_SERVERS',
servers,
};
dispatch(action);
},
};
};
const ConnectedDicomStorePicker = connect(
mapStateToProps,
mapDispatchToProps
)(DicomStorePickerModal);
export default ConnectedDicomStorePicker;
@@ -1,82 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './googleCloud.css';
import { withTranslation } from 'react-i18next';
import { Icon } from '@ohif/ui';
class DatasetsList extends Component {
state = {
search: '',
};
static propTypes = {
datasets: PropTypes.array,
loading: PropTypes.bool,
error: PropTypes.string,
onSelect: PropTypes.func,
};
static defaultProps = {
loading: true,
};
renderTableRow = dataset => {
return (
<tr
key={dataset.name}
className={
this.state.highlightedItem === dataset.name
? 'noselect active'
: 'noselect'
}
onMouseEnter={() => {
this.onHighlightItem(dataset.name);
}}
onClick={() => {
this.props.onSelect(dataset);
}}
>
<td>{dataset.name.split('/')[5]}</td>
</tr>
);
};
onHighlightItem(dataset) {
this.setState({ highlightedItem: dataset });
}
render() {
const { loading, datasets, filter, error } = this.props;
if (error) {
return <p>{error}</p>;
}
const loadingIcon = (
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
);
if (loading) {
return loadingIcon;
}
const body = (
<tbody id="DatasetList">
{datasets.filter(dataset => (dataset.name.split('/')[5].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
</tbody>
);
return (
<table id="tblDatasetList" className="gcp-table table noselect">
<thead>
<tr>
<th>{this.props.t('Dataset')}</th>
</tr>
</thead>
{datasets && body}
</table>
);
}
}
export default withTranslation('Common')(DatasetsList);
@@ -1,49 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-bootstrap-modal';
import DicomUploader from './DicomUploader';
import { withTranslation } from 'react-i18next';
class DicomFileUploaderModal extends Component {
static propTypes = {
url: PropTypes.string,
retrieveAuthHeaderFunction: PropTypes.func,
onClose: PropTypes.func,
};
state = {
uploaded: false,
};
render() {
if (!this.props.url) {
return null;
}
return (
<Modal
show={this.props.isOpen}
onHide={this.props.onClose}
aria-labelledby="ModalHeader"
className="modal fade themed in"
backdrop={false}
size={'md'}
keyboard={true}
>
<Modal.Header closeButton>
<Modal.Title>
{this.props.t('Upload DICOM Files')}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<DicomUploader
url={this.props.url}
retrieveAuthHeaderFunction={this.props.retrieveAuthHeaderFunction}
/>
</Modal.Body>
</Modal>
);
}
}
export default withTranslation('Common')(DicomFileUploaderModal);
@@ -1,82 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './googleCloud.css';
import { withTranslation } from 'react-i18next';
import { Icon } from '@ohif/ui';
class DicomStoreList extends Component {
state = {
search: '',
};
static propTypes = {
stores: PropTypes.array,
loading: PropTypes.bool.isRequired,
error: PropTypes.string,
onSelect: PropTypes.func,
};
static defaultProps = {
loading: true,
};
renderTableRow = store => {
return (
<tr
key={store.name}
className={
this.state.highlightedItem === store.name
? 'noselect active'
: 'noselect'
}
onMouseEnter={() => {
this.onHighlightItem(store.name);
}}
onClick={() => {
this.props.onSelect(store);
}}
>
<td className="project">{store.name.split('/')[7]}</td>
</tr>
);
};
onHighlightItem(store) {
this.setState({ highlightedItem: store });
}
render() {
const { loading, stores, filter, error } = this.props;
if (error) {
return <p>{error}</p>;
}
const loadingIcon = (
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
);
if (loading) {
return loadingIcon;
}
const body = (
<tbody id="StoreList">{
stores.filter(store => (store.name.split('/')[7].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
</tbody>
);
return (
<table id="tblStoreList" className="gcp-table table noselect">
<thead>
<tr>
<th>{this.props.t('DICOM Store')}</th>
</tr>
</thead>
{stores && body}
</table>
);
}
}
export default withTranslation('Common')(DicomStoreList);
@@ -1,20 +0,0 @@
.dicom-uploader .button {
float: left;
margin: 5px;
}
.invisible-input {
position: absolute;
display: none;
z-index: -1000;
max-width: 0 !important;
max-height: 0 !important;
}
.dicom-uploader .table-header {
color: #ffffff;
font-size: 16px;
font-weight: 28px;
text-align: left;
margin: 20px auto;
}
@@ -1,188 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { formatFileSize } from './utils/helpers';
import CancellationToken from './utils/CancellationToken';
import dicomUploader from './api/DicomUploadService';
import './DicomUploader.css';
export default class DicomUploader extends Component {
state = {
status: 'Upload',
isCancelled: false,
errorsCount: 0,
files: null,
uploadedVolume: null,
wholeVolumeStr: null,
isFilesListHidden: true,
timeLeft: null,
uploadedList: null,
totalCount: 0,
successfullyUploadedCount: 0,
lastFile: '',
uploadContext: null, // this is probably not needed, but we use this variable to distinguish between different downloads
};
static propTypes = {
id: PropTypes.string,
event: PropTypes.string,
url: PropTypes.string,
retrieveAuthHeaderFunction: PropTypes.func,
};
filesLeft() {
return (
this.state.uploadedList.length + ' of ' + this.state.totalCount + ' files'
);
}
volumeLeft() {
let left = formatFileSize(this.state.uploadedVolume);
return left + ' of ' + this.state.wholeVolumeStr;
}
percents() {
return parseInt(
(100 * this.state.uploadedList.length) /
Object.keys(this.state.files).length
);
}
isFinished() {
return (
this.state.isCancelled ||
Object.keys(this.state.files).length === this.state.uploadedList.length
);
}
errorsMessage() {
const errors = this.state.errorsCount === 1 ? ' error' : ' errors';
return (
this.state.errorsCount + errors + ' while uploading, click for more info'
);
}
uploadFiles = files => {
const filesArray = Array.from(files.target.files);
const filesDict = {};
filesArray.forEach((file, i) => {
const fileDesc = {
id: i,
name: file.name,
path: file.webkitRelativePath || file.name,
size: file.size,
error: null,
processed: false,
processedInUI: false,
};
filesDict[i] = fileDesc;
file.fileId = i;
});
const wholeVolume = filesArray.map(f => f.size).reduce((a, b) => a + b);
const uploadContext = Math.random();
this.setState({
status: 'Uploading...',
files: filesDict,
uploadedList: [],
uploadedVolume: 0,
lastFile: filesArray[0].name,
totalCount: filesArray.length,
wholeVolumeStr: formatFileSize(wholeVolume),
uploadContext: uploadContext,
cancellationToken: new CancellationToken(),
});
const cancellationToken = new CancellationToken();
const uploadCallback = (fileId, error) =>
uploadContext === this.state.uploadContext &&
this.uploadCallback.call(this, fileId, error);
dicomUploader.setRetrieveAuthHeaderFunction(this.props.retrieveAuthHeaderFunction);
dicomUploader.smartUpload(
files.target.files,
this.props.url,
uploadCallback,
cancellationToken
);
};
uploadCallback(fileId, error) {
const file = this.state.files[fileId];
file.processed = true;
if (!error) {
let uploadedVolume = this.state.uploadedVolume + file.size;
this.setState({ uploadedVolume });
} else {
file.error = error;
this.setState({ errorsCount: this.state.errorsCount + 1 });
}
this.setState({ lastFile: file.name });
let uploadedList = this.state.uploadedList;
uploadedList.push(file);
this.setState({ uploadedList });
}
renderTableRow = file => {
let error = null;
if (file.error !== null) {
error = <p style={{ color: 'red' }}>{file.error}</p>;
}
return (
<tr key={file.id}>
<td className="project">
{file.name} {error}
</td>
</tr>
);
};
render() {
if (this.state.files === null) {
return (
<div className="dicom-uploader">
<div className="button">
<label htmlFor="file">
<img src="./assets/Button_File.svg" alt="upload file"></img>
</label>
<input
className="invisible-input"
onChange={this.uploadFiles}
type="file"
id="file"
multiple
/>
</div>
<div className="button">
<label htmlFor="folder">
<img src="./assets/Button_Folder.svg" alt="upload folder"></img>
</label>
<input
className="invisible-input"
type="file"
onChange={this.uploadFiles}
id="folder"
webkitdirectory="true"
mozdirectory="true"
multiple
/>
</div>
</div>
);
}
return (
<table id="tblProjectList" className="table noselect">
<thead>
<tr>
<th className="table-header">
{this.percents()}% {this.filesLeft()}
</th>
</tr>
</thead>
<tbody id="ProjectList">
{this.state.uploadedList.map(this.renderTableRow)}
</tbody>
</table>
);
}
}
@@ -1,82 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './googleCloud.css';
import { withTranslation } from 'react-i18next';
import { Icon } from '@ohif/ui';
class LocationsList extends Component {
state = {
search: '',
};
static propTypes = {
locations: PropTypes.array,
loading: PropTypes.bool.isRequired,
error: PropTypes.string,
onSelect: PropTypes.func,
};
static defaultProps = {
loading: true,
};
renderTableRow = location => {
return (
<tr
key={location.locationId}
className={
this.state.highlightedItem === location.locationId
? 'noselect active'
: 'noselect'
}
onMouseEnter={() => {
this.onHighlightItem(location.locationId);
}}
onClick={() => {
this.props.onSelect(location);
}}
>
<td>{location.name.split('/')[3]}</td>
</tr>
);
};
onHighlightItem(locationId) {
this.setState({ highlightedItem: locationId });
}
render() {
const { loading, locations, filter, error } = this.props;
if (error) {
return <p>{error}</p>;
}
const loadingIcon = (
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
);
if (loading) {
return loadingIcon;
}
const body = (
<tbody id="LocationList">
{locations.filter(location => (location.name.split('/')[3].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
</tbody>
);
return (
<table id="tblLocationList" className="gcp-table table noselect">
<thead>
<tr>
<th>{this.props.t('Location')}</th>
</tr>
</thead>
{locations && body}
</table>
);
}
}
export default withTranslation('Common')(LocationsList);
@@ -1,95 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './googleCloud.css';
import { withTranslation } from 'react-i18next';
import { Icon } from '@ohif/ui';
class ProjectsList extends Component {
state = {
search: '',
highlightedItem: null,
};
static propTypes = {
projects: PropTypes.array,
loading: PropTypes.bool.isRequired,
error: PropTypes.string,
onSelect: PropTypes.func.isRequired,
t: PropTypes.func,
};
static defaultProps = {
loading: true,
};
renderTableRow = project => {
return (
<tr
key={project.projectId}
className={
this.state.highlightedItem === project.projectId
? 'noselect active'
: 'noselect'
}
onMouseEnter={() => {
this.onHighlightItem(project.projectId);
}}
onClick={() => {
this.onHighlightItem(project.projectId);
this.props.onSelect(project);
}}
>
<td>{project.name}</td>
<td>{project.projectId}</td>
</tr>
);
};
onHighlightItem(project) {
this.setState({ highlightedItem: project });
}
render() {
const { loading, projects, filter, error } = this.props;
if (error) {
return <p>{error}</p>;
}
const loadingIcon = (
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
);
if (loading) {
return loadingIcon;
}
const lowerCaseFilter = filter.toLowerCase();
const filteredProjects = projects.filter(project =>
typeof project.name === 'string' &&
(filter === "" || project.name.toLowerCase().includes(lowerCaseFilter))
);
const body = (
<tbody id="ProjectList">
{
filteredProjects.map(this.renderTableRow)
}
</tbody>
);
return (
<table id="tblProjectList" className="gcp-table table noselect">
<thead>
<tr>
<th>{this.props.t('Project')}</th>
<th>{this.props.t('ID')}</th>
</tr>
</thead>
{projects && body}
</table>
);
}
}
export default withTranslation('Common')(ProjectsList);
@@ -1,109 +0,0 @@
class GoogleCloudApi {
setAccessToken(accessToken) {
if (!accessToken) console.error('Access token is empty');
this.accessToken = accessToken;
}
get fetchConfig() {
if (!this.accessToken) throw new Error('OIDC access_token is not set');
return {
method: 'GET',
headers: {
Authorization: 'Bearer ' + this.accessToken,
},
};
}
get urlBase() {
return this.healthcareApiEndpoint || 'https://healthcare.googleapis.com/v1beta1';
}
set urlBase(url) {
this.healthcareApiEndpoint = url;
}
get urlBaseProject() {
return this.urlBase + `/projects`;
}
getUrlBaseDicomWeb(project, location, dataset, dicomStore) {
return (
this.urlBase +
`/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}/dicomWeb`
);
}
getUrlPath(project, location, dataset, dicomStore) {
`/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}`;
}
async doRequest(urlStr, config = {}, params = {}) {
const url = new URL(urlStr);
let data = null;
url.search = new URLSearchParams(params);
try {
const response = await fetch(url, { ...this.fetchConfig, config });
try {
data = await response.json();
} catch (err) {}
if (response.status >= 200 && response.status < 300 && data != null) {
if (data.nextPageToken != null) {
params.pageToken = data.nextPageToken;
let subPage = await this.doRequest(urlStr, config, params);
for (let key in data) {
if (data.hasOwnProperty(key)) {
data[key] = data[key].concat(subPage.data[key]);
}
}
}
return {
isError: false,
status: response.status,
data,
};
} else {
return {
isError: true,
status: response.status,
message:
(data && data.error && data.error.message) || 'Unknown error',
};
}
} catch (err) {
if (data && data.error) {
return {
isError: true,
status: err.status,
message: err.response.data.error.message || 'Unspecified error',
};
}
return {
isError: true,
message: (err && err.message) || 'Oops! Something went wrong',
};
}
}
async loadProjects() {
return this.doRequest(
'https://cloudresourcemanager.googleapis.com/v1/projects'
);
}
async loadLocations(projectId) {
return this.doRequest(`${this.urlBaseProject}/${projectId}/locations`);
}
async loadDatasets(projectId, locationId) {
return this.doRequest(
`${this.urlBaseProject}/${projectId}/locations/${locationId}/datasets`
);
}
async loadDicomStores(dataset) {
return this.doRequest(`${this.urlBase}/${dataset}/dicomStores`);
}
}
export default new GoogleCloudApi();
@@ -1,155 +0,0 @@
.loading-icon-spin {
animation: spin 2s linear infinite;
}
.loading-icon {
font-size: 30px;
margin: 20px 0;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
.gcp-table {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
border-spacing: 0;
border-collapse: collapse;
}
.gcp-table > tr {
height: 20px;
}
.gcp-table > thead {
white-space: nowrap;
}
.gcp-table > thead tr th {
padding: 0;
border-bottom: 1px solid var(--ui-border-color-active);
width: 100%;
text-align: left;
border-top: 0;
}
.gcp-table > tbody tr {
padding: 5px;
background-color: var(--ui-gray-darker);
}
.gcp-table > tbody tr:nth-child(even) {
background-color: var(--ui-gray-dark);
}
.gcp-table > tbody tr td {
padding: 8px;
height: $body-cell-height;
line-height: $body-cell-height;
color: var(--table-text-primary-color);
font-weight: 300;
border-top: 1px solid var(--ui-gray-lighter);
border-bottom: 1px solid var(--ui-gray-lighter);
white-space: nowrap;
}
.gcp-table > tbody tr td.emptyCell {
color: #516873;
}
.gcp-table > tbody tr td.emptyValue {
color: var(--ui-gray-light);
}
.gcp-table > tbody tr:hover,
.gcp-table > tbody tr:active,
.gcp-table > tbody tr.active {
background-color: var(--table-hover-color);
color: var(--text-primary-color);
}
.gcp-table > tbody tr:hover td,
.gcp-table > tbody tr:active td,
.gcp-table > tbody tr.active td {
border-top: 1px solid var(--ui-gray-lighter);
border-bottom: 1px solid var(--ui-gray-lighter);
background-color: var(--table-hover-color);
}
.gcp-files-selector {
display: flex;
justify-content: space-between;
}
.gcp-files-selector__btn-dir,
.gcp-files-selector__btn-file {
height: 80px !important;
width: 228px !important;
font-size: 14px;
border-radius: 8px;
margin: 0 !important;
}
.gcp-files-selector__btn-dir {
background-image: url('/assets/Button_Folder.svg');
}
.gcp-files-selector__btn-file {
background-image: url('/assets/Button_File.svg');
}
.gcp-window {
background-color: #000000;
}
.gcp-hidden {
display: none;
}
.gcp-picker {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
width: 650px;
height: 600px;
background-color: #161a1f;
padding: 20px 30px;
position: relative;
}
.gcp-dicom-picker__exit {
cursor: pointer;
position: absolute;
top: 30px;
right: 30px;
width: 14px !important;
height: 14px !important;
filter: opacity(0.5);
}
.gcp-dicom-picker__exit:hover {
filter: opacity(1);
}
.gcp-picker--btn {
margin: auto;
}
.gcp-picker--title {
color: #ffffff;
font-size: 24px;
text-align: center;
margin: 20px auto;
}
.gcp-picker--path {
color: #ffffff;
font-size: 16px;
font-weight: 28px;
text-align: left;
margin: 20px auto;
}
.gcp-input{
height: 40px;
margin: 0 5px 20px 5px;
padding: 0 20px;
cursor: pointer;
border: none;
background-color: var(--input-background-color);
color: var(--input-placeholder-color);
font-size: 10pt;
font-weight: normal;
border-radius: 4px;
}
@@ -1,13 +0,0 @@
export default class CancellationToken {
constructor() {
this.cancelled = false;
}
get() {
return this.cancelled;
}
set(value) {
this.cancelled = value;
}
}
@@ -1,59 +0,0 @@
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,
},
];
};
const isValidServer = server => {
return (
server &&
!!server.dataset &&
!!server.dicomStore &&
!!server.location &&
!!server.project
);
};
const isEqualServer = (server = {}, toCompare = {}) => {
const serverLength = Object.keys(server).length;
const toCompareLength = Object.keys(toCompare).length;
if (!serverLength || !toCompareLength) {
return false;
}
return (
server.dataset === toCompare.dataset &&
server.dataset === toCompare.dataset &&
server.dicomStore === toCompare.dicomStore &&
server.location === toCompare.location &&
server.project === toCompare.project
);
};
export { getServers, isValidServer, isEqualServer };
@@ -1,35 +0,0 @@
export function formatFileSize(size) {
if (size === 0) return '0 B';
const n = Math.floor(Math.log(size) / Math.log(1024));
return (
(size / Math.pow(1024, n)).toFixed(2) * 1 +
' ' +
['B', 'kB', 'MB', 'GB', 'TB'][n]
);
}
export function httpErrorToStr(error) {
if (!error) return 'Unknown error';
if (error.response) {
try {
const jsonResponse = JSON.parse(error.response);
if (
jsonResponse.error &&
jsonResponse.error.code &&
jsonResponse.error.message
)
return jsonResponse.error.code + ' - ' + jsonResponse.error.message;
} catch (err) {
return error.response;
}
}
return error.message || 'Unknown error.';
}
/* eslint-disable */
export function checkDicomFile(arrayBuffer) {
if (arrayBuffer.length <= 132) return false;
const arr = new Uint8Array(arrayBuffer.slice(128, 132));
// bytes from 128 to 132 must be "DICM"
return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]);
}
-15
View File
@@ -1,15 +0,0 @@
<head>
<meta charset="utf-8">
<title>OHIF Viewer</title>
<meta name="description" content="Open Health Imaging Foundation DICOM Viewer">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
<link rel="shortcut icon" type="image/png" href="/images/logo.png" sizes="16x16 32x32 64x64">
<!--Best display for mobile devices-->
<meta http-equiv="cleartype" content="on">
<meta name="MobileOptimized" content="320">
<meta name="HandheldFriendly" content="True">
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
<body>
<div id="root"></div>
</body>
+3 -36
View File
@@ -2,49 +2,16 @@
* Entry point for development and production PWA builds.
* Packaged (NPM) builds go through `index-umd.js`
*/
import 'regenerator-runtime/runtime';
import App from './App.js';
import React from 'react';
import ReactDOM from 'react-dom';
// test
/**
* EXTENSIONS
* =================
*
* Importing and modifying the extensions our app uses HERE allows us to leverage
* tree shaking and a few other niceties. However, by including them here they become
* "baked in" to the published application.
*
* Depending on your use case/needs, you may want to consider not adding any extensions
* by default HERE, and instead provide them via the extensions configuration key or
* by using the exported `App` component, and passing in your extensions as props using
* the defaultExtensions property.
*/
// import OHIFVTKExtension from '@ohif/extension-vtk';
// import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
// import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
// import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
/*
* Default Settings
*/
let config = {};
if (window) {
config = window.config || {};
}
/** Combine our appConfiguration and "baked-in" extensions */
const appProps = {
config,
// defaultExtensions: [
// OHIFVTKExtension,
// OHIFDicomHtmlExtension,
// OHIFDicomMicroscopyExtension,
// OHIFDicomPDFExtension,
// ],
config: window ? window.config : {},
defaultExtensions: [],
};
/** Create App */
-26
View File
@@ -1,26 +0,0 @@
import FileLoaderService from './localFileLoaders/fileLoaderService';
const processFile = async file => {
try {
const fileLoaderService = new FileLoaderService(file);
const imageId = fileLoaderService.addFile(file);
const image = await fileLoaderService.loadFile(file, imageId);
const dataset = await fileLoaderService.getDataset(image, imageId);
const studies = await fileLoaderService.getStudies(dataset, imageId);
return studies;
} catch (error) {
console.log(
error.name,
':Error when trying to load and process local files:',
error.message
);
}
};
export default async function filesToStudies(files) {
const processFilesPromises = files.map(processFile);
const studies = await Promise.all(processFilesPromises);
return FileLoaderService.groupSeries(studies.flat());
}
@@ -1,6 +0,0 @@
export default class FileLoader {
fileType;
loadFile(file, imageId) { }
getDataset(image, imageId) { }
getStudies(dataset, imageId) { }
}
-46
View File
@@ -1,46 +0,0 @@
export default function bounding(elementRef, currentPosition = {}) {
if (!elementRef.current) {
return;
}
const currentElement = elementRef.current;
const {
offsetParent,
offsetTop,
offsetHeight,
offsetLeft,
offsetWidth,
} = currentElement;
let top = currentPosition.top || offsetTop;
let left = currentPosition.left || offsetLeft;
if (!offsetParent) {
return;
}
let maxHeight = `${offsetParent.offsetHeight}px`;
if (offsetHeight + top > offsetParent.offsetHeight) {
top = top - (offsetHeight + top - offsetParent.offsetHeight);
if (top < 0) {
top = 0;
}
}
if (left + offsetWidth > offsetParent.offsetWidth) {
left = offsetParent.offsetWidth - offsetWidth;
if (left < 0) {
left = 0;
}
}
if (maxHeight && currentElement.style.maxHeight !== maxHeight) {
currentElement.style.maxHeight = maxHeight;
}
if (currentElement.style.top !== `${top}px`) {
currentElement.style.top = `${top}px`;
}
if (currentElement.style.left !== `${left}px`) {
currentElement.style.left = `${left}px`;
}
}
@@ -1,32 +0,0 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { CallbackComponent } from 'redux-oidc';
class CallbackPage extends Component {
static propTypes = {
userManager: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
render() {
return (
<CallbackComponent
userManager={this.props.userManager}
successCallback={() => {
const pathname = sessionStorage.getItem('ohif-redirect-to');
this.props.history.push(pathname);
}}
errorCallback={error => {
//this.props.history.push("/");
throw new Error(error);
}}
>
<div>Redirecting...</div>
</CallbackComponent>
);
}
}
export default withRouter(CallbackPage);
-9
View File
@@ -1,9 +0,0 @@
.not-found {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: white;
}
-18
View File
@@ -1,18 +0,0 @@
import React from 'react';
import './NotFound.css';
import { Link } from 'react-router-dom';
export default function NotFound({ message = 'Sorry, this page does not exist.', showGoBackButton = true }) {
return (
<div className={'not-found'}>
<div>
<h4>{message}</h4>
{showGoBackButton && (
<h5>
<Link to={'/'}>Go back to the Study List</Link>
</h5>
)}
</div>
</div>
);
}
@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
const NotFound = ({
message = 'Sorry, this page does not exist.',
showGoBackButton = true,
}) => {
return (
<div className="w-full h-full flex justify-center items-center text-white">
<div>
<h4>{message}</h4>
{showGoBackButton && (
<h5>
<Link to={'/'}>Go back to the Study List</Link>
</h5>
)}
</div>
</div>
);
};
NotFound.propTypes = {
message: PropTypes.string,
showGoBackButton: PropTypes.bool,
};
export default NotFound;
@@ -0,0 +1 @@
export { default } from './NotFound';
@@ -0,0 +1,13 @@
import React from 'react';
import { Link } from 'react-router-dom';
const StudyListContainer = () => {
return (
<div>
<div>StudyListContainer</div>
<Link to="/viewer/123">Go to Viewer Route</Link>
</div>
);
};
export default StudyListContainer;
@@ -0,0 +1 @@
export { default } from './StudyListContainer';
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import StudyListContainer from './StudyListContainer';
import NotFound from './NotFound';
const appRoutes = [
{ path: '/', exact: true, component: StudyListContainer },
{ path: '/viewer/:studyInstanceUids', component: NotFound },
{ component: NotFound },
];
const routes = () => {
return (
<Switch>
{appRoutes.map((route, i) => {
return (
<Route
key={i}
path={route.path}
exact={route.exact}
strict={route.strict}
// eslint-disable-next-line react/jsx-props-no-spreading
render={props => <route.component {...props} route={route} />}
/>
);
})}
</Switch>
);
};
export default routes;
-1
View File
@@ -1 +0,0 @@
// "export default '"$(git rev-parse HEAD)"';"
-44
View File
@@ -1,44 +0,0 @@
import {
applyMiddleware,
combineReducers,
createStore,
compose,
} from 'redux/es/redux.js';
// import { createLogger } from 'redux-logger';
import { reducer as oidcReducer } from 'redux-oidc';
import { redux } from '@ohif/core';
import thunkMiddleware from 'redux-thunk';
// Combine our @ohif/core and oidc reducers
// Set init data, using values found in localStorage
const { reducers, localStorage, sessionStorage } = redux;
const middleware = [thunkMiddleware];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
reducers.oidc = oidcReducer;
const rootReducer = combineReducers(reducers);
const preloadedState = {
...localStorage.loadState(),
...sessionStorage.loadState(),
};
const store = createStore(
rootReducer,
preloadedState,
composeEnhancers(applyMiddleware(...middleware))
);
// When the store's preferences change,
// Update our cached preferences in localStorage
store.subscribe(() => {
localStorage.saveState({
preferences: store.getState().preferences,
});
sessionStorage.saveState({
servers: store.getState().servers,
});
});
export default store;
@@ -1,28 +0,0 @@
import { createSelector } from 'reselect';
const getActiveViewportIndex = state => state.viewports.activeViewportIndex;
const getLayoutViewports = state => state.viewports.layout.viewports;
const getViewportSpecificData = state => state.viewports.viewportSpecificData;
/**
* Think of this as a computed getter for our store. It lets us watch parts of
* our redux state, and only update/recalculate when those values change.
*/
export const getActiveContexts = createSelector(
[getActiveViewportIndex, getLayoutViewports, getViewportSpecificData],
(activeViewportIndex, layoutViewports, viewportSpecificData) => {
const activeContexts = ['VIEWER'];
const activeLayoutViewport = layoutViewports[activeViewportIndex] || {};
const activeViewportSpecificData =
viewportSpecificData[activeViewportIndex] || {};
const activeViewportPluginName =
activeLayoutViewport.plugin || activeViewportSpecificData.plugin;
if (activeViewportPluginName) {
const activeViewportExtension = `ACTIVE_VIEWPORT::${activeViewportPluginName.toUpperCase()}`;
activeContexts.push(activeViewportExtension);
}
return activeContexts;
}
);
@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import StudyListRoute from './StudyListRoute.js';
const isActive = a => a.active === true;
const mapStateToProps = state => {
const activeServer = state.servers.servers.find(isActive);
return {
server: activeServer,
user: state.oidc.user,
};
};
const ConnectedStudyList = connect(
mapStateToProps,
null
)(StudyListRoute);
export default ConnectedStudyList;
-54
View File
@@ -1,54 +0,0 @@
/* "Tide" theme */
/* TODO:
- Convert the other themes to CSS Variables
- Allow user to set default CSS theme in JS config
- Export the themes as identical JS objects
- For theme switcher, update root-level CSS properties via JS (https://medium.com/@_bengarrison/accessing-and-modifying-css-variables-with-javascript-2ccb735bbff0)
*/
:root {
/* Interface UI Colors */
--default-color: #9CCEF9;
--hover-color: #ffffff;
--active-color: #20A5D6;
--ui-border-color: #44626F;
--ui-border-color-dark: #3C5D80;
--ui-border-color-active: #00a4d9;
--primary-background-color: #000000;
--box-background-color: #3E5975;
/* Common palette */
--ui-yellow: #E29E4A;
--ui-sky-blue: #6FBDE2;
/* State palette */
--ui-state-error: #FFCCCC;
--ui-state-error-border: #993333;
--ui-state-error-text: #661111;
--ui-gray-light: #516873;
--ui-gray: #263340;
--ui-gray-dark: #16202B;
--ui-gray-darker: #151A1F;
--ui-gray-darkest: #14202A;
/* Text Colors */
--text-color-active: #000000;
--text-primary-color: #ffffff;
--text-secondary-color: #91b9cd;
--large-numbers-color: #6fbde2;
--text-disabled-color: #878787;
--input-background-color: #2c363f;
--input-placeholder-color--hover: #4d5a63;
--input-placeholder-color: #d3d3d3;
--table-hover-color: #2c363f;
--table-text-primary-color: #ffffff;
--table-text-secondary-color: #91b9cd;
--large-numbers-color:#6FBDE2;
--state-error: #FFCCCC;
--state-error-border: #FFCCCC;
--state-error-text: #FFCCCC;
}
@@ -1,36 +0,0 @@
// https://github.com/maxmantz/redux-oidc/blob/master/docs/API.md
import { loadUser, createUserManager } from 'redux-oidc';
/**
* Creates a userManager from oidcSettings;
* loads the user into the provided redux store
* LINK: https://github.com/IdentityModel/oidc-client-js/wiki#configuration
*
* @param {*} store
* @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(store, oidcSettings) {
if (!store || !oidcSettings) {
return;
}
const settings = {
...oidcSettings,
automaticSilentRenew: true,
revokeAccessTokenOnSignout: true,
filterProtocolClaims: true,
loadUserInfo: true,
};
const userManager = createUserManager(settings);
loadUser(store, userManager);
return userManager;
}
@@ -1,23 +0,0 @@
import getUserManagerForOpenIdConnectClient from './getUserManagerForOpenIdConnectClient.js';
describe('getUserManagerForOpenIdConnectClient', () => {
it('returns undefined if store is not provided', () => {
const expectedReturnVal = undefined;
const returnVal = getUserManagerForOpenIdConnectClient(undefined, {});
expect(returnVal).toEqual(expectedReturnVal);
});
it('returns undefined if oidcSettings are not provided', () => {
const expectedReturnVal = undefined;
const returnVal = getUserManagerForOpenIdConnectClient({}, undefined);
expect(returnVal).toEqual(expectedReturnVal);
});
it('does not return undefined when store and oidcSettings are defined', () => {
const returnVal = getUserManagerForOpenIdConnectClient({}, {});
expect(returnVal).not.toBe(undefined);
});
});
-4
View File
@@ -1,4 +0,0 @@
import getUserManagerForOpenIdConnectClient from './getUserManagerForOpenIdConnectClient.js';
import initWebWorkers from './initWebWorkers.js';
export { getUserManagerForOpenIdConnectClient, initWebWorkers };
-11
View File
@@ -1,11 +0,0 @@
import * as utils from './index.js';
describe('utils', () => {
it('has the expected exports', () => {
const utilExports = Object.keys(utils).sort();
expect(utilExports).toEqual(
['getUserManagerForOpenIdConnectClient', 'initWebWorkers'].sort()
);
});
});
-42
View File
@@ -1,42 +0,0 @@
:root {
/* Sizes */
--top-bar-height: 40px;
--top-bar-expanded-height: 160px;
--toolbar-height: 78px;
--toolbar-drawer-height: 62px;
--left-sidepanel-menu-width: 307px;
--right-sidepanel-menu-width: 323px;
--study-list-padding: 8%;
--study-list-padding-medium-screen: 10px;
--snackbar-size: 344px;
/* Transitions */
--transition-duration: 0.3s;
--transition-effect: ease;
--sidepanel-transition: all 0.3s ease;
/* Thicknesses */
--viewport-border-thickness: 1px;
--ui-border-thickness: 1px;
/** Colors **/
--snackbar-success: rgba(94, 164, 0, 0.9);
--snackbar-error: rgba(236, 61, 61, 0.9);
--snackbar-warning: rgba(235, 173, 23, 0.9);
--snackbar-info: rgba(54, 156, 199, 0.9);
/** z-Index **/
--snackbar-zIndex: 1075;
/** DatePicker Colors **/
--datePicker-input-background: #2c363f;
--datePicker-picker-background: #9ccef9;
--datePicker-picker-disabled: #d3d3d3;
--datePicker-picker-text: #484848;
--datePicker-picker-text-disabled: #aaa;
--datePicker-picker-text-input: #fff;
}
-1
View File
@@ -1 +0,0 @@
export default '0.0.50';