diff --git a/package.json b/package.json
index ecff62337..772b5cd8e 100644
--- a/package.json
+++ b/package.json
@@ -106,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",
diff --git a/public/assets/Button_File.svg b/public/assets/Button_File.svg
new file mode 100644
index 000000000..45043e55e
--- /dev/null
+++ b/public/assets/Button_File.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/assets/Button_Folder.svg b/public/assets/Button_Folder.svg
new file mode 100644
index 000000000..7c3d0e8cc
--- /dev/null
+++ b/public/assets/Button_Folder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/config/google.js b/public/config/google.js
new file mode 100644
index 000000000..2fcbe54f3
--- /dev/null
+++ b/public/config/google.js
@@ -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,
+ },
+ ],
+}
diff --git a/src/connectedComponents/ToolbarRow.js b/src/connectedComponents/ToolbarRow.js
index c25db6a24..76de3c009 100644
--- a/src/connectedComponents/ToolbarRow.js
+++ b/src/connectedComponents/ToolbarRow.js
@@ -1,7 +1,11 @@
import './ToolbarRow.css';
import React, { Component } from 'react';
-import { ExpandableToolMenu, RoundedButtonGroup, ToolbarButton } from 'react-viewerbase';
+import {
+ ExpandableToolMenu,
+ RoundedButtonGroup,
+ ToolbarButton,
+} from 'react-viewerbase';
import { commandsManager, extensionManager } from './../App.js';
import ConnectedCineDialog from './ConnectedCineDialog';
diff --git a/src/googleCloud/ConnectedDicomFilesUploader.js b/src/googleCloud/ConnectedDicomFilesUploader.js
new file mode 100644
index 000000000..15a818fd8
--- /dev/null
+++ b/src/googleCloud/ConnectedDicomFilesUploader.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux';
+import DicomFileUploader from './DicomFileUploader.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
+)(DicomFileUploader);
+
+export default ConnectedDicomFileUploader;
diff --git a/src/googleCloud/ConnectedDicomStorePicker.js b/src/googleCloud/ConnectedDicomStorePicker.js
new file mode 100644
index 000000000..e6dcd6e88
--- /dev/null
+++ b/src/googleCloud/ConnectedDicomStorePicker.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux';
+import DicomStorePickerWindow from './DicomStorePickerWindow.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
+)(DicomStorePickerWindow);
+
+export default ConnectedDicomStorePicker;
diff --git a/src/googleCloud/DatasetPicker.js b/src/googleCloud/DatasetPicker.js
new file mode 100644
index 000000000..a7a62a30d
--- /dev/null
+++ b/src/googleCloud/DatasetPicker.js
@@ -0,0 +1,52 @@
+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 {
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: null,
+ loading: false,
+ datasets: [],
+ };
+ }
+
+ static propTypes = {
+ project: PropTypes.object,
+ location: PropTypes.object,
+ onSelect: PropTypes.func,
+ oidcKey: PropTypes.string,
+ };
+ static defaultProps = {};
+
+ async componentDidMount() {
+ api.setOidcStorageKey(this.props.oidcKey);
+
+ const response = await api.loadDatasets(
+ this.props.project.projectId,
+ this.props.location.locationId
+ );
+ this.loading = false;
+ if (response.isError) {
+ this.error = response.message;
+ return;
+ }
+ this.setState({ datasets: response.data.datasets || [] });
+ }
+
+ render() {
+ const { datasets, loading, error } = this.state;
+ const { onSelect } = this.props;
+ return (
+
+ );
+ }
+}
diff --git a/src/googleCloud/DatasetSelector.js b/src/googleCloud/DatasetSelector.js
new file mode 100644
index 000000000..85119fb59
--- /dev/null
+++ b/src/googleCloud/DatasetSelector.js
@@ -0,0 +1,156 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import DicomStorePicker from './DicomStorePicker';
+import DatasetPicker from './DatasetPicker';
+import ProjectPicker from './ProjectPicker';
+import LocationPicker from './LocationPicker';
+import GoogleCloudApi from './api/GoogleCloudApi';
+import './googleCloud.css';
+
+export default class DatasetSelector extends Component {
+ constructor(props) {
+ super(props);
+ this.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,
+ };
+ static defaultProps = {};
+
+ onProjectSelect = project => {
+ this.setState({
+ project: project,
+ });
+ };
+
+ onLocationSelect = location => {
+ this.setState({
+ location: location,
+ });
+ };
+
+ onDatasetSelect = dataset => {
+ this.setState({
+ dataset: 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;
+ return (
+ <>
+ Google Cloud Healthcare API
+ {project && (
+
+ {project.name}
+ {project && location && (
+ <>
+
+ {' '}
+ -> {location.name.split('/')[3]}
+
+ {project && location && dataset && (
+
+ {' '}
+ -> {dataset.name.split('/')[5]}
+
+ )}
+ >
+ )}
+
+ )}
+
+ {!project && (
+
+ )}
+
+ {project && !location && (
+ <>
+
+ >
+ )}
+ {project && location && !dataset && (
+ <>
+
+ >
+ )}
+ {project && location && dataset && (
+ <>
+
+ >
+ )}
+ >
+ );
+ }
+}
diff --git a/src/googleCloud/DatasetsList.js b/src/googleCloud/DatasetsList.js
new file mode 100644
index 000000000..af1d038b0
--- /dev/null
+++ b/src/googleCloud/DatasetsList.js
@@ -0,0 +1,57 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import './googleCloud.css';
+
+export default class DatasetsList extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ search: '',
+ };
+ }
+
+ static propTypes = {
+ datasets: PropTypes.array,
+ loading: PropTypes.bool,
+ error: PropTypes.string,
+ onSelect: PropTypes.func,
+ };
+ static defaultProps = {};
+
+ renderTableRow(dataset) {
+ return (
+ {
+ this.onHighlightItem(dataset.name);
+ }}
+ onClick={() => {
+ this.props.onSelect(dataset);
+ }}
+ >
+ | {dataset.name.split('/')[5]} |
+
+ );
+ }
+
+ onHighlightItem(dataset) {
+ this.setState({ highlightedItem: dataset });
+ }
+
+ render() {
+ return (
+
+
+ {this.props.datasets.map(dataset => {
+ return this.renderTableRow(dataset);
+ })}
+
+
+ );
+ }
+}
diff --git a/src/googleCloud/DicomFileUploader.js b/src/googleCloud/DicomFileUploader.js
new file mode 100644
index 000000000..9e7fe0b76
--- /dev/null
+++ b/src/googleCloud/DicomFileUploader.js
@@ -0,0 +1,38 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import DicomUploader from './DicomUploader';
+
+class DicomFileUploader extends Component {
+ static propTypes = {
+ url: PropTypes.string,
+ oidcStorageKey: PropTypes.string.isRequired,
+ onClose: PropTypes.func,
+ };
+ state = {
+ uploaded: false,
+ };
+
+ constructor(props) {
+ super(props);
+ this.element = React.createRef();
+ }
+
+ render() {
+ if (this.props.url != null) {
+ return (
+
+
+
+
+ );
+ }
+ return <>>;
+ }
+}
+
+export default DicomFileUploader;
diff --git a/src/googleCloud/DicomStoreList.js b/src/googleCloud/DicomStoreList.js
new file mode 100644
index 000000000..ba5a16ca6
--- /dev/null
+++ b/src/googleCloud/DicomStoreList.js
@@ -0,0 +1,57 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import './googleCloud.css';
+
+export default class DicomStoreList extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ search: '',
+ };
+ }
+
+ static propTypes = {
+ stores: PropTypes.array,
+ loading: PropTypes.bool,
+ error: PropTypes.string,
+ onSelect: PropTypes.func,
+ };
+ static defaultProps = {};
+
+ renderTableRow(store) {
+ return (
+ {
+ this.onHighlightItem(store.name);
+ }}
+ onClick={() => {
+ this.props.onSelect(store);
+ }}
+ >
+ | {store.name.split('/')[7]} |
+
+ );
+ }
+
+ onHighlightItem(store) {
+ this.setState({ highlightedItem: store });
+ }
+
+ render() {
+ return (
+
+
+ {this.props.stores.map(store => {
+ return this.renderTableRow(store);
+ })}
+
+
+ );
+ }
+}
diff --git a/src/googleCloud/DicomStorePicker.js b/src/googleCloud/DicomStorePicker.js
new file mode 100644
index 000000000..f4a44182b
--- /dev/null
+++ b/src/googleCloud/DicomStorePicker.js
@@ -0,0 +1,50 @@
+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 {
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: null,
+ loading: false,
+ stores: [],
+ locations: [],
+ };
+ }
+
+ static propTypes = {
+ dataset: PropTypes.object,
+ onSelect: PropTypes.func,
+ };
+ static defaultProps = {};
+
+ 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);
+ this.loading = false;
+ if (response.isError) {
+ this.error = response.message;
+ return;
+ }
+ this.setState({ stores: response.data.dicomStores || [] });
+ }
+
+ render() {
+ const { stores, loading, error } = this.state;
+ const { onSelect } = this.props;
+
+ return (
+
+ );
+ }
+}
diff --git a/src/googleCloud/DicomStorePickerWindow.js b/src/googleCloud/DicomStorePickerWindow.js
new file mode 100644
index 000000000..8b080f91e
--- /dev/null
+++ b/src/googleCloud/DicomStorePickerWindow.js
@@ -0,0 +1,57 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import DatasetSelector from './DatasetSelector';
+import './googleCloud.css';
+
+class DicomStorePickerWindow extends Component {
+ static propTypes = {
+ url: PropTypes.string,
+ oidcStorageKey: PropTypes.string.isRequired,
+ setServers: PropTypes.func.isRequired,
+ onClose: PropTypes.func,
+ };
+
+ constructor(props) {
+ super(props);
+ this.element = React.createRef();
+ this.handleEvent = this.handleEvent.bind(this);
+ }
+
+ componentDidMount() {}
+
+ handleEvent(data) {
+ const servers = [
+ {
+ name: data.dicomStore,
+ imageRendering: 'wadors',
+ thumbnailRendering: 'wadors',
+ qidoSupportsIncludeField: false,
+ requestOptions: { requestFromBrowser: true },
+ type: 'dicomWeb',
+ qidoRoot: data.qidoRoot,
+ wadoRoot: data.wadoRoot,
+ wadoUriRoot: data.wadoUriRoot,
+ active: true,
+ },
+ ];
+
+ this.props.setServers(servers);
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default DicomStorePickerWindow;
diff --git a/src/googleCloud/DicomUploader.js b/src/googleCloud/DicomUploader.js
new file mode 100644
index 000000000..423c51fb5
--- /dev/null
+++ b/src/googleCloud/DicomUploader.js
@@ -0,0 +1,204 @@
+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 {
+ constructor(props) {
+ super(props);
+ this.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 destinguish between different downloads
+ };
+ this.uploadFiles = this.uploadFiles.bind(this);
+ }
+
+ static propTypes = {
+ id: PropTypes.string,
+ event: PropTypes.string,
+ url: PropTypes.string,
+ oidcKey: PropTypes.string,
+ };
+ static defaultProps = {};
+
+ 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: 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: uploadedList });
+ }
+
+ renderTableRow(file) {
+ let error = <>>;
+ if (file.error != null) {
+ error = (
+ <>
+ {file.error}
+ >
+ );
+ }
+ return (
+
+ |
+ {file.name} {error}
+ |
+
+ );
+ }
+
+ render() {
+ if (this.state.files == null) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ } else {
+ return (
+ <>
+
+
+
+ |
+ {this.percents()}% {this.filesLeft()}
+ |
+
+
+
+ {this.state.uploadedList.map(file => {
+ return this.renderTableRow(file);
+ })}
+
+
+ >
+ );
+ }
+ }
+}
diff --git a/src/googleCloud/LocationPicker.js b/src/googleCloud/LocationPicker.js
new file mode 100644
index 000000000..f15782dc2
--- /dev/null
+++ b/src/googleCloud/LocationPicker.js
@@ -0,0 +1,50 @@
+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 {
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: null,
+ loading: false,
+ 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);
+
+ this.loading = false;
+ if (response.isError) {
+ this.error = response.message;
+ return;
+ }
+ this.setState({ locations: response.data.locations || [] });
+ }
+
+ static defaultProps = {};
+
+ render() {
+ const { locations, loading, error } = this.state;
+ const { onSelect } = this.props;
+ return (
+
+ );
+ }
+}
diff --git a/src/googleCloud/LocationsList.js b/src/googleCloud/LocationsList.js
new file mode 100644
index 000000000..a1ad230a7
--- /dev/null
+++ b/src/googleCloud/LocationsList.js
@@ -0,0 +1,56 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import './googleCloud.css';
+export default class LocationsList extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ search: '',
+ };
+ }
+
+ static propTypes = {
+ locations: PropTypes.array,
+ loading: PropTypes.bool,
+ error: PropTypes.string,
+ onSelect: PropTypes.func,
+ };
+ static defaultProps = {};
+
+ renderTableRow(location) {
+ return (
+ {
+ this.onHighlightItem(location.locationId);
+ }}
+ onClick={() => {
+ this.props.onSelect(location);
+ }}
+ >
+ | {location.name.split('/')[3]} |
+
+ );
+ }
+
+ onHighlightItem(locationId) {
+ this.setState({ highlightedItem: locationId });
+ }
+
+ render() {
+ return (
+
+
+ {this.props.locations.map(project => {
+ return this.renderTableRow(project);
+ })}
+
+
+ );
+ }
+}
diff --git a/src/googleCloud/ProjectPicker.js b/src/googleCloud/ProjectPicker.js
new file mode 100644
index 000000000..d9ba1f8c7
--- /dev/null
+++ b/src/googleCloud/ProjectPicker.js
@@ -0,0 +1,46 @@
+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 {
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: null,
+ loading: false,
+ projects: [],
+ };
+ }
+
+ static propTypes = {
+ onSelect: PropTypes.func,
+ oidcKey: PropTypes.string,
+ };
+ static defaultProps = {};
+
+ async componentDidMount() {
+ api.setOidcStorageKey(this.props.oidcKey);
+ const response = await api.loadProjects();
+ this.loading = false;
+ if (response.isError) {
+ this.error = response.message;
+ return;
+ }
+ this.setState({ projects: response.data.projects || [] });
+ }
+
+ render() {
+ const { projects, loading, error } = this.state;
+ const { onSelect } = this.props;
+ return (
+
+ );
+ }
+}
diff --git a/src/googleCloud/ProjectsList.js b/src/googleCloud/ProjectsList.js
new file mode 100644
index 000000000..98974d8a5
--- /dev/null
+++ b/src/googleCloud/ProjectsList.js
@@ -0,0 +1,59 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import './googleCloud.css';
+
+export default class ProjectsList extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ search: '',
+ highlightedItem: null,
+ };
+ }
+
+ static propTypes = {
+ projects: PropTypes.array,
+ loading: PropTypes.bool,
+ error: PropTypes.string,
+ onSelect: PropTypes.func.isRequired,
+ };
+ static defaultProps = {};
+
+ renderTableRow(project) {
+ return (
+ {
+ this.onHighlightItem(project.projectId);
+ }}
+ onClick={() => {
+ this.onHighlightItem(project.projectId);
+ this.props.onSelect(project);
+ }}
+ >
+ | {project.name} |
+
+ );
+ }
+
+ onHighlightItem(project) {
+ this.setState({ highlightedItem: project });
+ }
+
+ render() {
+ return (
+
+
+ {this.props.projects.map(project => {
+ return this.renderTableRow(project);
+ })}
+
+
+ );
+ }
+}
diff --git a/src/googleCloud/api/DicomUploadService.js b/src/googleCloud/api/DicomUploadService.js
new file mode 100644
index 000000000..4ad30ce40
--- /dev/null
+++ b/src/googleCloud/api/DicomUploadService.js
@@ -0,0 +1,92 @@
+import { httpErrorToStr, getOidcToken, checkDicomFile } from '../utils/helpers';
+const DICOM = require('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) {
+ const promise = new Promise((resolve, reject) => {
+ var 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);
+ });
+ return promise;
+ }
+
+ 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 DICOM.api.DICOMwebClient({
+ url,
+ headers: { Authorization: 'Bearer ' + accessToken },
+ });
+ }
+}
+
+export default new DicomUploadService();
diff --git a/src/googleCloud/api/GoogleCloudApi.js b/src/googleCloud/api/GoogleCloudApi.js
new file mode 100644
index 000000000..616ae8259
--- /dev/null
+++ b/src/googleCloud/api/GoogleCloudApi.js
@@ -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 = {}) {
+ var 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 (var key in data) {
+ if (data.hasOwnProperty(key)) {
+ data[key] = data[key].concat(subPage.data[key]);
+ }
+ }
+ }
+ return {
+ isError: false,
+ status: response.status,
+ data: 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();
diff --git a/src/googleCloud/googleCloud.css b/src/googleCloud/googleCloud.css
new file mode 100644
index 000000000..466483d93
--- /dev/null
+++ b/src/googleCloud/googleCloud.css
@@ -0,0 +1,49 @@
+.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--title {
+ color: #ffffff;
+ font-size: 24px;
+ font-weight: 28px;
+ text-align: center;
+ margin: 20px auto;
+}
+
+.gcp-picker--path {
+ color: #ffffff;
+ font-size: 16px;
+ font-weight: 28px;
+ text-align: leaft;
+ margin: 20px auto;
+}
diff --git a/src/googleCloud/utils/CancellationToken.js b/src/googleCloud/utils/CancellationToken.js
new file mode 100644
index 000000000..cc40fdd57
--- /dev/null
+++ b/src/googleCloud/utils/CancellationToken.js
@@ -0,0 +1,13 @@
+export default class CancellationToken {
+ constructor() {
+ this.cancelled = false;
+ }
+
+ get() {
+ return this.cancelled;
+ }
+
+ set(value) {
+ this.cancelled = value;
+ }
+}
diff --git a/src/googleCloud/utils/helpers.js b/src/googleCloud/utils/helpers.js
new file mode 100644
index 000000000..fda924c39
--- /dev/null
+++ b/src/googleCloud/utils/helpers.js
@@ -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]);
+}
diff --git a/src/studylist/StudyListWithData.js b/src/studylist/StudyListWithData.js
index 38c630eb6..c6bf3d4c4 100644
--- a/src/studylist/StudyListWithData.js
+++ b/src/studylist/StudyListWithData.js
@@ -5,11 +5,16 @@ import { withRouter } from 'react-router-dom';
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 = {
- studies: null,
+ searchData: {},
+ studies: [],
error: null,
+ modalComponentId: null,
+ showStudyList: true,
};
static propTypes = {
@@ -39,11 +44,31 @@ class StudyListWithData extends Component {
componentDidMount() {
// TODO: Avoid using timepoints here
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
+ if (!this.props.server && window.config.enableGoogleCloudAdapter) {
+ this.setState({
+ modalComponentId: 'DicomStorePicker',
+ showStudyList: false,
+ });
+ } else {
+ this.searchForStudies({
+ ...StudyListWithData.defaultSearchData,
+ ...(this.props.filters || {}),
+ });
+ }
+ }
- this.searchForStudies({
- ...StudyListWithData.defaultSearchData,
- ...(this.props.filters || {}),
- });
+ componentDidUpdate(prevProps) {
+ if (!this.state.searchData && !this.state.studies) {
+ this.searchForStudies();
+ }
+ if (this.props.server !== prevProps.server) {
+ this.setState({
+ modalComponentId: null,
+ showStudyList: true,
+ searchData: null,
+ studies: null,
+ });
+ }
}
searchForStudies = (searchData = StudyListWithData.defaultSearchData) => {
@@ -123,6 +148,17 @@ class StudyListWithData extends Component {
//console.log('onImport');
};
+ openModal = modalComponentId => {
+ this.setState({
+ modalComponentId,
+ showStudyList: false,
+ });
+ };
+
+ closeModal = () => {
+ this.setState({ modalComponentId: null });
+ };
+
onSelectItem = studyInstanceUID => {
this.props.history.push(`/viewer/${studyInstanceUID}`);
};
@@ -131,16 +167,63 @@ class StudyListWithData extends Component {
this.searchForStudies(searchData);
};
+ update = () => {
+ this.setState({
+ modalComponentId: null,
+ showStudyList: true,
+ });
+ };
+
render() {
if (this.state.error) {
return Error: {JSON.stringify(this.state.error)}
;
- } else if (this.state.studies === null) {
+ } else if (this.state.studies === null && !this.state.modalComponentId) {
return Loading...
;
}
- return (
- <>
-
+ let healthCareApiButtons = '';
+ let healthCareApiWindows = '';
+ if (window.config.enableGoogleCloudAdapter) {
+ if (this.state.modalComponentId) {
+ if (this.state.modalComponentId === 'DicomStorePicker') {
+ healthCareApiWindows = (
+
+ );
+ } else if (this.state.modalComponentId === 'DicomFilesUploader') {
+ healthCareApiWindows = (
+
+ );
+ }
+ healthCareApiWindows = (
+ <>
+
+ {healthCareApiWindows}
+
+ >
+ );
+ }
+ healthCareApiButtons = (
+ <>
+
+
+
+
+ >
+ );
+ }
+ let studyList = <>>;
+ studyList = (
+
+ >
+ {healthCareApiButtons}
+ {healthCareApiWindows}
+
+
+ );
+ return (
+ <>
+
+ {studyList}
>
);
}
diff --git a/yarn.lock b/yarn.lock
index 4d93670db..7d9279c6f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2921,7 +2921,7 @@ bytes@3.1.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
-cacache@^11.0.1, cacache@^11.0.2, cacache@^11.3.2:
+cacache@^11.0.1, cacache@^11.0.2, cacache@^11.3.2, cacache@^11.3.3:
version "11.3.3"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc"
integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==
@@ -4313,7 +4313,7 @@ debug@3.2.6, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
dependencies:
ms "^2.1.1"
-debuglog@^1.0.1:
+debuglog@*, debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -6344,7 +6344,7 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"
-graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==
@@ -6958,7 +6958,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
-imurmurhash@^0.1.4:
+imurmurhash@*, imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -8602,7 +8602,7 @@ libnpm@^2.0.1:
read-package-json "^2.0.13"
stringify-package "^1.0.0"
-libnpmaccess@^3.0.1:
+libnpmaccess@*, libnpmaccess@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-3.0.1.tgz#5b3a9de621f293d425191aa2e779102f84167fa8"
integrity sha512-RlZ7PNarCBt+XbnP7R6PoVgOq9t+kou5rvhaInoNibhPO7eMlRfS0B8yjatgn2yaHIwWNyoJDolC/6Lc5L/IQA==
@@ -8631,7 +8631,7 @@ libnpmhook@^5.0.2:
get-stream "^4.0.0"
npm-registry-fetch "^3.8.0"
-libnpmorg@^1.0.0:
+libnpmorg@*, libnpmorg@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/libnpmorg/-/libnpmorg-1.0.0.tgz#979b868c48ba28c5820e3bb9d9e73c883c16a232"
integrity sha512-o+4eVJBoDGMgRwh2lJY0a8pRV2c/tQM/SxlqXezjcAg26Qe9jigYVs+Xk0vvlYDWCDhP0g74J8UwWeAgsB7gGw==
@@ -8656,7 +8656,7 @@ libnpmpublish@^1.1.0:
semver "^5.5.1"
ssri "^6.0.1"
-libnpmsearch@^2.0.0:
+libnpmsearch@*, libnpmsearch@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/libnpmsearch/-/libnpmsearch-2.0.1.tgz#eccc73a8fbf267d765d18082b85daa2512501f96"
integrity sha512-K0yXyut9MHHCAH+DOiglQCpmBKPZXSUu76+BE2maSEfQN15OwNaA/Aiioe9lRFlVFOr7WcuJCY+VSl+gLi9NTA==
@@ -8665,7 +8665,7 @@ libnpmsearch@^2.0.0:
get-stream "^4.0.0"
npm-registry-fetch "^3.8.0"
-libnpmteam@^1.0.1:
+libnpmteam@*, libnpmteam@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/libnpmteam/-/libnpmteam-1.0.1.tgz#ff704b1b6c06ea674b3b1101ac3e305f5114f213"
integrity sha512-gDdrflKFCX7TNwOMX1snWojCoDE5LoRWcfOC0C/fqF7mBq8Uz9zWAX4B2RllYETNO7pBupBaSyBDkTAC15cAMg==
@@ -8923,6 +8923,11 @@ lodash-es@^4.17.11:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==
+lodash._baseindexof@*:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
+ integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
+
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -8931,11 +8936,33 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
+lodash._bindcallback@*:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
+ integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
+
+lodash._cacheindexof@*:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
+ integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
+
+lodash._createcache@*:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
+ integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
+ dependencies:
+ lodash._getnative "^3.0.0"
+
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
+lodash._getnative@*, lodash._getnative@^3.0.0:
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+ integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
+
lodash._reinterpolate@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -9011,6 +9038,11 @@ lodash.once@^4.1.1:
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+lodash.restparam@*:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
+ integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
+
lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
@@ -10061,7 +10093,7 @@ npm-logical-tree@^1.2.1:
semver "^5.5.0"
validate-npm-package-name "^3.0.0"
-npm-packlist@^1.1.12, npm-packlist@^1.1.6, npm-packlist@^1.4.1:
+npm-packlist@^1.1.12, npm-packlist@^1.1.6:
version "1.4.1"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==
@@ -10069,6 +10101,14 @@ npm-packlist@^1.1.12, npm-packlist@^1.1.6, npm-packlist@^1.4.1:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
+npm-packlist@^1.4.4:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44"
+ integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
npm-path@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64"
@@ -10085,7 +10125,7 @@ npm-pick-manifest@^2.2.3:
npm-package-arg "^6.0.0"
semver "^5.4.1"
-npm-profile@^4.0.1:
+npm-profile@*, npm-profile@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-4.0.1.tgz#d350f7a5e6b60691c7168fbb8392c3603583f5aa"
integrity sha512-NQ1I/1Q7YRtHZXkcuU1/IyHeLy6pd+ScKg4+DQHdfsm769TGq6HPrkbuNJVJS4zwE+0mvvmeULzQdWn2L2EsVA==
@@ -10128,9 +10168,9 @@ npm-which@^3.0.1:
which "^1.2.10"
npm@^6.8.0:
- version "6.9.1"
- resolved "https://registry.yarnpkg.com/npm/-/npm-6.9.1.tgz#f4cc74841116baf4d9aca89ce83ffa4cba8c3ce0"
- integrity sha512-E4+eXNaGyVa7QiyhBdnsPxd7zb/whI8PrW2RJjqh6HHnweERuko3XTm9MwH+bRmRo6u27uzSGFm9oKwNOReXmQ==
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/npm/-/npm-6.10.0.tgz#7ed37211db53ad486643418c8028092d1ed8b24d"
+ integrity sha512-pOMc81mT4fHXv/iMbw4T4GQVZzlzx/Vf5bta+JgMWVR+qqBeNI0mAbKrQ15vZf3eMJ+DaJj6+XgD7650JQs+rg==
dependencies:
JSONStream "^1.3.5"
abbrev "~1.1.1"
@@ -10139,9 +10179,9 @@ npm@^6.8.0:
aproba "^2.0.0"
archy "~1.0.0"
bin-links "^1.1.2"
- bluebird "^3.5.3"
+ bluebird "^3.5.5"
byte-size "^5.0.1"
- cacache "^11.3.2"
+ cacache "^11.3.3"
call-limit "~1.1.0"
chownr "^1.1.1"
ci-info "^2.0.0"
@@ -10160,7 +10200,7 @@ npm@^6.8.0:
fs-write-stream-atomic "~1.0.10"
gentle-fs "^2.0.1"
glob "^7.1.3"
- graceful-fs "^4.1.15"
+ graceful-fs "^4.2.0"
has-unicode "~2.0.1"
hosted-git-info "^2.7.1"
iferr "^1.0.2"
@@ -10195,7 +10235,7 @@ npm@^6.8.0:
npm-install-checks "~3.0.0"
npm-lifecycle "^2.1.0"
npm-package-arg "^6.1.0"
- npm-packlist "^1.4.1"
+ npm-packlist "^1.4.4"
npm-pick-manifest "^2.2.3"
npm-registry-fetch "^3.9.0"
npm-user-validate "~1.0.0"
@@ -10203,7 +10243,7 @@ npm@^6.8.0:
once "~1.4.0"
opener "^1.5.1"
osenv "^0.1.5"
- pacote "^9.5.0"
+ pacote "^9.5.1"
path-is-inside "~1.0.2"
promise-inflight "~1.0.1"
qrcode-terminal "^0.12.0"
@@ -10213,8 +10253,9 @@ npm@^6.8.0:
read-cmd-shim "~1.0.1"
read-installed "~4.0.3"
read-package-json "^2.0.13"
- read-package-tree "^5.2.2"
- readable-stream "^3.2.0"
+ read-package-tree "^5.3.1"
+ readable-stream "^3.3.0"
+ readdir-scoped-modules "^1.1.0"
request "^2.88.0"
retry "^0.12.0"
rimraf "^2.6.3"
@@ -10226,7 +10267,7 @@ npm@^6.8.0:
sorted-union-stream "~2.1.3"
ssri "^6.0.1"
stringify-package "^1.0.0"
- tar "^4.4.8"
+ tar "^4.4.10"
text-table "~0.2.0"
tiny-relative-date "^1.3.0"
uid-number "0.0.6"
@@ -10239,7 +10280,7 @@ npm@^6.8.0:
validate-npm-package-name "~3.0.0"
which "^1.3.1"
worker-farm "^1.6.0"
- write-file-atomic "^2.4.2"
+ write-file-atomic "^2.4.3"
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2, npmlog@^4.1.2, npmlog@~4.1.2:
version "4.1.2"
@@ -10695,7 +10736,7 @@ package-json@^4.0.0:
registry-url "^3.0.3"
semver "^5.1.0"
-pacote@^9.1.0, pacote@^9.2.3, pacote@^9.5.0:
+pacote@^9.1.0, pacote@^9.2.3, pacote@^9.5.1:
version "9.5.1"
resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.5.1.tgz#adb0d23daeef6d0b813ab5891d0c6459ccec998d"
integrity sha512-Zqvczvf/zZ7QNosdE9uTC7SRuvSs9tFqRkF6cJl+2HH7COBnx4BRAGpeXJlrbN+mM0CMHpbi620xdEHhCflghA==
@@ -12574,10 +12615,10 @@ react-transition-group@^4.1.1:
loose-envify "^1.4.0"
prop-types "^15.6.2"
-react-viewerbase@0.15.3:
- version "0.15.3"
- resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.15.3.tgz#d2d263865dbb4fabc920b8b7c6f75f5dbb95011d"
- integrity sha512-unGny2lE3Ahttyj+6+TK9LkNS/U/62wt+ojKNf0H71Vt2s0v7xqEbNQQPFpu5xLPZIr5rCO3PoveEzijlQqTzg==
+react-viewerbase@0.17.0:
+ version "0.17.0"
+ resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.17.0.tgz#e741f94b24b6cef419fdd32b9bcfcdcfe81783dc"
+ integrity sha512-fy51pHrdhdXcPg6Pb1l8S5fzxlenFVhRUe80ZFq1aK9omrQXgaBF0uhm537WREyx2CS0/ZOD/wQnnbyjqEvYdQ==
dependencies:
"@babel/runtime" "7.2.0"
"@ohif/i18n" "0.2.1"
@@ -12677,10 +12718,10 @@ read-installed@~4.0.3:
optionalDependencies:
graceful-fs "^4.1.2"
-read-package-tree@^5.2.2:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.0.tgz#4f95472e45e7145fb77f4069d12844b139f5ea12"
- integrity sha512-Gi64+EWmi4515E1rPR77ae/Ip8cjFQTlsWytSYJj974U0tSnxm67pyXltbDjB1lvLw4dc85HbtidGL1K2c/oxw==
+read-package-tree@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636"
+ integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==
dependencies:
read-package-json "^2.0.0"
readdir-scoped-modules "^1.0.0"
@@ -12777,7 +12818,7 @@ readable-stream@^1.0.26-4, readable-stream@~1.1.10:
isarray "0.0.1"
string_decoder "~0.10.x"
-readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.2.0:
+readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.3.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
@@ -12818,6 +12859,16 @@ readdir-scoped-modules@^1.0.0:
graceful-fs "^4.1.2"
once "^1.3.0"
+readdir-scoped-modules@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
+ integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==
+ dependencies:
+ debuglog "^1.0.1"
+ dezalgo "^1.0.0"
+ graceful-fs "^4.1.2"
+ once "^1.3.0"
+
readdirp@^2.0.0, readdirp@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -14657,7 +14708,7 @@ tar@^2.0.0:
fstream "^1.0.12"
inherits "2"
-tar@^4, tar@^4.4.8:
+tar@^4, tar@^4.4.10, tar@^4.4.8:
version "4.4.10"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1"
integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==
@@ -15956,7 +16007,7 @@ write-file-atomic@2.4.1:
imurmurhash "^0.1.4"
signal-exit "^3.0.2"
-write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2:
+write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"
integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==