Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4ee9e346 | ||
|
|
1a9cfea365 | ||
|
|
9055f19fb5 | ||
|
|
bcbc5e4bbf | ||
|
|
408ba36f4f | ||
|
|
091cab49e0 | ||
|
|
4a09f33446 | ||
|
|
4095629310 | ||
|
|
91d2d31846 | ||
|
|
ff2c1cb2f1 | ||
|
|
727c0d8ba6 | ||
|
|
4b3994e788 | ||
|
|
571dd5848f | ||
|
|
2d48d0aa37 | ||
|
|
562d63fa74 | ||
|
|
1a6246f99b | ||
|
|
ce1a62a961 | ||
|
|
dd6acb2d03 |
No files matched your search
@@ -75,6 +75,7 @@ jobs:
|
||||
# Could do a check here, if it doesn't exist, install cypress?
|
||||
- run: yarn install
|
||||
- run: yarn run test:e2e:ci
|
||||
- run: yarn run test:e2e:scriptTag
|
||||
|
||||
npm_publish:
|
||||
<<: *defaults
|
||||
|
||||
+2
-1
@@ -36,4 +36,5 @@
|
||||
*.pdf binary
|
||||
*.ez binary
|
||||
*.bz2 binary
|
||||
*.swp binary
|
||||
*.swp binary
|
||||
*.fig binary
|
||||
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<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" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<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" />
|
||||
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous" />
|
||||
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css"
|
||||
integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous" />
|
||||
|
||||
<title>OHIF Standalone Viewer</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- Load React. -->
|
||||
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
|
||||
<script>
|
||||
// Workaround to deal with react-router-dom complaining
|
||||
// if process.env is not defined. Must be run before
|
||||
// ohif-viewer package is included.
|
||||
"use strict";
|
||||
|
||||
var process = {
|
||||
env: {
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
};
|
||||
|
||||
window.process = process;
|
||||
window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
// default: ''
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="/dist/index.umd.js" crossorigin></script>
|
||||
<script>
|
||||
var Viewer = window.OHIFStandaloneViewer.App;
|
||||
var app = React.createElement(Viewer, window.config, null);
|
||||
|
||||
ReactDOM.render(app, document.getElementById("root"));
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1074,10 +1074,10 @@ gitbook-plugin-sitemap@^1.2.0:
|
||||
dependencies:
|
||||
sitemap "1.5.0"
|
||||
|
||||
gitbook-plugin-theme-cornerstone@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/gitbook-plugin-theme-cornerstone/-/gitbook-plugin-theme-cornerstone-1.1.3.tgz#320e1d688eaf27dbf8d580f285509b55984437fd"
|
||||
integrity sha512-fURj67XGQ5rZPj8LXbwEviMxLfWgN/s08t9Wnp1dQqffBUJwsLe00R/IeecOjsQHJ3Gvb8GjbXJ6qzIDGu5hPA==
|
||||
gitbook-plugin-theme-cornerstone@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/gitbook-plugin-theme-cornerstone/-/gitbook-plugin-theme-cornerstone-1.1.4.tgz#bc21b65acee2ac67e2ddb0986442ec448fe0c7fb"
|
||||
integrity sha512-BADgFyEKhkc0pPSBRwXa5VU2UoYQkd2cRwjrtjHyV6w4h/M7uyeWYFOcsgi2ju3rpHRCwF8lMg/TtDdAV8V1SQ==
|
||||
|
||||
gitbook-plugin-versions@^2.1.4:
|
||||
version "2.1.4"
|
||||
@@ -1608,9 +1608,9 @@ lodash.map@^4.4.0:
|
||||
integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=
|
||||
|
||||
lodash.merge@^4.4.0:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
|
||||
integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash.pad@^4.1.0:
|
||||
version "4.5.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "0.0.38",
|
||||
"version": "0.0.40",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -202,6 +202,10 @@ class OHIFCornerstoneViewport extends Component {
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (!studyInstanceUid || !displaySetInstanceUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
@@ -89,15 +90,60 @@ const actions = {
|
||||
// TODO
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: () => {
|
||||
console.warn('clearAnnotations: not yet implemented');
|
||||
// const toolState =
|
||||
// cornerstoneTools.globalImageIdSpecificToolStateManager.toolState;
|
||||
// if (!toolState) return;
|
||||
// Object.keys(toolState).forEach(imageId => {
|
||||
// if (!cornerstoneImageId || cornerstoneImageId === imageId)
|
||||
// delete toolState[imageId];
|
||||
// });
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
toolState,
|
||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
if (
|
||||
!toolState ||
|
||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
||||
|
||||
const measurementsToRemove = [];
|
||||
|
||||
Object.keys(imageIdToolState).forEach(toolType => {
|
||||
const { data } = imageIdToolState[toolType];
|
||||
|
||||
data.forEach(measurementData => {
|
||||
const { _id, lesionNamingNumber, measurementNumber } = measurementData;
|
||||
if (!_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
measurementsToRemove.push({
|
||||
toolType,
|
||||
_id,
|
||||
lesionNamingNumber,
|
||||
measurementNumber,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
measurementsToRemove.forEach(measurementData => {
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved({
|
||||
detail: {
|
||||
toolType: measurementData.toolType,
|
||||
measurementData,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -147,7 +193,11 @@ const definitions = {
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: Clear Annotations
|
||||
clearAnnotations: {
|
||||
commandFn: actions.clearAnnotations,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: Next/Previous image
|
||||
// TODO: First/Last image
|
||||
// Next/Previous series/DisplaySet
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// -- ACTIVE_ROUTE::VIEWER
|
||||
// -- ACTIVE_VIEWPORT::CORNERSTONE
|
||||
// setToolActive commands should receive the button event that triggered
|
||||
// so we can do the "bind to this butyon" magic
|
||||
// so we can do the "bind to this button" magic
|
||||
|
||||
const TOOLBAR_BUTTON_TYPES = {
|
||||
COMMAND: 'command',
|
||||
@@ -98,6 +98,98 @@ const definitions = [
|
||||
behavior: 'CINE',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'More',
|
||||
label: 'More',
|
||||
icon: 'ellipse-circle',
|
||||
buttons: [
|
||||
{
|
||||
id: 'Magnify',
|
||||
label: 'Magnify',
|
||||
icon: 'circle',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Magnify' },
|
||||
},
|
||||
{
|
||||
id: 'WwwcRegion',
|
||||
label: 'ROI Window',
|
||||
icon: 'stop',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'WwwcRegion' },
|
||||
},
|
||||
{
|
||||
id: 'DragProbe',
|
||||
label: 'Probe',
|
||||
icon: 'dot-circle',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'DragProbe' },
|
||||
},
|
||||
{
|
||||
id: 'EllipticalRoi',
|
||||
label: 'Ellipse',
|
||||
icon: 'circle-o',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'EllipticalRoi' },
|
||||
},
|
||||
{
|
||||
id: 'RectangleRoi',
|
||||
label: 'Rectangle',
|
||||
icon: 'square-o',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'RectangleRoi' },
|
||||
},
|
||||
{
|
||||
id: 'Invert',
|
||||
label: 'Invert',
|
||||
icon: 'adjust',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'invertViewport',
|
||||
},
|
||||
{
|
||||
id: 'RotateRight',
|
||||
label: 'Rotate Right',
|
||||
icon: 'rotate-right',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'rotateViewportCW',
|
||||
},
|
||||
{
|
||||
id: 'FlipH',
|
||||
label: 'Flip H',
|
||||
icon: 'ellipse-h',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'flipViewportHorizontal',
|
||||
},
|
||||
{
|
||||
id: 'FlipV',
|
||||
label: 'Flip V',
|
||||
icon: 'ellipse-v',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'flipViewportVertical',
|
||||
},
|
||||
{
|
||||
id: 'Clear',
|
||||
label: 'Clear',
|
||||
icon: 'trash',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'clearAnnotations',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.9",
|
||||
"description": "OHIF extension for Dicom Microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -19,9 +19,10 @@
|
||||
"lint": "eslint -c .eslintrc --fix src && prettier --single-quote --write src/**/*.{js,jsx,json,css}"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ohif-core": "^0.6.0",
|
||||
"react": "^15.0.0 || ^16.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0",
|
||||
"ohif-core": "^0.6.0"
|
||||
"react-resize-detector": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
@@ -81,6 +82,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"classnames": "^2.2.6",
|
||||
"dicom-microscopy-viewer": "^0.4.3"
|
||||
"dicom-microscopy-viewer": "0.5.0",
|
||||
"lodash.debounce": "^4.0.8"
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import builtins from 'rollup-plugin-node-builtins';
|
||||
|
||||
const globals = {
|
||||
react: 'React',
|
||||
'react-resize-detector': 'ReactResizeDetector',
|
||||
'react-dom': 'ReactDOM',
|
||||
'ohif-core': 'OHIF'
|
||||
};
|
||||
@@ -52,6 +53,15 @@ export default {
|
||||
],
|
||||
'.yalc/dicom-microscopy-viewer/build/dicom-microscopy-viewer.js': [
|
||||
'api'
|
||||
],
|
||||
'node_modules/react-resize-detector/node_modules/prop-types/index.js': [
|
||||
'bool',
|
||||
'number',
|
||||
'string',
|
||||
'shape',
|
||||
'func',
|
||||
'any',
|
||||
'node'
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import { api } from 'dicom-microscopy-viewer';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
|
||||
|
||||
@@ -9,10 +10,16 @@ class DicomMicroscopyViewport extends Component {
|
||||
error: null,
|
||||
};
|
||||
|
||||
viewer = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.container = React.createRef();
|
||||
|
||||
this.debouncedResize = debounce(() => {
|
||||
if (this.viewer) this.viewer.resize();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// install the microscopy renderer into the web page.
|
||||
@@ -52,17 +59,18 @@ class DicomMicroscopyViewport extends Component {
|
||||
.then(metadata => {
|
||||
metadata = metadata.filter(m => m);
|
||||
|
||||
const viewer = new microscopyViewer({
|
||||
this.viewer = new microscopyViewer({
|
||||
client: dicomWebClient,
|
||||
metadata,
|
||||
retrieveRendered: false
|
||||
});
|
||||
|
||||
viewer.render({ container, retrieveRendered: true });
|
||||
this.viewer.render({ container });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const { displaySet } = this.props.viewportData;
|
||||
|
||||
this.installOpenLayersRenderer(this.container.current, displaySet);
|
||||
}
|
||||
@@ -71,6 +79,13 @@ class DicomMicroscopyViewport extends Component {
|
||||
const style = { width: '100%', height: '100%' };
|
||||
return (
|
||||
<div className={'DicomMicroscopyViewer'} style={style}>
|
||||
{ReactResizeDetector && (
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
onResize={this.onWindowResize}
|
||||
/>
|
||||
)}
|
||||
{this.state.error ? (
|
||||
<h2>{JSON.stringify(this.state.error)}</h2>
|
||||
) : (
|
||||
@@ -79,6 +94,10 @@ class DicomMicroscopyViewport extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
this.debouncedResize();
|
||||
};
|
||||
}
|
||||
|
||||
export default DicomMicroscopyViewport;
|
||||
@@ -1617,10 +1617,10 @@ des.js@^1.0.0:
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
dicom-microscopy-viewer@^0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.4.3.tgz#defaa821026765bcd109b97258742df72a9b92bb"
|
||||
integrity sha512-PjT3qH0hv6dKOQzXFIHw4w/d8QCbiUOJOGZHvAkkRvnjeNRP7/ri8vbFvcSev+nYxKskNb1Tn0xd1+dsDpPGNw==
|
||||
dicom-microscopy-viewer@0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.5.0.tgz#e4f74261337d8a78d51490af0d17edc63997e619"
|
||||
integrity sha512-r3BPLBvexv9v/Rr/FBTgXgp7orIFKLWW5uYM6IsYpz36ttU84Edk7Z2GAyk6887QNsfEMLzYV+5ik6HRrMg0cA==
|
||||
dependencies:
|
||||
dicomweb-client "^0.4.2"
|
||||
ol "^5.3.0"
|
||||
@@ -3199,6 +3199,11 @@ lodash.camelcase@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
@@ -3238,6 +3243,7 @@ longest-streak@^2.0.1:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
|
||||
+5
-3
@@ -28,6 +28,7 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --reporters=default --reporters=jest-junit && codecov",
|
||||
"test:e2e": "",
|
||||
"test:e2e:ci": "start-server-and-test start http://localhost:5000 cy:run:ci",
|
||||
"test:e2e:scriptTag": "cross-env START_SERVER=true start-server-and-test build:package http-get://localhost:5000/index.umd.js cy:run",
|
||||
"staticDeploy": "./generateStaticSite.sh",
|
||||
"docs:publish": "./publishStaticSite.sh",
|
||||
"start": "yarn run dev",
|
||||
@@ -76,9 +77,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.4.5",
|
||||
"@ohif/extension-cornerstone": "0.0.38",
|
||||
"@ohif/extension-cornerstone": "0.0.40",
|
||||
"@ohif/extension-dicom-html": "0.0.3",
|
||||
"@ohif/extension-dicom-microscopy": "0.0.8",
|
||||
"@ohif/extension-dicom-microscopy": "0.0.9",
|
||||
"@ohif/extension-dicom-pdf": "0.0.7",
|
||||
"@ohif/extension-vtk": "0.1.3",
|
||||
"@ohif/i18n": "0.2.2",
|
||||
@@ -105,7 +106,7 @@
|
||||
"react-router": "^5.0.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-transition-group": "^4.1.1",
|
||||
"react-viewerbase": "0.15.3",
|
||||
"react-viewerbase": "0.17.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-oidc": "3.1.x",
|
||||
@@ -154,6 +155,7 @@
|
||||
"rollup-plugin-node-resolve": "^5.0.2",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.0",
|
||||
"rollup-plugin-postcss": "^2.0.3",
|
||||
"rollup-plugin-serve": "^1.0.1",
|
||||
"rollup-plugin-url": "^2.2.2",
|
||||
"semantic-release": "15.13.x",
|
||||
"start-server-and-test": "^1.9.1",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="228" height="80"><defs><style>@font-face{font-family:"Roboto-Bold";src:url(data:application/font-woff;charset=utf-8;base64,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) format("woff"); font-weight:bold;font-style:normal;}</style></defs><path d="M6 0h216a6 6 0 0 1 6 6v68a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z" fill="#52ABD3"/><text font-family="Roboto-Bold, Roboto" font-size="14" font-weight="bold" fill="#fff"><tspan x="72.148" y="45">FILE UPLOAD</tspan></text><path d="M58.285 79.648H40.642L36.616 58.63l-24.92 10.66 11.258 5.986-2.325 4.373H6a6 6 0 0 1-6-6V55.073L22.088 13.53a1 1 0 0 1 1.352-.413l36.2 19.248L68.3 60.813 58.285 79.648zm-27.108 0h-.923l.203-.382.72.382zM54.232 34.02l-9.86 18.542 18.542 9.86-8.683-28.4z" fill="#4591B3"/></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="228" height="80"><defs><style>@font-face{font-family:"Roboto-Bold";src:url(data:application/font-woff;charset=utf-8;base64,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) format("woff"); font-weight:bold;font-style:normal;}</style></defs><path d="M6 0h216a6 6 0 0 1 6 6v68a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z" fill="#52ABD3"/><text font-family="Roboto-Bold, Roboto" font-size="14" font-weight="bold" fill="#fff"><tspan x="60" y="45">FOLDER UPLOAD</tspan></text><path d="M51.706 79.74H39.752L38.32 56.483l-25.104 6.482 10.012 7.274-6.904 9.502H6a6 6 0 0 1-6-6V13.585c2.513-1.005 5.48-.74 7.84.975l19.902 14.46L29.6 40.413l26.536 19.28c3.65 2.65 4.485 7.778 1.858 11.393l-6.288 8.655zm-15.4 0H26.52l3.38-4.653 6.404 4.653z" fill="#4591B3"/></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -3,6 +3,7 @@ window.config = {
|
||||
routerBasename: '/',
|
||||
// default: ''
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
enableGoogleCloudAdapter: true,
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
dicomWeb: [],
|
||||
},
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
oidc: [
|
||||
{
|
||||
// ~ REQUIRED
|
||||
// Authorization Server URL
|
||||
authority: 'https://accounts.google.com',
|
||||
client_id: '99926187585-6nli1cbsf1774f575vj9ti0j7h6ru711.apps.googleusercontent.com', //'YOURCLIENTID.apps.googleusercontent.com',
|
||||
redirect_uri: 'http://localhost:5000/callback', // `OHIFStandaloneViewer.js`
|
||||
response_type: 'id_token token',
|
||||
scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
|
||||
// ~ OPTIONAL
|
||||
post_logout_redirect_uri: '/logout-redirect.html',
|
||||
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
|
||||
"automaticSilentRenew": true,
|
||||
"revokeAccessTokenOnSignout": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -2,6 +2,7 @@ window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
// default: ''
|
||||
showStudyList: true,
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/demo',
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
+11
-1
@@ -9,12 +9,15 @@ import svgr from '@svgr/rollup'
|
||||
import pkg from './package.json'
|
||||
// Deal with https://github.com/rollup/rollup-plugin-commonjs/issues/297
|
||||
import builtins from 'rollup-plugin-node-builtins'
|
||||
import serve from 'rollup-plugin-serve'
|
||||
|
||||
const globals = {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
}
|
||||
|
||||
const startServer = process.env.START_SERVER === 'true';
|
||||
|
||||
export default {
|
||||
input: 'src/index_publish.js',
|
||||
output: [
|
||||
@@ -43,7 +46,7 @@ export default {
|
||||
url(),
|
||||
svgr(),
|
||||
json(),
|
||||
resolve({preferBuiltins: true}),
|
||||
resolve({ preferBuiltins: true }),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
runtimeHelpers: true,
|
||||
@@ -70,6 +73,13 @@ export default {
|
||||
},
|
||||
}),
|
||||
builtins(),
|
||||
startServer && serve({
|
||||
open: true,
|
||||
// Multiple folders to serve from
|
||||
contentBase: ['.', 'dist', 'cypress/support/script-tag', 'public'],
|
||||
host: 'localhost',
|
||||
port: 5000,
|
||||
})
|
||||
],
|
||||
external: Object.keys(pkg.peerDependencies || {}),
|
||||
}
|
||||
@@ -42,6 +42,9 @@ class OHIFStandaloneViewer extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
user: PropTypes.object,
|
||||
setContext: PropTypes.func,
|
||||
userManager: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -93,14 +96,6 @@ class OHIFStandaloneViewer extends Component {
|
||||
* See http://reactcommunity.org/react-transition-group/with-react-router/
|
||||
*/
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
Component: StudyListRouting,
|
||||
},
|
||||
{
|
||||
path: '/studylist',
|
||||
Component: StudyListRouting,
|
||||
},
|
||||
{
|
||||
path: '/viewer',
|
||||
Component: StandaloneRouting,
|
||||
@@ -110,7 +105,7 @@ class OHIFStandaloneViewer extends Component {
|
||||
Component: ViewerRouting,
|
||||
},
|
||||
{
|
||||
path: '/study/:studyInstanceUid/series/:seriesInstanceUids',
|
||||
path: '/study/:studyInstanceUids/series/:seriesInstanceUids',
|
||||
Component: ViewerRouting,
|
||||
},
|
||||
{
|
||||
@@ -119,6 +114,21 @@ class OHIFStandaloneViewer extends Component {
|
||||
},
|
||||
];
|
||||
|
||||
const showStudyList =
|
||||
window.config.showStudyList !== undefined
|
||||
? window.config.showStudyList
|
||||
: true;
|
||||
if (showStudyList) {
|
||||
routes.push({
|
||||
path: '/studylist',
|
||||
Component: StudyListRouting,
|
||||
});
|
||||
routes.push({
|
||||
path: '/',
|
||||
Component: StudyListRouting,
|
||||
});
|
||||
}
|
||||
|
||||
const currentPath = this.props.location.pathname;
|
||||
const noMatchingRoutes = !routes.find(r =>
|
||||
matchPath(currentPath, {
|
||||
|
||||
@@ -140,6 +140,7 @@ function convertTimepointsToTableData(timepoints) {
|
||||
return [
|
||||
{
|
||||
label: 'Study date:',
|
||||
key: 'StudyDate',
|
||||
date: moment(timepoints[0].latestDate).format('DD-MMM-YY'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -71,6 +71,10 @@ class Header extends Component {
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const showStudyList =
|
||||
window.config.showStudyList !== undefined
|
||||
? window.config.showStudyList
|
||||
: true;
|
||||
return (
|
||||
<div className={`entry-header ${this.props.home ? 'header-big' : ''}`}>
|
||||
<div className="header-left-box">
|
||||
@@ -85,7 +89,7 @@ class Header extends Component {
|
||||
|
||||
{this.props.children}
|
||||
|
||||
{!this.props.home && (
|
||||
{showStudyList && !this.props.home && (
|
||||
<Link
|
||||
className="header-btn header-studyListLinkSection"
|
||||
to={{
|
||||
|
||||
@@ -13,7 +13,7 @@ class PluginSwitch extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="PluginSwitch">
|
||||
<ExpandableToolMenu buttons={this.props.buttons} />
|
||||
<ExpandableToolMenu buttons={this.props.buttons} text={'View'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import './ToolbarRow.css';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
ExpandableToolMenu,
|
||||
RoundedButtonGroup,
|
||||
ToolbarButton,
|
||||
ExpandableToolMenu,
|
||||
} from 'react-viewerbase';
|
||||
import { commandsManager, extensionManager } from './../App.js';
|
||||
|
||||
@@ -13,6 +13,7 @@ import ConnectedLayoutButton from './ConnectedLayoutButton';
|
||||
import ConnectedPluginSwitch from './ConnectedPluginSwitch.js';
|
||||
import { MODULE_TYPES } from 'ohif-core';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class ToolbarRow extends Component {
|
||||
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
||||
@@ -53,7 +54,7 @@ class ToolbarRow extends Component {
|
||||
{
|
||||
value: 'studies',
|
||||
icon: 'th-large',
|
||||
bottomLabel: 'Series',
|
||||
bottomLabel: this.props.t('Series'),
|
||||
},
|
||||
],
|
||||
right: [],
|
||||
@@ -155,7 +156,7 @@ class ToolbarRow extends Component {
|
||||
*/
|
||||
function _getButtonComponents(toolbarButtons, activeButtons) {
|
||||
return toolbarButtons.map((button, index) => {
|
||||
if (button.buttons) {
|
||||
if (button.buttons && button.buttons.length) {
|
||||
// Iterate over button definitions and update `onClick` behavior
|
||||
const childButtons = button.buttons.map(childButton => {
|
||||
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
|
||||
@@ -171,7 +172,6 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={button.id}
|
||||
@@ -239,4 +239,4 @@ function _handleBuiltIn({ behavior } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export default ToolbarRow;
|
||||
export default withTranslation('Common')(ToolbarRow);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { connect } from 'react-redux';
|
||||
import DicomFileUploaderModal from './DicomFileUploaderModal.js';
|
||||
|
||||
const isActive = a => a.active === true;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeServer = state.servers.servers.find(isActive);
|
||||
const { authority, client_id } = window.config.oidc[0];
|
||||
const oidcStorageKey = `oidc.user:${authority}:${client_id}`;
|
||||
|
||||
return {
|
||||
oidcStorageKey,
|
||||
url: activeServer && activeServer.qidoRoot,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedDicomFileUploader = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(DicomFileUploaderModal);
|
||||
|
||||
export default ConnectedDicomFileUploader;
|
||||
@@ -0,0 +1,34 @@
|
||||
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);
|
||||
const { authority, client_id } = window.config.oidc[0];
|
||||
const oidcStorageKey = `oidc.user:${authority}:${client_id}`;
|
||||
|
||||
return {
|
||||
oidcStorageKey,
|
||||
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;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import DatasetsList from './DatasetsList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class DatasetPicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
project: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
oidcKey: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setOidcStorageKey(this.props.oidcKey);
|
||||
|
||||
const response = await api.loadDatasets(
|
||||
this.props.project.projectId,
|
||||
this.props.location.locationId
|
||||
);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
datasets: response.data.datasets || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { datasets, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<DatasetsList
|
||||
datasets={datasets}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import DicomStorePicker from './DicomStorePicker';
|
||||
import DatasetPicker from './DatasetPicker';
|
||||
import ProjectPicker from './ProjectPicker';
|
||||
import LocationPicker from './LocationPicker';
|
||||
import GoogleCloudApi from './api/GoogleCloudApi';
|
||||
import './googleCloud.css';
|
||||
|
||||
class DatasetSelector extends Component {
|
||||
state = {
|
||||
project: null,
|
||||
location: null,
|
||||
dataset: null,
|
||||
unloading: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
event: PropTypes.string,
|
||||
oidcKey: PropTypes.string,
|
||||
canClose: PropTypes.string,
|
||||
setServers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onProjectSelect = project => {
|
||||
this.setState({
|
||||
project,
|
||||
});
|
||||
};
|
||||
|
||||
onLocationSelect = location => {
|
||||
this.setState({
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
onDatasetSelect = dataset => {
|
||||
this.setState({
|
||||
dataset,
|
||||
});
|
||||
};
|
||||
|
||||
onProjectClick = () => {
|
||||
this.setState({
|
||||
dataset: null,
|
||||
location: null,
|
||||
project: null,
|
||||
});
|
||||
};
|
||||
|
||||
onLocationClick = () => {
|
||||
this.setState({
|
||||
dataset: null,
|
||||
location: null,
|
||||
});
|
||||
};
|
||||
|
||||
onDatasetClick = () => {
|
||||
this.setState({
|
||||
dataset: null,
|
||||
});
|
||||
};
|
||||
|
||||
onDicomStoreSelect = dicomStoreJson => {
|
||||
const dicomStore = dicomStoreJson.name;
|
||||
const parts = dicomStore.split('/');
|
||||
const result = {
|
||||
wadoUriRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`,
|
||||
qidoRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`,
|
||||
wadoRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`,
|
||||
project: parts[1],
|
||||
location: parts[3],
|
||||
dataset: parts[5],
|
||||
dicomStore: parts[7],
|
||||
};
|
||||
this.props.setServers(result);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { project, location, dataset } = this.state;
|
||||
const {
|
||||
onProjectClick,
|
||||
onLocationClick,
|
||||
onDatasetClick,
|
||||
onProjectSelect,
|
||||
onLocationSelect,
|
||||
onDatasetSelect,
|
||||
onDicomStoreSelect,
|
||||
} = this;
|
||||
|
||||
let projectBreadcrumbs = (
|
||||
<div className="gcp-picker--path">
|
||||
<span>{this.props.t('Select a Project')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (project) {
|
||||
projectBreadcrumbs = (
|
||||
<div className="gcp-picker--path">
|
||||
<span onClick={onProjectClick}>{project.name}</span>
|
||||
{project && location && (
|
||||
<span onClick={onLocationClick}>
|
||||
{' '}
|
||||
-> {location.name.split('/')[3]}
|
||||
</span>
|
||||
)}
|
||||
{project && location && dataset && (
|
||||
<span onClick={onDatasetClick}>
|
||||
{' '}
|
||||
-> {dataset.name.split('/')[5]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectBreadcrumbs}
|
||||
{!project && (
|
||||
<ProjectPicker
|
||||
oidcKey={this.props.oidcKey}
|
||||
onSelect={onProjectSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
{project && !location && (
|
||||
<LocationPicker
|
||||
oidcKey={this.props.oidcKey}
|
||||
project={project}
|
||||
onSelect={onLocationSelect}
|
||||
/>
|
||||
)}
|
||||
{project && location && !dataset && (
|
||||
<DatasetPicker
|
||||
oidcKey={this.props.oidcKey}
|
||||
project={project}
|
||||
location={location}
|
||||
onSelect={onDatasetSelect}
|
||||
/>
|
||||
)}
|
||||
{project && location && dataset && (
|
||||
<DicomStorePicker
|
||||
oidcKey={this.props.oidcKey}
|
||||
dataset={dataset}
|
||||
onSelect={onDicomStoreSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DatasetSelector);
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from 'react-viewerbase';
|
||||
|
||||
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() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="DatasetList">
|
||||
{this.props.datasets.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblDatasetList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Dataset')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.datasets && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DatasetsList);
|
||||
@@ -0,0 +1,49 @@
|
||||
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,
|
||||
oidcStorageKey: PropTypes.string.isRequired,
|
||||
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('Google Cloud Healthcare API - DICOM Upload')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DicomUploader
|
||||
url={this.props.url}
|
||||
oidcKey={this.props.oidcStorageKey}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomFileUploaderModal);
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from 'react-viewerbase';
|
||||
|
||||
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() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="StoreList">{this.props.stores.map(this.renderTableRow)}</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblStoreList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('DICOM Store')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.stores && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomStoreList);
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import DicomStoreList from './DicomStoreList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class DicomStorePicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
stores: [],
|
||||
locations: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dataset: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const { authority, client_id } = window.config.oidc[0];
|
||||
const oidcStorageKey = `oidc.user:${authority}:${client_id}`;
|
||||
api.setOidcStorageKey(oidcStorageKey);
|
||||
const response = await api.loadDicomStores(this.props.dataset.name);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
stores: response.data.dicomStores || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stores, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
|
||||
return (
|
||||
<DicomStoreList
|
||||
stores={stores}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import DatasetSelector from './DatasetSelector';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class DicomStorePickerModal extends Component {
|
||||
static propTypes = {
|
||||
url: PropTypes.string,
|
||||
oidcStorageKey: PropTypes.string.isRequired,
|
||||
setServers: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
handleEvent = data => {
|
||||
const servers = [
|
||||
{
|
||||
name: data.dicomStore,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
qidoSupportsIncludeField: false,
|
||||
type: 'dicomWeb',
|
||||
qidoRoot: data.qidoRoot,
|
||||
wadoRoot: data.wadoRoot,
|
||||
wadoUriRoot: data.wadoUriRoot,
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
|
||||
this.props.setServers(servers);
|
||||
};
|
||||
|
||||
render() {
|
||||
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('Google Cloud Healthcare API')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DatasetSelector
|
||||
setServers={this.handleEvent}
|
||||
oidcKey={this.props.oidcStorageKey}
|
||||
url={this.props.url}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomStorePickerModal);
|
||||
@@ -0,0 +1,188 @@
|
||||
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 './googleCloud.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,
|
||||
oidcKey: PropTypes.string,
|
||||
};
|
||||
|
||||
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.setOidcStorageKey(this.props.oidcKey);
|
||||
dicomUploader.smartUpload(
|
||||
files.target.files,
|
||||
this.props.url,
|
||||
this.props.oidcKey,
|
||||
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="gcp-dicom-uploader">
|
||||
<div className="button">
|
||||
<label htmlFor="file">
|
||||
<img src="./assets/Button_File.svg" alt="upload file"></img>
|
||||
</label>
|
||||
<input
|
||||
className="gcp-invisible-input"
|
||||
accept=".dcm"
|
||||
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="gcp-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="gcp-picker--path">
|
||||
{this.percents()}% {this.filesLeft()}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ProjectList">
|
||||
{this.state.uploadedList.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import LocationsList from './LocationsList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class LocationPicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
locations: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
project: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
oidcKey: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setOidcStorageKey(this.props.oidcKey);
|
||||
|
||||
const response = await api.loadLocations(this.props.project.projectId);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
locations: response.data.locations || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { locations, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<LocationsList
|
||||
locations={locations}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from 'react-viewerbase';
|
||||
|
||||
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() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="LocationList">
|
||||
{this.props.locations.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblLocationList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Location')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.locations && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(LocationsList);
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import ProjectsList from './ProjectsList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class ProjectPicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
projects: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func,
|
||||
oidcKey: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setOidcStorageKey(this.props.oidcKey);
|
||||
const response = await api.loadProjects();
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
projects: response.data.projects || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { projects, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<ProjectsList
|
||||
projects={projects}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from 'react-viewerbase';
|
||||
|
||||
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() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="ProjectList">
|
||||
{this.props.projects.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>
|
||||
{this.props.projects && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(ProjectsList);
|
||||
@@ -0,0 +1,91 @@
|
||||
import { httpErrorToStr, getOidcToken, checkDicomFile } from '../utils/helpers';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
class DicomUploadService {
|
||||
setOidcStorageKey(oidcStorageKey) {
|
||||
/* eslint-disable */
|
||||
if (!oidcStorageKey) console.error('OIDC storage key is empty');
|
||||
this.oidcStorageKey = oidcStorageKey;
|
||||
}
|
||||
|
||||
async smartUpload(files, url, authToken, uploadCallback, cancellationToken) {
|
||||
/* eslint-disable */
|
||||
const CHUNK_SIZE = 1; // Only one file per request is supported so far
|
||||
const MAX_PARALLEL_JOBS = 50; // FIXME: tune MAX_PARALLEL_JOBS number
|
||||
//
|
||||
let filesArray = Array.from(files);
|
||||
if (filesArray.length === 0) {
|
||||
console.warn('No files are supplied for uploading');
|
||||
return;
|
||||
}
|
||||
let parallelJobsCount = Math.min(filesArray.length, MAX_PARALLEL_JOBS);
|
||||
let completed = false;
|
||||
|
||||
const processJob = async (resolve, reject) => {
|
||||
while (filesArray.length > 0) {
|
||||
if (cancellationToken.get()) return;
|
||||
let chunk = filesArray.slice(0, CHUNK_SIZE);
|
||||
filesArray = filesArray.slice(CHUNK_SIZE);
|
||||
let error = null;
|
||||
try {
|
||||
if (chunk.length > 1) throw new Error('Not implemented');
|
||||
if (chunk.length === 1)
|
||||
await this.simpleUpload(chunk[0], url, authToken);
|
||||
} catch (err) {
|
||||
// It looks like a stupid bug of Babel that err is not an actual Exception object
|
||||
error = httpErrorToStr(err);
|
||||
}
|
||||
chunk.forEach(file => uploadCallback(file.fileId, error));
|
||||
if (!completed && filesArray.length === 0) {
|
||||
completed = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await new Promise(resolve => {
|
||||
for (let i = 0; i < parallelJobsCount; i++) {
|
||||
processJob(resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async simpleUpload(file, url, authToken) {
|
||||
/* eslint-disable */
|
||||
const client = this.getClient(url);
|
||||
const loadedFile = await this.readFile(file);
|
||||
const content = loadedFile.content;
|
||||
if (!checkDicomFile(content))
|
||||
throw new Error('The file has a wrong DICOM header');
|
||||
await client.storeInstances({ datasets: [content] });
|
||||
}
|
||||
|
||||
readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
content: reader.result,
|
||||
});
|
||||
};
|
||||
reader.onerror = error => reject(error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
getClient(url) {
|
||||
if (!this.oidcStorageKey) throw new Error('OIDC storage key is not set');
|
||||
const accessToken = getOidcToken(this.oidcStorageKey);
|
||||
if (!accessToken) throw new Error('OIDC access_token is not set');
|
||||
return new api.DICOMwebClient({
|
||||
url,
|
||||
headers: { Authorization: 'Bearer ' + accessToken },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new DicomUploadService();
|
||||
@@ -0,0 +1,98 @@
|
||||
import { getOidcToken } from '../utils/helpers';
|
||||
|
||||
class GoogleCloudApi {
|
||||
setOidcStorageKey(oidcStorageKey) {
|
||||
if (!oidcStorageKey) console.error('OIDC storage key is empty');
|
||||
this.oidcStorageKey = oidcStorageKey;
|
||||
}
|
||||
|
||||
get fetchConfig() {
|
||||
if (!this.oidcStorageKey) throw new Error('OIDC storage key is not set');
|
||||
const accessToken = getOidcToken(this.oidcStorageKey);
|
||||
if (!accessToken) throw new Error('OIDC access_token is not set');
|
||||
return {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + accessToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get urlBase() {
|
||||
return `https://healthcare.googleapis.com/v1beta1`;
|
||||
}
|
||||
|
||||
get urlBaseProject() {
|
||||
return this.urlBase + `/projects`;
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -0,0 +1,154 @@
|
||||
.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-invisible-input {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: -1000;
|
||||
max-width: 0 !important;
|
||||
max-height: 0 !important;
|
||||
}
|
||||
.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-dicom-uploader .button {
|
||||
float: left;
|
||||
margin: 5px;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export default class CancellationToken {
|
||||
constructor() {
|
||||
this.cancelled = false;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.cancelled = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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.';
|
||||
}
|
||||
|
||||
export function getOidcToken(oidcStorageKey) {
|
||||
const oidcConfigStr = sessionStorage.getItem(oidcStorageKey);
|
||||
if (oidcConfigStr) return JSON.parse(oidcConfigStr).access_token;
|
||||
}
|
||||
|
||||
/* 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]);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ function ViewerRouting({ match }) {
|
||||
if (studyInstanceUids && !seriesInstanceUids) {
|
||||
studyUIDs = studyInstanceUids.split(';');
|
||||
} else if (studyInstanceUids && seriesInstanceUids) {
|
||||
studyUIDs = [match.params.studyInstanceUid];
|
||||
studyUIDs = [studyInstanceUids];
|
||||
seriesUIDs = match.params.seriesInstanceUids.split(';');
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -145,6 +145,8 @@ export default function setupTools(store) {
|
||||
{ name: 'Pan', mouseButtonMasks: [1, 4] },
|
||||
{ name: 'Zoom', mouseButtonMasks: [1, 2] },
|
||||
{ name: 'Wwwc', mouseButtonMasks: [1] },
|
||||
{ name: 'Magnify', mouseButtonMasks: [1] },
|
||||
{ name: 'WwwcRegion', mouseButtonMasks: [1] },
|
||||
{
|
||||
name: 'Bidirectional',
|
||||
props: {
|
||||
@@ -176,7 +178,7 @@ export default function setupTools(store) {
|
||||
{ name: 'Brush', mouseButtonMasks: [1] },
|
||||
{
|
||||
name: 'FreehandMouse',
|
||||
configuration: {
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
@@ -210,6 +212,7 @@ export default function setupTools(store) {
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{ name: 'DragProbe', mouseButtonMasks: [1] },
|
||||
{ name: 'PanMultiTouch' },
|
||||
{ name: 'ZoomTouchPinch' },
|
||||
{ name: 'StackScrollMouseWheel' },
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
import ConnectedStudyList from './ConnectedStudyList';
|
||||
|
||||
// TODO: Move to react-viewerbase
|
||||
|
||||
function StudyListRouting({ match }) {
|
||||
// TODO: Figure out which filters we want to pass in via a URL
|
||||
//const { patientId } = match.params;
|
||||
function toLowerCaseFirstLetter(word) {
|
||||
return word[0].toLowerCase() + word.slice(1);
|
||||
}
|
||||
|
||||
return <ConnectedStudyList />;
|
||||
function getFilters({ search }) {
|
||||
const searchParameters = queryString.parse(search);
|
||||
const filters = {};
|
||||
|
||||
Object.entries(searchParameters).forEach(([key, value]) => {
|
||||
filters[toLowerCaseFirstLetter(key)] = value;
|
||||
});
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
function StudyListRouting({ location }) {
|
||||
const filters = location ? getFilters(location) : undefined;
|
||||
|
||||
return <ConnectedStudyList filters={filters} />;
|
||||
}
|
||||
|
||||
StudyListRouting.propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
patientIds: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default StudyListRouting;
|
||||
export default withRouter(StudyListRouting);
|
||||
@@ -2,18 +2,23 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF from 'ohif-core';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { StudyList } from 'react-viewerbase';
|
||||
import ConnectedHeader from '../connectedComponents/ConnectedHeader.js';
|
||||
import moment from 'moment';
|
||||
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
|
||||
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
|
||||
|
||||
class StudyListWithData extends Component {
|
||||
state = {
|
||||
searchData: {},
|
||||
studies: null,
|
||||
studies: [],
|
||||
error: null,
|
||||
modalComponentId: null,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
filters: PropTypes.object,
|
||||
patientId: PropTypes.string,
|
||||
server: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
@@ -28,23 +33,43 @@ class StudyListWithData extends Component {
|
||||
.subtract(StudyListWithData.studyListDateFilterNumDays, 'days')
|
||||
.toDate();
|
||||
static defaultStudyDateTo = new Date();
|
||||
static defaultSearchData = {
|
||||
currentPage: 0,
|
||||
rowsPerPage: StudyListWithData.rowsPerPage,
|
||||
studyDateFrom: StudyListWithData.defaultStudyDateFrom,
|
||||
studyDateTo: StudyListWithData.defaultStudyDateTo,
|
||||
sortData: StudyListWithData.defaultSort,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// TODO: Avoid using timepoints here
|
||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
||||
|
||||
this.searchForStudies();
|
||||
if (!this.props.server && window.config.enableGoogleCloudAdapter) {
|
||||
this.setState({
|
||||
modalComponentId: 'DicomStorePicker',
|
||||
});
|
||||
} else {
|
||||
this.searchForStudies({
|
||||
...StudyListWithData.defaultSearchData,
|
||||
...(this.props.filters || {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchForStudies = (
|
||||
searchData = {
|
||||
currentPage: 0,
|
||||
rowsPerPage: StudyListWithData.rowsPerPage,
|
||||
studyDateFrom: StudyListWithData.defaultStudyDateFrom,
|
||||
studyDateTo: StudyListWithData.defaultStudyDateTo,
|
||||
sortData: StudyListWithData.defaultSort,
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!this.state.searchData && !this.state.studies) {
|
||||
this.searchForStudies();
|
||||
}
|
||||
) => {
|
||||
if (this.props.server !== prevProps.server) {
|
||||
this.setState({
|
||||
modalComponentId: null,
|
||||
searchData: null,
|
||||
studies: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchForStudies = (searchData = StudyListWithData.defaultSearchData) => {
|
||||
const { server } = this.props;
|
||||
const filter = {
|
||||
patientId: searchData.patientId,
|
||||
@@ -121,6 +146,16 @@ class StudyListWithData extends Component {
|
||||
//console.log('onImport');
|
||||
};
|
||||
|
||||
openModal = modalComponentId => {
|
||||
this.setState({
|
||||
modalComponentId,
|
||||
});
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({ modalComponentId: null });
|
||||
};
|
||||
|
||||
onSelectItem = studyInstanceUID => {
|
||||
this.props.history.push(`/viewer/${studyInstanceUID}`);
|
||||
};
|
||||
@@ -129,16 +164,60 @@ class StudyListWithData extends Component {
|
||||
this.searchForStudies(searchData);
|
||||
};
|
||||
|
||||
closeModals = () => {
|
||||
this.setState({
|
||||
modalComponentId: null,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
} else if (this.state.studies === null) {
|
||||
} else if (this.state.studies === null && !this.state.modalComponentId) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectedHeader home={true} user={this.props.user} />
|
||||
let healthCareApiButtons = null;
|
||||
let healthCareApiWindows = null;
|
||||
|
||||
// TODO: This should probably be a prop
|
||||
if (window.config.enableGoogleCloudAdapter) {
|
||||
healthCareApiWindows = (
|
||||
<>
|
||||
<ConnectedDicomStorePicker
|
||||
isOpen={this.state.modalComponentId === 'DicomStorePicker'}
|
||||
onClose={this.closeModals}
|
||||
/>
|
||||
<ConnectedDicomFilesUploader
|
||||
isOpen={this.state.modalComponentId === 'DicomFilesUploader'}
|
||||
onClose={this.closeModals}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
healthCareApiButtons = (
|
||||
<div
|
||||
className="form-inline btn-group pull-right"
|
||||
style={{ padding: '20px' }}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => this.openModal('DicomStorePicker')}
|
||||
>
|
||||
{this.props.t('Change DICOM Store')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => this.openModal('DicomFilesUploader')}
|
||||
>
|
||||
{this.props.t('Upload Studies')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const studyList = (
|
||||
<div className="paginationArea">
|
||||
<StudyList
|
||||
studies={this.state.studies}
|
||||
studyListFunctionsEnabled={false}
|
||||
@@ -150,10 +229,19 @@ class StudyListWithData extends Component {
|
||||
StudyListWithData.studyListDateFilterNumDays
|
||||
}
|
||||
onSearch={this.onSearch}
|
||||
/>
|
||||
>
|
||||
{healthCareApiButtons}
|
||||
{healthCareApiWindows}
|
||||
</StudyList>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ConnectedHeader home={true} user={this.props.user} />
|
||||
{studyList}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(StudyListWithData);
|
||||
export default withRouter(withTranslation('Common')(StudyListWithData));
|
||||
Reference in new issue
Block a user