Compare commits

...
Author SHA1 Message Date
David Wire 3d425f54cc fix(ToolbarRow): Update ToolbarRow so that an activeButton in a submenu is passed in as an activeCom (#668) 2019-07-17 10:13:59 +02:00
Romulo Bordezani 7bc9b45853 feat(svfCursorsSupport): enable CornerstoneTools SVG Icons on OHIF Viewer (#659) (#672)
- Fixes the issue 659.
2019-07-16 22:41:41 -04:00
João Felipe de Medeiros Moreira 5bd3af0cca fix(Viewer): Removes fixed modality and get the one returned from ohif-core (#664)
* fix(Viewer): Removes fixed modality insert and starting using the information returned from ohif-cor

* fix(Viewer): Includes condition when display set does not have modality information to use UN for un
2019-07-15 10:09:58 -04:00
Erik Ziegler d4aee36542 fix(GoogleCloud): Fix Cloud Healthcare file upload buttons (#658)
fix #654
2019-07-11 23:32:49 +02:00
dependabot[bot] 2a281da19f chore(deps): bump lodash.merge in /extensions/ohif-i18n (#647)
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:54:49 -07:00
dependabot[bot] 4631218dcb chore(deps): bump lodash.merge in /extensions/ohif-vtk-extension (#646)
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:45:47 -07:00
Erik Ziegler 8b3e5dffb1 fix(filesToStudies): Improve error handling for local file viewing (#652) 2019-07-11 14:34:29 +02:00
Erik Ziegler ef6819cf40 fix(docker-google-cloud): Update docker build process to allow Google Cloud CLIENT_ID to be passed in at runtime, update docs. (#651) 2019-07-11 14:08:00 +02:00
Erik Ziegler 2f104b5e91 feat(ViewerLocalFileData): Add drag/drop to load DICOM files in the Viewer at /local (#644) 2019-07-11 13:49:29 +02:00
Erik Ziegler f256ac5a7d fix(package.json): Fix issues with babel-core causes undefined _objectSpread2 (#650) 2019-07-11 12:59:33 +02:00
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
65 changed files with 3006 additions and 658 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
+7
View File
@@ -1,5 +1,6 @@
# Output
dist/
build/
# Dependencies
node_modules/
@@ -7,6 +8,7 @@ node_modules/
# Root
README.md
Dockerfile
dockerfile
# Misc. Config
.git
@@ -14,3 +16,8 @@ Dockerfile
.gitignore
.vscode
.circleci
# Unnecessary things to pull into container
extensions
docs
cypress
+2 -1
View File
@@ -36,4 +36,5 @@
*.pdf binary
*.ez binary
*.bz2 binary
*.swp binary
*.swp binary
*.fig binary
+16 -4
View File
@@ -27,22 +27,34 @@ FROM node:11.2.0-slim as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
ENV GENERATE_SOURCEMAP=false
COPY package.json /usr/src/app/package.json
COPY yarn.lock /usr/src/app/yarn.lock
ADD . /usr/src/app/
# Run the install before copying the rest of the files
RUN yarn install
ENV PATH /usr/src/app/node_modules/.bin:$PATH
ENV GENERATE_SOURCEMAP=false
ENV REACT_APP_CONFIG=config/default.js
# White list instead of copying the whole directory
COPY src /usr/src/app/src
COPY public /usr/src/app/public
COPY .babelrc /usr/src/app/.babelrc
COPY .eslintrc /usr/src/app/.eslintrc
RUN yarn run build:web
# Stage 2: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginx:1.15.5-alpine
RUN apk add --no-cache bash
RUN rm -rf /etc/nginx/conf.d
COPY docker/Viewer-v2.x /etc/nginx/conf.d
COPY docker/Viewer-v2.x/entrypoint.sh /usr/src/
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+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>
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
if [ ! -z "$CLIENT_ID" ]
then
echo "Google Cloud Healthcare $CLIENT_ID has been provided: "
echo $CLIENT_ID
echo "Updating config..."
# - Use SED to replace the CLIENT_ID that is currently in public/config/google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/config/google.js
# - Copy public/config/google.js to overwrite public/config/default.js
cp /usr/share/nginx/html/config/google.js /usr/share/nginx/html/config/default.js
fi
echo "Starting Nginx to serve the OHIF Viewer..."
exec "$@"
@@ -1,11 +1,5 @@
# Google Cloud Healthcare
> ATTENTION: The original documentation for this integration lives in the legacy
> `version 1` Meteor documentation. You can
> [find it here](/history/v1/connecting-to-image-archives/google-cloud-healthcare.html).
> These docs will mirror the Meteor documentation until our `React`
> implementation has been updated to work with Google Cloud Healthcare.
> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a
> powerful option for storing medical imaging data in the cloud.
@@ -16,10 +10,7 @@ store their data in the cloud. It offers an
[almost-entirely complete DICOMWeb API](https://cloud.google.com/healthcare/docs/dicom)
which requires tokens generated via the
[OAuth 2.0 Sign In flow](https://developers.google.com/identity/sign-in/web/sign-in).
Images can even be transcoded on the fly if this is desired. The Cloud
Healthcare API is a very attractive option because it allows us to avoid
deploying the Meteor server entirely. We can just deploy OHIF as a client-only
static site application.
Images can even be transcoded on the fly if this is desired.
## Setup a Google Cloud Healthcare Project
@@ -55,70 +46,26 @@ static site application.
## Run the viewer with your OAuth Client ID
1. Open the `config/oidc-googleCloud.json` file and change `YOURCLIENTID` to
1. Open the `config/google.js` file and change `YOURCLIENTID` to
your Client ID value.
1. Run the OHIF Viewer using the oidc-googleCloud.json configuration file
1. Run the OHIF Viewer using the config/google.js configuration file
```bash
cd OHIFViewer
METEOR_PACKAGE_DIRS="../Packages" meteor npm install
METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/oidc-googleCloud.json
yarn install
REACT_APP_CONFIG=config/google.js yarn run dev
```
## Running via Docker
OHIF is also providing a Docker container which can connect to Google Cloud
Healthcare with a Client ID which is provided at runtime. This is a very simple
method to get up and running. Internally, the container is running
[Nginx](https://nginx.org/) to serve the
[Standalone Viewer](../standalone-viewer/usage.md).
The OHIF Viewer Docker container can be connected to Google Cloud
Healthcare by providing a Client ID at runtime. This is a very simple
method to get up and running.
1. Install Docker (https://www.docker.com/)
1. Run the Docker container, providing a Client ID as an environment variable.
Client IDs look like `xyz.apps.googleusercontent.com`.
```bash
docker run --env CLIENT_ID=$CLIENT_ID --publish 3000:80 ohif/viewer-google-cloud:latest
```
## Building the ohif/viewer-google-cloud Docker Image
The
[ohif/viewer-google-cloud](https://cloud.docker.com/u/ohif/repository/docker/ohif/viewer-google-cloud)
Docker image is built as follows. The Dockerfile and nginx.conf are in the
`/dockersupport/viewer-google-cloud` folder.
1. [Install Meteor](https://www.meteor.com/install)
1. Clone the repository
```bash
git clone https://github.com/OHIF/Viewers.git
cd Viewers
```
1. Install meteor-build-client-fixed2 so you can build the Standalone Viewer
```bash
npm install -g meteor-build-client-fixed2
```
1. Build the Standalone client-only OHIF Viewer
```bash
cd OHIFViewer/
METEOR_PACKAGE_DIRS="../Packages" meteor npm install
METEOR_PACKAGE_DIRS="../Packages" meteor-build-client-fixed2 ../dockersupport/viewer-google-cloud/build -s ../config/oidc.json
```
1. Build the Docker image
```bash
cd ../dockersupport/viewer-google-cloud
docker build -t ohif/viewer-google-cloud .
```
1. Run the Docker image using an OAuth Client ID
```bash
docker run --env CLIENT_ID={$someID}.apps.googleusercontent.com --publish 3000:80 ohif/viewer-google-cloud
docker run --env CLIENT_ID=$CLIENT_ID --publish 3000:80 ohif/viewer:latest
```
+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"
+3 -3
View File
@@ -3571,9 +3571,9 @@ lodash.memoize@^4.1.2:
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
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.pick@^4.2.1:
version "4.4.0"
+3 -3
View File
@@ -4051,9 +4051,9 @@ lodash.isequal@4.5.0:
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.merge@^4.6.1:
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@^4.1.1, lodash@^4.17.11:
version "4.17.11"
+7 -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",
@@ -59,6 +60,7 @@
"not op_mini all"
],
"resolutions": {
"@babel/core": "^7.5.4",
"browserslist": "4.6.2",
"caniuse-lite": "1.0.30000974"
},
@@ -76,9 +78,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",
@@ -99,13 +101,14 @@
"ohif-core": "0.9.1",
"oidc-client": "1.7.x",
"prop-types": "^15.7.2",
"react-dropzone": "^10.1.5",
"react-i18next": "^10.11.0",
"react-redux": "^7.1.0",
"react-resize-detector": "^4.2.0",
"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 +157,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,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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

+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: '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 || {}),
}
+1
View File
@@ -41,6 +41,7 @@ import store from './store';
// ~~~~ APP SETUP
initCornerstoneTools({
globalToolSyncEnabled: true,
showSVGCursors: true,
});
const commandsManagerConfig = {
+22 -7
View File
@@ -9,6 +9,7 @@ import { ViewerbaseDragDropContext } from 'react-viewerbase';
// import asyncComponent from './components/AsyncComponent.js'
import IHEInvokeImageDisplay from './routes/IHEInvokeImageDisplay.js';
import ViewerRouting from './routes/ViewerRouting.js';
import ViewerLocalFileData from './connectedComponents/ViewerLocalFileData.js';
import StudyListRouting from './studylist/StudyListRouting.js';
import StandaloneRouting from './routes/StandaloneRouting.js';
import CallbackPage from './routes/CallbackPage.js';
@@ -42,6 +43,9 @@ class OHIFStandaloneViewer extends Component {
static propTypes = {
history: PropTypes.object.isRequired,
user: PropTypes.object,
setContext: PropTypes.func,
userManager: PropTypes.object,
location: PropTypes.object,
};
componentDidMount() {
@@ -94,12 +98,8 @@ class OHIFStandaloneViewer extends Component {
*/
const routes = [
{
path: '/',
Component: StudyListRouting,
},
{
path: '/studylist',
Component: StudyListRouting,
path: '/local',
Component: ViewerLocalFileData,
},
{
path: '/viewer',
@@ -110,7 +110,7 @@ class OHIFStandaloneViewer extends Component {
Component: ViewerRouting,
},
{
path: '/study/:studyInstanceUid/series/:seriesInstanceUids',
path: '/study/:studyInstanceUids/series/:seriesInstanceUids',
Component: ViewerRouting,
},
{
@@ -119,6 +119,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>
);
}
+13 -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,10 +156,17 @@ class ToolbarRow extends Component {
*/
function _getButtonComponents(toolbarButtons, activeButtons) {
return toolbarButtons.map((button, index) => {
if (button.buttons) {
let activeCommand = undefined;
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);
if (activeButtons.indexOf(childButton.id) > -1) {
activeCommand = childButton.id;
}
return childButton;
});
@@ -168,10 +176,10 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
text={button.label}
icon={button.icon}
buttons={childButtons}
activeCommand={activeCommand}
/>
);
}
return (
<ToolbarButton
key={button.id}
@@ -239,4 +247,4 @@ function _handleBuiltIn({ behavior } = {}) {
}
}
export default ToolbarRow;
export default withTranslation('Common')(ToolbarRow);
+1 -1
View File
@@ -344,7 +344,7 @@ const _mapStudiesToThumbnails = function(studies) {
if (displaySet.images && displaySet.images.length) {
imageId = displaySet.images[0].getImageId();
} else {
altImageText = 'SR';
altImageText = displaySet.modality ? displaySet.modality : 'UN';
}
return {
@@ -0,0 +1,15 @@
.drag-drop-instructions {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.drag-drop-instructions h3 {
color: var(--active-color);
}
.drag-drop-instructions h4 {
color: var(--text-secondary-color);
}
@@ -0,0 +1,117 @@
import React, { Component } from 'react';
import { metadata, utils } from 'ohif-core';
import ConnectedViewer from './ConnectedViewer.js';
import PropTypes from 'prop-types';
import { extensionManager } from './../App.js';
import Dropzone from 'react-dropzone';
import filesToStudies from '../lib/filesToStudies';
import './ViewerLocalFileData.css';
import { withTranslation } from 'react-i18next';
const { OHIFStudyMetadata } = metadata;
const { studyMetadataManager, updateMetaDataManager } = utils;
class ViewerLocalFileData extends Component {
static propTypes = {
studies: PropTypes.array,
};
state = {
studies: null,
loading: false,
error: null,
};
updateStudies = studies => {
// Render the viewer when the data is ready
studyMetadataManager.purge();
// Map studies to new format, update metadata manager?
const updatedStudies = studies.map(study => {
const studyMetadata = new OHIFStudyMetadata(
study,
study.studyInstanceUid
);
const sopClassHandlerModules =
extensionManager.modules['sopClassHandlerModule'];
study.displaySets =
study.displaySets ||
studyMetadata.createDisplaySets(sopClassHandlerModules);
studyMetadata.setDisplaySets(study.displaySets);
// Updates WADO-RS metaDataManager
updateMetaDataManager(study);
studyMetadataManager.add(studyMetadata);
return study;
});
this.setState({
studies: updatedStudies,
});
};
render() {
const onDrop = async acceptedFiles => {
this.setState({ loading: true });
const studies = await filesToStudies(acceptedFiles);
const updatedStudies = this.updateStudies(studies);
if (!updatedStudies) {
return;
}
this.setState({ studies: updatedStudies, loading: false });
};
if (this.state.error) {
return <div>Error: {JSON.stringify(this.state.error)}</div>;
}
return (
<Dropzone onDrop={onDrop}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
{this.state.studies ? (
<ConnectedViewer
studies={this.state.studies}
studyInstanceUids={
this.state.studies &&
this.state.studies.map(a => a.studyInstanceUid)
}
/>
) : (
<div className={'drag-drop-instructions'}>
<div className={'drag-drop-contents'}>
{this.state.loading ? (
<h3>{this.props.t('Loading...')}</h3>
) : (
<>
<h3>
{this.props.t(
'Drag and Drop DICOM files here to load them in the Viewer'
)}
</h3>
<h4>
{this.props.t(
"Or click to load the browser's file selector"
)}
</h4>
</>
)}
</div>
<input {...getInputProps()} style={{ display: 'none' }} />
</div>
)}
</div>
)}
</Dropzone>
);
}
}
export default withTranslation('Common')(ViewerLocalFileData);
@@ -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]);
}
+160
View File
@@ -0,0 +1,160 @@
import cornerstone from 'cornerstone-core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import * as dcmjs from 'dcmjs';
function datasetsToStudies(datasets) {
const StudyInstanceUIDs = new Set();
datasets.forEach(ds => {
StudyInstanceUIDs.add(ds.StudyInstanceUID);
});
const studies = [];
Array.from(StudyInstanceUIDs).forEach(studyInstanceUid => {
const studyDatasets = datasets.filter(
ds => ds.StudyInstanceUID === studyInstanceUid
);
const firstDataset = studyDatasets[0];
const study = {
studyInstanceUid: firstDataset.StudyInstanceUID,
studyDate: firstDataset.StudyDate,
studyTime: firstDataset.StudyTime,
accessionNumber: firstDataset.AccessionNumber,
referringPhysicianName: firstDataset.ReferringPhysicianName,
patientName: firstDataset.PatientName,
patientId: firstDataset.PatientID,
patientBirthdate: firstDataset.PatientBirthDate,
patientSex: firstDataset.PatientSex,
studyId: firstDataset.StudyID,
studyDescription: firstDataset.StudyDescription,
//numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']),
//numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']),
// modality: DICOMWeb.getString(study['00080060']),
// modalitiesInStudy: DICOMWeb.getString(study['00080061']),
//modalities:
seriesList: [],
};
const SeriesInstanceUIDs = new Set();
studyDatasets.forEach(ds => {
SeriesInstanceUIDs.add(ds.SeriesInstanceUID);
});
Array.from(SeriesInstanceUIDs).forEach(seriesInstanceUid => {
const seriesDatasets = studyDatasets.filter(
ds => ds.SeriesInstanceUID === seriesInstanceUid
);
const SOPInstanceUIDs = new Set();
seriesDatasets.forEach(ds => {
SOPInstanceUIDs.add(ds.SOPInstanceUID);
study.seriesList.push({
seriesInstanceUid: ds.SeriesInstanceUID,
seriesDescription: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
instances: [],
});
});
Array.from(SOPInstanceUIDs).forEach(sopInstanceUid => {
const instance = seriesDatasets.find(
a => a.SOPInstanceUID === sopInstanceUid
);
const series = study.seriesList.find(
a => a.seriesInstanceUid === seriesInstanceUid
);
series.instances.push({
sopInstanceUid: instance.SOPInstanceUID,
sopClassUid: instance.SOPClassUID,
rows: instance.Rows,
columns: instance.Columns,
numberOfFrames: instance.NumberOfFrames,
instanceNumber: instance.InstanceNumber,
getImageId: () => instance.imageId, // TODO: Change getImageId to check for instance.imageId property first.
/*imageType: DICOMWeb.getString(instance['00080008']),
modality: DICOMWeb.getString(instance['00080060']),
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(
DICOMWeb.getString(instance['00181065'])
),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),*/
});
});
});
studies.push(study);
});
console.warn(studies);
return studies;
}
export default async function filesToStudies(files) {
const imagePromises = files.map(file => {
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
return cornerstone
.loadAndCacheImage(imageId)
.catch(error => console.warn(error));
});
const images = await Promise.all(imagePromises);
const datasets = [];
images.forEach(image => {
if (!image || !image.data) {
return;
}
const arrayBuffer = image.data.byteArray.buffer;
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
dicomData.meta
);
dataset.imageId = image.imageId;
datasets.push(dataset);
});
return datasetsToStudies(datasets);
}
+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);
+148 -25
View File
@@ -1,19 +1,26 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Dropzone from 'react-dropzone';
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';
import filesToStudies from '../lib/filesToStudies.js';
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 +35,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 +148,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,31 +166,117 @@ class StudyListWithData extends Component {
this.searchForStudies(searchData);
};
closeModals = () => {
this.setState({
modalComponentId: null,
});
};
render() {
const onDrop = async acceptedFiles => {
// Do something with the files
console.warn(acceptedFiles);
try {
const studies = await filesToStudies(acceptedFiles);
this.setState({ studies });
} catch (error) {
this.setState({ error });
}
};
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>;
}
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>
);
}
console.warn(this.state.studies);
const studyList = (
<div className="paginationArea">
{this.state.studies ? (
<StudyList
studies={this.state.studies}
studyListFunctionsEnabled={false}
onImport={this.onImport}
onSelectItem={this.onSelectItem}
pageSize={this.rowsPerPage}
defaultSort={StudyListWithData.defaultSort}
studyListDateFilterNumDays={
StudyListWithData.studyListDateFilterNumDays
}
onSearch={this.onSearch}
>
{healthCareApiButtons}
{healthCareApiWindows}
</StudyList>
) : (
<Dropzone onDrop={onDrop}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()} className={'drag-drop-instructions'}>
<h3>
{this.props.t(
'Drag and Drop DICOM files here to load them in the Viewer'
)}
</h3>
<h4>
{this.props.t("Or click to load the browser's file selector")}
</h4>
<input {...getInputProps()} style={{ display: 'none' }} />
</div>
)}
</Dropzone>
)}
</div>
);
return (
<>
<ConnectedHeader home={true} user={this.props.user} />
<StudyList
studies={this.state.studies}
studyListFunctionsEnabled={false}
onImport={this.onImport}
onSelectItem={this.onSelectItem}
pageSize={this.rowsPerPage}
defaultSort={StudyListWithData.defaultSort}
studyListDateFilterNumDays={
StudyListWithData.studyListDateFilterNumDays
}
onSearch={this.onSearch}
/>
{studyList}
</>
);
}
}
export default withRouter(StudyListWithData);
export default withRouter(withTranslation('Common')(StudyListWithData));
+646 -499
View File
File diff suppressed because it is too large. Load diff