feat(DicomUploader): Make DICOM Uploader work with all DICOMWeb servers (#717)
* feat(DicomUploader): Make DICOM Uploader work with all DICOMWeb servers * Bump viewerbase version
This commit is contained in:
parent
903c41d26f
commit
da318c9c8c
@ -108,7 +108,7 @@
|
||||
"react-router": "^5.0.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-transition-group": "^4.1.1",
|
||||
"react-viewerbase": "0.17.0",
|
||||
"react-viewerbase": "0.17.6",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-oidc": "3.1.x",
|
||||
|
||||
@ -21,31 +21,7 @@ window.config = {
|
||||
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
|
||||
automaticSilentRenew: true,
|
||||
revokeAccessTokenOnSignout: true,
|
||||
metadata: {
|
||||
issuer: "https://accounts.google.com",
|
||||
authorization_endpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
token_endpoint: "https://www.googleapis.com/oauth2/v4/token",
|
||||
userinfo_endpoint: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
jwks_uri: "https://www.googleapis.com/oauth2/v3/certs",
|
||||
},
|
||||
signingKeys: [
|
||||
{
|
||||
"kid": "6e5508d27965ad7907c232212defa48ed763727e",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"alg": "RS256",
|
||||
"n": "vOsmJlsBscTxCcOLa6IfCuUnvXI5cBC-o-NIDC1R5O782U_BC67p9dtMLL2UHpQ1wj_b6I4R8cHzddPrPDZ7eTKJw5q18pZU4B5hCmFe9A0JvzyQe3VFPYhKSI5LV_6UIwWdGWjJic8tCJ05AJaVOogSUCn17ss_8KQHSbs66zVbwls9p_ObdHGuzLQE-Y-fkxO5aD9S09DB5dNKuZNL76wgZAhc-HEyo1HkTbDaGAPCk-EpoqfjjoPY2FZGHg5QRCUMdxnYoebjzyO6oaJ8yVniVpOkf-MNF6HltRBbOzE3u6Y2VGjWoj_W0AUidrLC_KQ57URkcAfk1BhMUVJekQ",
|
||||
"use": "sig"
|
||||
},
|
||||
{
|
||||
"use": "sig",
|
||||
"kid": "84f294c45160088d079fee68138f52133d3e228c",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"alg": "RS256",
|
||||
"n": "iyzj9wpDDZLCbgbr2zKv3bs8zqjflcVEd7PYMjKGYpoaY2LdqfjFxrwTqd9Ea4m3NIR2giOx9JLQhtqqSSpBJpBBpHmaEd2FCPwd4GQTKJurEP6Ho9HWAuRTMhs8W04pd__HQ0Bc22AEamieGLtzcYfIaAc9g5RCxZdRVbGK0Z0vSOAwN1PC_S76nWGphouHukU40EiwjqC-D9G2xYFbKNb0_NJMxJ5UCenN85FjEii5-oW0wCBmt_1Sr76Q_e0INxfGu6dRf0vGXPvqxkINz2knjl9ec2SvOK2hnmRN4O9zToKH70_DBrsZE0ePDScTOWPHJU2wOyE6gzkL6FdaFQ"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
studyListFunctionsEnabled: true,
|
||||
}
|
||||
|
||||
@ -21,4 +21,5 @@ window.config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
studyListFunctionsEnabled: true,
|
||||
}
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import { connect } from 'react-redux';
|
||||
import DicomFileUploaderModal from './DicomFileUploaderModal.js';
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
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}`;
|
||||
|
||||
// TODO: Not sure I like this approach since it means we are recreating
|
||||
// this function every time redux changes
|
||||
const retrieveAuthHeaderFunction = () => OHIF.DICOMWeb.getAuthorizationHeader(activeServer);
|
||||
|
||||
return {
|
||||
oidcStorageKey,
|
||||
retrieveAuthHeaderFunction,
|
||||
url: activeServer && activeServer.qidoRoot,
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ import { withTranslation } from 'react-i18next';
|
||||
class DicomFileUploaderModal extends Component {
|
||||
static propTypes = {
|
||||
url: PropTypes.string,
|
||||
oidcStorageKey: PropTypes.string.isRequired,
|
||||
retrieveAuthHeaderFunction: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
@ -32,13 +32,13 @@ class DicomFileUploaderModal extends Component {
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{this.props.t('Google Cloud Healthcare API - DICOM Upload')}
|
||||
{this.props.t('Upload DICOM Files')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DicomUploader
|
||||
url={this.props.url}
|
||||
oidcKey={this.props.oidcStorageKey}
|
||||
retrieveAuthHeaderFunction={this.props.retrieveAuthHeaderFunction}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
|
||||
20
src/googleCloud/DicomUploader.css
Normal file
20
src/googleCloud/DicomUploader.css
Normal file
@ -0,0 +1,20 @@
|
||||
.dicom-uploader .button {
|
||||
float: left;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.invisible-input {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: -1000;
|
||||
max-width: 0 !important;
|
||||
max-height: 0 !important;
|
||||
}
|
||||
|
||||
.dicom-uploader .table-header {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 28px;
|
||||
text-align: left;
|
||||
margin: 20px auto;
|
||||
}
|
||||
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { formatFileSize } from './utils/helpers';
|
||||
import CancellationToken from './utils/CancellationToken';
|
||||
import dicomUploader from './api/DicomUploadService';
|
||||
import './googleCloud.css';
|
||||
import './DicomUploader.css';
|
||||
|
||||
export default class DicomUploader extends Component {
|
||||
state = {
|
||||
@ -26,7 +26,7 @@ export default class DicomUploader extends Component {
|
||||
id: PropTypes.string,
|
||||
event: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
oidcKey: PropTypes.string,
|
||||
retrieveAuthHeaderFunction: PropTypes.func,
|
||||
};
|
||||
|
||||
filesLeft() {
|
||||
@ -94,11 +94,12 @@ export default class DicomUploader extends Component {
|
||||
const uploadCallback = (fileId, error) =>
|
||||
uploadContext === this.state.uploadContext &&
|
||||
this.uploadCallback.call(this, fileId, error);
|
||||
dicomUploader.setOidcStorageKey(this.props.oidcKey);
|
||||
|
||||
dicomUploader.setRetrieveAuthHeaderFunction(this.props.retrieveAuthHeaderFunction);
|
||||
|
||||
dicomUploader.smartUpload(
|
||||
files.target.files,
|
||||
this.props.url,
|
||||
this.props.oidcKey,
|
||||
uploadCallback,
|
||||
cancellationToken
|
||||
);
|
||||
@ -137,14 +138,13 @@ export default class DicomUploader extends Component {
|
||||
render() {
|
||||
if (this.state.files === null) {
|
||||
return (
|
||||
<div className="gcp-dicom-uploader">
|
||||
<div className="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"
|
||||
className="invisible-input"
|
||||
onChange={this.uploadFiles}
|
||||
type="file"
|
||||
id="file"
|
||||
@ -157,7 +157,7 @@ export default class DicomUploader extends Component {
|
||||
<img src="./assets/Button_Folder.svg" alt="upload folder"></img>
|
||||
</label>
|
||||
<input
|
||||
className="gcp-invisible-input"
|
||||
className="invisible-input"
|
||||
type="file"
|
||||
onChange={this.uploadFiles}
|
||||
id="folder"
|
||||
@ -174,7 +174,7 @@ export default class DicomUploader extends Component {
|
||||
<table id="tblProjectList" className="table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="gcp-picker--path">
|
||||
<th className="table-header">
|
||||
{this.percents()}% {this.filesLeft()}
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@ -1,23 +1,16 @@
|
||||
import { httpErrorToStr, getOidcToken, checkDicomFile } from '../utils/helpers';
|
||||
import { httpErrorToStr, checkDicomFile } from '../utils/helpers';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
class DicomUploadService {
|
||||
setOidcStorageKey(oidcStorageKey) {
|
||||
/* eslint-disable */
|
||||
if (!oidcStorageKey) console.error('OIDC storage key is empty');
|
||||
this.oidcStorageKey = oidcStorageKey;
|
||||
}
|
||||
|
||||
async smartUpload(files, url, authToken, uploadCallback, cancellationToken) {
|
||||
/* eslint-disable */
|
||||
async smartUpload(files, url, uploadCallback, cancellationToken) {
|
||||
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;
|
||||
throw new Error('No files were provided.');
|
||||
}
|
||||
|
||||
let parallelJobsCount = Math.min(filesArray.length, MAX_PARALLEL_JOBS);
|
||||
let completed = false;
|
||||
|
||||
@ -30,7 +23,7 @@ class DicomUploadService {
|
||||
try {
|
||||
if (chunk.length > 1) throw new Error('Not implemented');
|
||||
if (chunk.length === 1)
|
||||
await this.simpleUpload(chunk[0], url, authToken);
|
||||
await this.simpleUpload(chunk[0], url);
|
||||
} catch (err) {
|
||||
// It looks like a stupid bug of Babel that err is not an actual Exception object
|
||||
error = httpErrorToStr(err);
|
||||
@ -51,13 +44,13 @@ class DicomUploadService {
|
||||
});
|
||||
}
|
||||
|
||||
async simpleUpload(file, url, authToken) {
|
||||
/* eslint-disable */
|
||||
async simpleUpload(file, url) {
|
||||
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');
|
||||
throw new Error('This is not a valid DICOM file.');
|
||||
|
||||
await client.storeInstances({ datasets: [content] });
|
||||
}
|
||||
|
||||
@ -77,13 +70,17 @@ class DicomUploadService {
|
||||
});
|
||||
}
|
||||
|
||||
setRetrieveAuthHeaderFunction(func) {
|
||||
this.retrieveAuthHeaderFunc = func;
|
||||
}
|
||||
|
||||
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');
|
||||
const headers = this.retrieveAuthHeaderFunc();
|
||||
|
||||
// TODO: a bit weird we are creating a new dicomweb client instance for every upload
|
||||
return new api.DICOMwebClient({
|
||||
url,
|
||||
headers: { Authorization: 'Bearer ' + accessToken },
|
||||
headers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,13 +94,6 @@
|
||||
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;
|
||||
}
|
||||
@ -148,7 +141,3 @@
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.gcp-dicom-uploader .button {
|
||||
float: left;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
@ -26,11 +26,6 @@ export function httpErrorToStr(error) {
|
||||
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;
|
||||
|
||||
@ -24,7 +24,14 @@ function getFilters({ search }) {
|
||||
function StudyListRouting({ location }) {
|
||||
const filters = location ? getFilters(location) : undefined;
|
||||
|
||||
return <ConnectedStudyList filters={filters} />;
|
||||
let studyListFunctionsEnabled = false;
|
||||
if (window.config && window.config.studyListFunctionsEnabled) {
|
||||
studyListFunctionsEnabled = window.config.studyListFunctionsEnabled;
|
||||
}
|
||||
return <ConnectedStudyList
|
||||
filters={filters}
|
||||
studyListFunctionsEnabled={studyListFunctionsEnabled}
|
||||
/>;
|
||||
}
|
||||
|
||||
StudyListRouting.propTypes = {
|
||||
|
||||
@ -27,6 +27,11 @@ class StudyListWithData extends Component {
|
||||
server: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
history: PropTypes.object,
|
||||
studyListFunctionsEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
studyListFunctionsEnabled: true
|
||||
};
|
||||
|
||||
static rowsPerPage = 25;
|
||||
@ -147,7 +152,7 @@ class StudyListWithData extends Component {
|
||||
};
|
||||
|
||||
onImport = () => {
|
||||
//console.log('onImport');
|
||||
this.openModal('DicomFilesUploader')
|
||||
};
|
||||
|
||||
openModal = modalComponentId => {
|
||||
@ -176,9 +181,6 @@ class StudyListWithData extends Component {
|
||||
|
||||
render() {
|
||||
const onDrop = async acceptedFiles => {
|
||||
// Do something with the files
|
||||
console.warn(acceptedFiles);
|
||||
|
||||
try {
|
||||
const studies = await filesToStudies(acceptedFiles);
|
||||
|
||||
@ -200,16 +202,10 @@ class StudyListWithData extends Component {
|
||||
// TODO: This should probably be a prop
|
||||
if (window.config.enableGoogleCloudAdapter) {
|
||||
healthCareApiWindows = (
|
||||
<>
|
||||
<ConnectedDicomStorePicker
|
||||
isOpen={this.state.modalComponentId === 'DicomStorePicker'}
|
||||
onClose={this.closeModals}
|
||||
/>
|
||||
<ConnectedDicomFilesUploader
|
||||
isOpen={this.state.modalComponentId === 'DicomFilesUploader'}
|
||||
onClose={this.closeModals}
|
||||
/>
|
||||
</>
|
||||
<ConnectedDicomStorePicker
|
||||
isOpen={this.state.modalComponentId === 'DicomStorePicker'}
|
||||
onClose={this.closeModals}
|
||||
/>
|
||||
);
|
||||
|
||||
healthCareApiButtons = (
|
||||
@ -223,24 +219,16 @@ class StudyListWithData extends Component {
|
||||
>
|
||||
{this.props.t('Change DICOM Store')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => this.openModal('DicomFilesUploader')}
|
||||
>
|
||||
{this.props.t('Upload Studies')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
console.warn(this.state.studies);
|
||||
|
||||
const studyList = (
|
||||
<div className="paginationArea">
|
||||
{this.state.studies ? (
|
||||
<StudyList
|
||||
studies={this.state.studies}
|
||||
studyListFunctionsEnabled={false}
|
||||
studyListFunctionsEnabled={this.props.studyListFunctionsEnabled}
|
||||
onImport={this.onImport}
|
||||
onSelectItem={this.onSelectItem}
|
||||
pageSize={this.rowsPerPage}
|
||||
@ -250,6 +238,10 @@ class StudyListWithData extends Component {
|
||||
}
|
||||
onSearch={this.onSearch}
|
||||
>
|
||||
{this.props.studyListFunctionsEnabled ? <ConnectedDicomFilesUploader
|
||||
isOpen={this.state.modalComponentId === 'DicomFilesUploader'}
|
||||
onClose={this.closeModals}
|
||||
/> : null}
|
||||
{healthCareApiButtons}
|
||||
{healthCareApiWindows}
|
||||
</StudyList>
|
||||
|
||||
@ -12700,10 +12700,10 @@ react-transition-group@^4.1.1:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
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==
|
||||
react-viewerbase@0.17.6:
|
||||
version "0.17.6"
|
||||
resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.17.6.tgz#21dcc3a2a48174d235bda47aad97c339dcf3135a"
|
||||
integrity sha512-8kbXBbz1sIqlpLBWGjeSyua/axeAxNqy28DLPVVZlBuoTjDwyhdliuZhkDiQOHpv0POuYn3zxZdZcus7jAd5iQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "7.2.0"
|
||||
"@ohif/i18n" "0.2.1"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user