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:
parent
4095629310
commit
4a09f33446
@ -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",
|
||||
|
||||
1
public/assets/Button_File.svg
Normal file
1
public/assets/Button_File.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="228" height="80"><defs><style>@font-face{font-family:"Roboto-Bold";src:url(data:application/font-woff;charset=utf-8;base64,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) format("woff"); font-weight:bold;font-style:normal;}</style></defs><path d="M6 0h216a6 6 0 0 1 6 6v68a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z" fill="#52ABD3"/><text font-family="Roboto-Bold, Roboto" font-size="14" font-weight="bold" fill="#fff"><tspan x="72.148" y="45">FILE UPLOAD</tspan></text><path d="M58.285 79.648H40.642L36.616 58.63l-24.92 10.66 11.258 5.986-2.325 4.373H6a6 6 0 0 1-6-6V55.073L22.088 13.53a1 1 0 0 1 1.352-.413l36.2 19.248L68.3 60.813 58.285 79.648zm-27.108 0h-.923l.203-.382.72.382zM54.232 34.02l-9.86 18.542 18.542 9.86-8.683-28.4z" fill="#4591B3"/></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
public/assets/Button_Folder.svg
Normal file
1
public/assets/Button_Folder.svg
Normal 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
26
public/config/google.js
Normal file
@ -0,0 +1,26 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
enableGoogleCloudAdapter: true,
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
dicomWeb: [],
|
||||
},
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
oidc: [
|
||||
{
|
||||
// ~ REQUIRED
|
||||
// Authorization Server URL
|
||||
authority: 'https://accounts.google.com',
|
||||
client_id: '99926187585-6nli1cbsf1774f575vj9ti0j7h6ru711.apps.googleusercontent.com', //'YOURCLIENTID.apps.googleusercontent.com',
|
||||
redirect_uri: 'http://localhost:5000/callback', // `OHIFStandaloneViewer.js`
|
||||
response_type: 'id_token token',
|
||||
scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
|
||||
// ~ OPTIONAL
|
||||
post_logout_redirect_uri: '/logout-redirect.html',
|
||||
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
|
||||
"automaticSilentRenew": true,
|
||||
"revokeAccessTokenOnSignout": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -1,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';
|
||||
|
||||
22
src/googleCloud/ConnectedDicomFilesUploader.js
Normal file
22
src/googleCloud/ConnectedDicomFilesUploader.js
Normal 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;
|
||||
34
src/googleCloud/ConnectedDicomStorePicker.js
Normal file
34
src/googleCloud/ConnectedDicomStorePicker.js
Normal 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;
|
||||
52
src/googleCloud/DatasetPicker.js
Normal file
52
src/googleCloud/DatasetPicker.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
156
src/googleCloud/DatasetSelector.js
Normal file
156
src/googleCloud/DatasetSelector.js
Normal 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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
57
src/googleCloud/DatasetsList.js
Normal file
57
src/googleCloud/DatasetsList.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
38
src/googleCloud/DicomFileUploader.js
Normal file
38
src/googleCloud/DicomFileUploader.js
Normal 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;
|
||||
57
src/googleCloud/DicomStoreList.js
Normal file
57
src/googleCloud/DicomStoreList.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
50
src/googleCloud/DicomStorePicker.js
Normal file
50
src/googleCloud/DicomStorePicker.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
57
src/googleCloud/DicomStorePickerWindow.js
Normal file
57
src/googleCloud/DicomStorePickerWindow.js
Normal 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;
|
||||
204
src/googleCloud/DicomUploader.js
Normal file
204
src/googleCloud/DicomUploader.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/googleCloud/LocationPicker.js
Normal file
50
src/googleCloud/LocationPicker.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
56
src/googleCloud/LocationsList.js
Normal file
56
src/googleCloud/LocationsList.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
46
src/googleCloud/ProjectPicker.js
Normal file
46
src/googleCloud/ProjectPicker.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
59
src/googleCloud/ProjectsList.js
Normal file
59
src/googleCloud/ProjectsList.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
92
src/googleCloud/api/DicomUploadService.js
Normal file
92
src/googleCloud/api/DicomUploadService.js
Normal 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();
|
||||
98
src/googleCloud/api/GoogleCloudApi.js
Normal file
98
src/googleCloud/api/GoogleCloudApi.js
Normal 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();
|
||||
49
src/googleCloud/googleCloud.css
Normal file
49
src/googleCloud/googleCloud.css
Normal 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;
|
||||
}
|
||||
13
src/googleCloud/utils/CancellationToken.js
Normal file
13
src/googleCloud/utils/CancellationToken.js
Normal file
@ -0,0 +1,13 @@
|
||||
export default class CancellationToken {
|
||||
constructor() {
|
||||
this.cancelled = false;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.cancelled = value;
|
||||
}
|
||||
}
|
||||
40
src/googleCloud/utils/helpers.js
Normal file
40
src/googleCloud/utils/helpers.js
Normal 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]);
|
||||
}
|
||||
@ -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
119
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==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user