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.
This commit is contained in:
Egor Lezhnin 2019-07-10 10:26:19 +03:00 committed by Erik Ziegler
parent 4095629310
commit 4a09f33446
26 changed files with 1451 additions and 46 deletions

View File

@ -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",

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

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

26
public/config/google.js Normal file
View File

@ -0,0 +1,26 @@
window.config = {
routerBasename: '/',
relativeWebWorkerScriptsPath: '',
enableGoogleCloudAdapter: true,
servers: {
// This is an array, but we'll only use the first entry for now
dicomWeb: [],
},
// This is an array, but we'll only use the first entry for now
oidc: [
{
// ~ REQUIRED
// Authorization Server URL
authority: 'https://accounts.google.com',
client_id: '99926187585-6nli1cbsf1774f575vj9ti0j7h6ru711.apps.googleusercontent.com', //'YOURCLIENTID.apps.googleusercontent.com',
redirect_uri: 'http://localhost:5000/callback', // `OHIFStandaloneViewer.js`
response_type: 'id_token token',
scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
// ~ OPTIONAL
post_logout_redirect_uri: '/logout-redirect.html',
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
"automaticSilentRenew": true,
"revokeAccessTokenOnSignout": true,
},
],
}

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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 (
<DatasetsList
datasets={datasets}
loading={loading}
error={error}
onSelect={onSelect}
></DatasetsList>
);
}
}

View File

@ -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 (
<>
<span className="gcp-picker--title">Google Cloud Healthcare API</span>
{project && (
<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>
)}
{!project && (
<ProjectPicker
oidcKey={this.props.oidcKey}
onSelect={onProjectSelect}
></ProjectPicker>
)}
{project && !location && (
<>
<LocationPicker
oidcKey={this.props.oidcKey}
project={project}
onSelect={onLocationSelect}
></LocationPicker>
</>
)}
{project && location && !dataset && (
<>
<DatasetPicker
oidcKey={this.props.oidcKey}
project={project}
location={location}
onSelect={onDatasetSelect}
></DatasetPicker>
</>
)}
{project && location && dataset && (
<>
<DicomStorePicker
oidcKey={this.props.oidcKey}
dataset={dataset}
onSelect={onDicomStoreSelect}
></DicomStorePicker>
</>
)}
</>
);
}
}

View File

@ -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 (
<tr
key={dataset.name}
className={
this.state.highlightedItem === dataset.name
? 'studylistStudy noselect active'
: 'studylistStudy 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() {
return (
<table id="tblDatasetList" className="studyListToolbar table noselect">
<tbody id="DatasetList">
{this.props.datasets.map(dataset => {
return this.renderTableRow(dataset);
})}
</tbody>
</table>
);
}
}

View File

@ -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 (
<div className="gcp-window">
<DicomUploader
url={this.props.url}
oidcKey={this.props.oidcStorageKey}
></DicomUploader>
<button className="btn btn-primary" onClick={this.props.onClose}>
close
</button>
</div>
);
}
return <></>;
}
}
export default DicomFileUploader;

View File

@ -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 (
<tr
key={store.name}
className={
this.state.highlightedItem === store.name
? 'studylistStudy noselect active'
: 'studylistStudy 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() {
return (
<table id="tblStoreList" className="studyListToolbar table noselect">
<tbody id="StoreList">
{this.props.stores.map(store => {
return this.renderTableRow(store);
})}
</tbody>
</table>
);
}
}

View File

@ -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 (
<DicomStoreList
stores={stores}
loading={loading}
error={error}
onSelect={onSelect}
></DicomStoreList>
);
}
}

View File

@ -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 (
<div className="gcp-window">
<DatasetSelector
setServers={this.handleEvent}
oidcKey={this.props.oidcStorageKey}
url={this.props.url}
></DatasetSelector>
<button className="btn btn-primary" onClick={this.props.onClose}>
close
</button>
</div>
);
}
}
export default DicomStorePickerWindow;

View File

@ -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 = (
<>
<font color="red">{file.error}</font>
</>
);
}
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>
);
} else {
return (
<>
<table
id="tblProjectList"
className="studyListToolbar table noselect"
>
<thead>
<tr>
<th className="gcp-picker--path">
{this.percents()}% {this.filesLeft()}
</th>
</tr>
</thead>
<tbody id="ProjectList">
{this.state.uploadedList.map(file => {
return this.renderTableRow(file);
})}
</tbody>
</table>
</>
);
}
}
}

View File

@ -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 (
<LocationsList
locations={locations}
loading={loading}
error={error}
onSelect={onSelect}
></LocationsList>
);
}
}

View File

@ -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 (
<tr
key={location.locationId}
className={
this.state.highlightedItem === location.locationId
? 'studylistStudy noselect active'
: 'studylistStudy 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() {
return (
<table id="tblLocationList" className="studyListToolbar table noselect">
<tbody id="LocationList">
{this.props.locations.map(project => {
return this.renderTableRow(project);
})}
</tbody>
</table>
);
}
}

View File

@ -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 (
<ProjectsList
projects={projects}
loading={loading}
error={error}
onSelect={onSelect}
></ProjectsList>
);
}
}

View File

@ -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 (
<tr
key={project.projectId}
className={
this.state.highlightedItem === project.projectId
? 'studylistStudy noselect active'
: 'studylistStudy noselect'
}
onMouseEnter={() => {
this.onHighlightItem(project.projectId);
}}
onClick={() => {
this.onHighlightItem(project.projectId);
this.props.onSelect(project);
}}
>
<td>{project.name}</td>
</tr>
);
}
onHighlightItem(project) {
this.setState({ highlightedItem: project });
}
render() {
return (
<table id="tblProjectList" className="studyListToolbar table noselect">
<tbody id="ProjectList">
{this.props.projects.map(project => {
return this.renderTableRow(project);
})}
</tbody>
</table>
);
}
}

View File

@ -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();

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 = {}) {
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();

View File

@ -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;
}

View File

@ -0,0 +1,13 @@
export default class CancellationToken {
constructor() {
this.cancelled = false;
}
get() {
return this.cancelled;
}
set(value) {
this.cancelled = value;
}
}

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]);
}

View File

@ -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 <div>Error: {JSON.stringify(this.state.error)}</div>;
} else if (this.state.studies === null) {
} else if (this.state.studies === null && !this.state.modalComponentId) {
return <div>Loading...</div>;
}
return (
<>
<ConnectedHeader home={true} user={this.props.user} />
let healthCareApiButtons = '';
let healthCareApiWindows = '';
if (window.config.enableGoogleCloudAdapter) {
if (this.state.modalComponentId) {
if (this.state.modalComponentId === 'DicomStorePicker') {
healthCareApiWindows = (
<ConnectedDicomStorePicker onClose={this.update} />
);
} else if (this.state.modalComponentId === 'DicomFilesUploader') {
healthCareApiWindows = (
<ConnectedDicomFilesUploader onClose={this.update} />
);
}
healthCareApiWindows = (
<>
<div className="col-md-4 col-md-offset-2 layoutChooser pagination-area">
{healthCareApiWindows}
</div>
</>
);
}
healthCareApiButtons = (
<>
<div className="form-inline form-group pull-right">
<button
className="btn btn-primary "
onClick={() => this.openModal('DicomStorePicker')}
>
Change Dicom Store
</button>
<button
className="btn btn-primary"
onClick={() => this.openModal('DicomFilesUploader')}
>
Upload Studies
</button>
</div>
</>
);
}
let studyList = <></>;
studyList = (
<div name="paginationArea">
<StudyList
studies={this.state.studies}
studyListFunctionsEnabled={false}
@ -152,7 +235,16 @@ class StudyListWithData extends Component {
StudyListWithData.studyListDateFilterNumDays
}
onSearch={this.onSearch}
/>
>
{healthCareApiButtons}
{healthCareApiWindows}
</StudyList>
</div>
);
return (
<>
<ConnectedHeader home={true} user={this.props.user} />
{studyList}
</>
);
}

119
yarn.lock
View File

@ -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==