Compare commits

...
Author SHA1 Message Date
Erik Ziegler 3a4ee9e346 fix(DicomUploadService): Remove require usage (#649) 2019-07-11 12:18:25 +02:00
Biharck Araujo 1a9cfea365 fix(ToolbarRow): Add i18n string for Series switcher (#634) 2019-07-11 11:32:09 +02:00
Erik Ziegler 9055f19fb5 ci(Add e2e test for script tag usage to CI): (#648) 2019-07-11 11:06:32 +02:00
Erik Ziegler bcbc5e4bbf fix(yarn.lock): Blow away lockfile because it seems to be broken for some users (#643) 2019-07-11 10:18:21 +02:00
dependabot[bot] 408ba36f4f chore(deps): bump lodash.merge from 4.6.1 to 4.6.2 in /docs/latest (#645)
Bumps [lodash.merge](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-11 09:01:59 +02:00
Erik Ziegler 091cab49e0 refactor(GoogleCloud): Clean up some of the Healthcare API Adapter components (#641) 2019-07-10 14:54:27 +02:00
Egor Lezhnin 4a09f33446 feat(GoogleCloud): Initial commits to include the Healthcare API Adapter (#584)
* This is a React port of https://github.com/quantumsoftgroup/healthcare-api-adapter - It still needs some UI cleanup.
2019-07-10 09:26:19 +02:00
Erik Ziegler 4095629310 fix(package.json): Bump @ohif/extension-cornerstone to 0.0.40 (#638) 2019-07-09 18:38:31 +02:00
Erik Ziegler 91d2d31846 @ohif/extension-cornerstone: Prevent errors when display set is empty object (0.0.40) (#637) 2019-07-09 18:17:13 +02:00
João Felipe de Medeiros Moreira ff2c1cb2f1 test(E2E): End-to-end tests for script tag (#633)
fix #621
2019-07-09 18:08:18 +02:00
Gustavo André Lelis 727c0d8ba6 fix(labelling): Update naming of configuration to props as cornerstoneTools has also changed (#484) 2019-07-08 21:38:03 +02:00
João Felipe de Medeiros Moreira 4b3994e788 feat(StudyList): Disable study list link/route using 'showStudyList' config property (#597)
fix #574
2019-07-08 21:29:59 +02:00
Erik Ziegler 571dd5848f fix(ViewerRouting): Fix routing failures for /study/x/series/y (#617)
re #614
2019-07-08 18:27:08 +02:00
David Wire 2d48d0aa37 feat(StudyList): Add QIDO style query parameters to set initial filters (#543)
This pull request adds the ability to use query parameters to pass filters into the StudyList from
the url. These parameters are of the same form used by the QIDO requests to apply the filters. This
solution seemed to me to be the most obvious way to manage them. There's one open question however,
around what to do with the parameters after the initial load. For now, the URL parameters are
overridden the moment the user tries to add another filter. Depending on review feedback it seems
like it may make more sense for those query parameter filters to be "permanent" and so any further
filtering would augment those parameters rather than replace them.
2019-07-08 18:15:06 +02:00
Erik Ziegler 562d63fa74 fix(package.json): Update @ohif/extension-dicom-microscopy to fix resizing behaviour
fix #550
2019-07-08 14:31:23 +02:00
biharck 1a6246f99b @ohif/extension-dicom-microscopy: Add resize call when the container has resized (0.0.9) 2019-07-08 14:31:23 +02:00
Erik Ziegler ce1a62a961 chore(gitattributes): Fix gitattributes handling of .fig files 2019-07-08 13:49:56 +02:00
Evren Ozkan dd6acb2d03 feat(extension-cornerstone): Add additional tools: Magnify, ROI Window, Probe, Ellipse, Rectangle, Invert, Rotate Right, Flip H, Flip V, Clear 2019-07-08 13:49:56 +02:00
54 changed files with 2579 additions and 545 deletions

No files matched your search

+1
View File
@@ -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
View File
@@ -36,4 +36,5 @@
*.pdf binary
*.ez binary
*.bz2 binary
*.swp binary
*.swp binary
*.fig binary
+77
View File
@@ -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>
+7 -7
View File
@@ -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
View File
@@ -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",
+1
View File
@@ -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

+1
View File
@@ -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,d09GRgABAAAAAAngAA4AAAAADmgAAiMSAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAE0AAABgdXsSVWNtYXAAAAGUAAAAdAAAAYIIaRq4Y3Z0IAAAAggAAABIAAAASCt+BLVmcGdtAAACUAAAAToAAAG8X/Iaq2dhc3AAAAOMAAAADAAAAAwACAATZ2x5ZgAAA5gAAAMqAAAE8AOZz5doZWFkAAAGxAAAADYAAAA2/oPRxGhoZWEAAAb8AAAAHwAAACQM0QOkaG10eAAABxwAAAAsAAAALDJkBEFsb2NhAAAHSAAAABgAAAAYBioHqG1heHAAAAdgAAAAHgAAACACHAHcbmFtZQAAB4AAAAF0AAACi5COTQJwb3N0AAAI9AAAABQAAAAg/20Ab3ByZXAAAAkIAAAA2AAAASkqdjYweJxjYGH5zbSHgZWBgXUWqzEDA6M8hGa+yJDGxMDAAMIQ0MDAoC7AgADu/v7uDApAGMrG8I+BIY19FhOQwzgfJMdixboBLMcMADN+C04AAAB4nGNgYGBmgGAZBkYGEKgB8hjBfBaGBCAtwiAAFGFhUGBwZHBlcGPwYfBnCGAIYgj9/x8oCxJ1QRX9//z/yf8H/u/+v/3/5v87/2/5vxlqMgZgZMMujgzYCSngYmBgYYVYAPQGEycD0FAOwuYOFgAANFkaMQAqAOkApAD+AE4AYAExAKwAxQDUAHwALQAAABT+YAAUApsAIAMhAAsEOgAUBI0AEAWwABQGGAAVAaYAEQbAAA4G3wACAAAAAHicXZDLSgMxGIUTZ6ytF3ApBCFpqEJJcO9qFpmCdDM6XSQuvGALtitfQJiNMGThs5zZzez6OH0J0XSqRd3k5JyffJw/IGqMXmYrSt9dTT/fYE6rXnR/p0EV5+ncgD5o7CjQodCIFB8hGoxurHTcc3819XzEnx+niAethsHMuwsOktt5OCdWIHFse505d6kRrzFxi/EuABbfgEULCO8/NHbVmCM6y+y1RWEYEuOYEDzFMrNYGiac0+hsOwZ9nZ9s2u4pdIYa3Q0ht0gYiPN+46RA4T3zYYMfv/zra0r+B8nvIPxAWtMiayeFFGwdSCFFaOiMRk+Nc5uGiiJU3FfopxoHCjLIoarOacl9bpuExOSp7pJyYhvSj1YvjkEGOC/rY7LN1lseKSRlzcmtrSQxrCEyWhmnvwBTLIi1AAAAAQACAAgAAv//AA94nIWUa0gUURTHz7lzZx/uKrO2sxlL5vhIQnHV7Cm9iNCCKApKIftSUZBQ0UONCtKI+hDRhzB6EMRGEKh3BkOLHgpBlBCLmAaRfTD7kK+sKIyda3dmM3cNbIaBPcve8/+f3/nPAkI9AF0qM3DDdmCwoYLRUFCnaasr7YKIgnhXVxpuQn2pK5g7ZNx2Y1VrtfuUm1QZG8VnHZAl5VVob4PfKxlVdMd0WViEmk/cqnikUbOD90itZi1ZS7abTTL7xi8AgR2Tw3KB/BwUSIdygy7IsGSowvyRmB2vcOCdsmN7E3aYV9H9ibLBBFk/oVmZ2SR3SUlqdgZNVXNKFmZlOpyqP7C4eOkyueAaH2x7wD/exBsYxGoM3uCR8LXGMIavN95B0tbHH7NmXN9d3odlLS38YS8Ojg4NlH34MTIAEGOWJ5jJsGgWZvFgCotybArYTE5Hd2L++Lg4Li5i93onCg+oUBrrhqIBTnUDUYA1Miq6N25kUPTk+P7LfFoxTZ1vTe6UfNaUS8TM9djRgaH793jkWeMV9oi06yCzJt7Ztb6LP29G+iRa8ql3c/+g5SU21zbbS0XMiUOIO2bCNzzUYS3JE2pd6dnkIf8kgMSVDkV3zQyEGhcJEo4WSq/N3SIUNfzkuJWKu3+5fBFeUmAu7DHktHmWpKywQGQW5HaRLIpky6dCiXVICcUbNDxAYxHTAwnGsqykZlDV73CiqqEgKADmIbk1xnvIfCSYdrYBS/lTDl+PnDpeN05Uc0hmn7qvvig1m1Ik19F9+08Ihscmh2lE+FZglSGEhBRM73HKr0sUrplcE/eJPj9omZArXFmhneOjEf6Gm5PiHcL8i3UmlVl03Rj/jCnD0qvomssNmCS9/MPtvb1DP2wxPGrAXpXCkv9wk4SaNCVtJ81C5ZXQ+p03ARWTFH1OoieBqDgdVT/VcjQbUK6ItOCzPDxxnr/jJp9Ab83pM4f4CEFzUma17y/1/vx2+MDBc0R4cwLIu4Q3JyRB2f/WaGfP/uuJrdGduMYk+1srUJKG9i1J7ebXvbyTpGOAaLzT7MfqNsznPTL7tZUsIBrAbz0Oh+oAAAABAAAAAiMS6Qx7uF8PPPUAGQgAAAAAAMTwES4AAAAA1QFS1gAH/+wFXAXEAAEACQACAAAAAAAAeJxjYGRgYM/5x8PAwNrGwM7AxhrDABRBAdwAQ2YChAADjABkBGIAggWGAFYEVQCCBTMAggSAAIIFGwCCAf4AAAVEAHQFKQCCBWIABwAAAAAAQACaAMABBgFMAagBqAHoAjYCeHicY2BkYGDgZpBnYGIAAUYwyccA5DLuATEBCQoA/AAAeJyFkstKw0AUhv+xFyxKEQS3zsqFkEkvlGK7amsphVIhlW4EoZehCcROTUJLn8Q38o3c+ycZiroxw8x85z9zbhAAV/iEQP653DkLlGnlfIYiWpYLuMCD5SL50XIJ13iyXKa+tFzFPd4YJYoVWq/4sCxwiS/LZzgXwnIBN6JiuUi+tVzCnWhZLlN/sVzFs3gfmN0xCjZ+Ihu1el2OjNmEWo63KyV7YSi91BVLT8c62uu18szSJKZvwnVO8g/OdRQHZisbqt5sd9Oc7dzrpF4/SXYd1z0cDmqxW6x8rUy0ccNgpbexjt3JeDCczoZOQ9UwgMEOR0QIsIGPBBIN1FDnkhjRa6iH0LTG2GIFRepRCXl7p6g4szRvzVx7nmu+9Bi95E64+9wh1Z+a/EedZ9liVjGsnXam2FcTbXRPfbZ/xTqn2LSrhLN1+JO4OGRLYUFlwSl8ZlZ8GbF/l+8Dapo14mwGFxNOO8AQU8x4Olnl2jdOS28leJxjYGYAg/9ZDCkMmIAbACoqAdx4nD3NTw7BQByG4Rn/qVDSyJDQhkhEm5RNF900EVYW7EnYWhGUlXADcQM30MzGKcRRHMCC34fYPHk7nS8TpM2AmUxyJWSyK2839EKGJcxrqOtaRh4V/p9FTtuxW0NFN9O+U0bF6rrIKah4x2lWPouEa1dFFpV0rF+lRn23WUQpz9t52aNiMq3E6XWjt5uJrmxl+EoGKjFU+VoewELlvvQ0OpuAPbA1+jEAc3AEF/ACuvZd+Fj4WPgyK2g2F/hEPYAu6PIEHMEdvICHe3YJC2AbhEe8AeAyUcg=) 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

+1
View File
@@ -3,6 +3,7 @@ window.config = {
routerBasename: '/',
// default: ''
relativeWebWorkerScriptsPath: '',
showStudyList: true,
servers: {
dicomWeb: [
{
+1
View File
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
showStudyList: true,
servers: {
dicomWeb: [
{
+1
View File
@@ -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
+26
View File
@@ -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,
},
],
}
+1
View File
@@ -2,6 +2,7 @@ window.config = {
// default: '/'
routerBasename: '/',
// default: ''
showStudyList: true,
relativeWebWorkerScriptsPath: '',
servers: {
dicomWeb: [
+1
View File
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/demo',
showStudyList: true,
servers: {
dicomWeb: [
{
+1
View File
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
showStudyList: true,
servers: {
dicomWeb: [
{
+11 -1
View File
@@ -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 || {}),
}
+19 -9
View File
@@ -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'),
},
];
+5 -1
View File
@@ -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={{
+1 -1
View File
@@ -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>
);
}
+5 -5
View File
@@ -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;
+55
View File
@@ -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}
/>
);
}
}
+156
View File
@@ -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);
+80
View File
@@ -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);
+49
View File
@@ -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);
+78
View File
@@ -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);
+53
View File
@@ -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}
/>
);
}
}
+67
View File
@@ -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);
+188
View File
@@ -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>
);
}
}
+51
View File
@@ -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}
/>
);
}
}
+80
View File
@@ -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);
+49
View File
@@ -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}
/>
);
}
}
+85
View File
@@ -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);
+91
View File
@@ -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();
+98
View File
@@ -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();
+154
View File
@@ -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;
}
}
+40
View File
@@ -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]);
}
+1 -1
View File
@@ -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
View File
@@ -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' },
+24 -10
View File
@@ -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);
+105 -17
View File
@@ -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));
+626 -465
View File
File diff suppressed because it is too large. Load diff