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:
Erik Ziegler 2019-07-26 15:04:28 +02:00 committed by GitHub
parent 903c41d26f
commit da318c9c8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 85 additions and 105 deletions

View File

@ -108,7 +108,7 @@
"react-router": "^5.0.1", "react-router": "^5.0.1",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.0.1",
"react-transition-group": "^4.1.1", "react-transition-group": "^4.1.1",
"react-viewerbase": "0.17.0", "react-viewerbase": "0.17.6",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-oidc": "3.1.x", "redux-oidc": "3.1.x",

View File

@ -21,31 +21,7 @@ window.config = {
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
automaticSilentRenew: true, automaticSilentRenew: true,
revokeAccessTokenOnSignout: 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,
} }

View File

@ -21,4 +21,5 @@ window.config = {
}, },
], ],
}, },
studyListFunctionsEnabled: true,
} }

View File

@ -1,15 +1,18 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import DicomFileUploaderModal from './DicomFileUploaderModal.js'; import DicomFileUploaderModal from './DicomFileUploaderModal.js';
import OHIF from 'ohif-core';
const isActive = a => a.active === true; const isActive = a => a.active === true;
const mapStateToProps = state => { const mapStateToProps = state => {
const activeServer = state.servers.servers.find(isActive); 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 { return {
oidcStorageKey, retrieveAuthHeaderFunction,
url: activeServer && activeServer.qidoRoot, url: activeServer && activeServer.qidoRoot,
}; };
}; };

View File

@ -7,7 +7,7 @@ import { withTranslation } from 'react-i18next';
class DicomFileUploaderModal extends Component { class DicomFileUploaderModal extends Component {
static propTypes = { static propTypes = {
url: PropTypes.string, url: PropTypes.string,
oidcStorageKey: PropTypes.string.isRequired, retrieveAuthHeaderFunction: PropTypes.func,
onClose: PropTypes.func, onClose: PropTypes.func,
}; };
@ -32,13 +32,13 @@ class DicomFileUploaderModal extends Component {
> >
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
{this.props.t('Google Cloud Healthcare API - DICOM Upload')} {this.props.t('Upload DICOM Files')}
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<DicomUploader <DicomUploader
url={this.props.url} url={this.props.url}
oidcKey={this.props.oidcStorageKey} retrieveAuthHeaderFunction={this.props.retrieveAuthHeaderFunction}
/> />
</Modal.Body> </Modal.Body>
</Modal> </Modal>

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

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { formatFileSize } from './utils/helpers'; import { formatFileSize } from './utils/helpers';
import CancellationToken from './utils/CancellationToken'; import CancellationToken from './utils/CancellationToken';
import dicomUploader from './api/DicomUploadService'; import dicomUploader from './api/DicomUploadService';
import './googleCloud.css'; import './DicomUploader.css';
export default class DicomUploader extends Component { export default class DicomUploader extends Component {
state = { state = {
@ -26,7 +26,7 @@ export default class DicomUploader extends Component {
id: PropTypes.string, id: PropTypes.string,
event: PropTypes.string, event: PropTypes.string,
url: PropTypes.string, url: PropTypes.string,
oidcKey: PropTypes.string, retrieveAuthHeaderFunction: PropTypes.func,
}; };
filesLeft() { filesLeft() {
@ -94,11 +94,12 @@ export default class DicomUploader extends Component {
const uploadCallback = (fileId, error) => const uploadCallback = (fileId, error) =>
uploadContext === this.state.uploadContext && uploadContext === this.state.uploadContext &&
this.uploadCallback.call(this, fileId, error); this.uploadCallback.call(this, fileId, error);
dicomUploader.setOidcStorageKey(this.props.oidcKey);
dicomUploader.setRetrieveAuthHeaderFunction(this.props.retrieveAuthHeaderFunction);
dicomUploader.smartUpload( dicomUploader.smartUpload(
files.target.files, files.target.files,
this.props.url, this.props.url,
this.props.oidcKey,
uploadCallback, uploadCallback,
cancellationToken cancellationToken
); );
@ -137,14 +138,13 @@ export default class DicomUploader extends Component {
render() { render() {
if (this.state.files === null) { if (this.state.files === null) {
return ( return (
<div className="gcp-dicom-uploader"> <div className="dicom-uploader">
<div className="button"> <div className="button">
<label htmlFor="file"> <label htmlFor="file">
<img src="./assets/Button_File.svg" alt="upload file"></img> <img src="./assets/Button_File.svg" alt="upload file"></img>
</label> </label>
<input <input
className="gcp-invisible-input" className="invisible-input"
accept=".dcm"
onChange={this.uploadFiles} onChange={this.uploadFiles}
type="file" type="file"
id="file" id="file"
@ -157,7 +157,7 @@ export default class DicomUploader extends Component {
<img src="./assets/Button_Folder.svg" alt="upload folder"></img> <img src="./assets/Button_Folder.svg" alt="upload folder"></img>
</label> </label>
<input <input
className="gcp-invisible-input" className="invisible-input"
type="file" type="file"
onChange={this.uploadFiles} onChange={this.uploadFiles}
id="folder" id="folder"
@ -174,7 +174,7 @@ export default class DicomUploader extends Component {
<table id="tblProjectList" className="table noselect"> <table id="tblProjectList" className="table noselect">
<thead> <thead>
<tr> <tr>
<th className="gcp-picker--path"> <th className="table-header">
{this.percents()}% {this.filesLeft()} {this.percents()}% {this.filesLeft()}
</th> </th>
</tr> </tr>

View File

@ -1,23 +1,16 @@
import { httpErrorToStr, getOidcToken, checkDicomFile } from '../utils/helpers'; import { httpErrorToStr, checkDicomFile } from '../utils/helpers';
import { api } from 'dicomweb-client'; import { api } from 'dicomweb-client';
class DicomUploadService { class DicomUploadService {
setOidcStorageKey(oidcStorageKey) { async smartUpload(files, url, uploadCallback, cancellationToken) {
/* 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 CHUNK_SIZE = 1; // Only one file per request is supported so far
const MAX_PARALLEL_JOBS = 50; // FIXME: tune MAX_PARALLEL_JOBS number const MAX_PARALLEL_JOBS = 50; // FIXME: tune MAX_PARALLEL_JOBS number
//
let filesArray = Array.from(files); let filesArray = Array.from(files);
if (filesArray.length === 0) { if (filesArray.length === 0) {
console.warn('No files are supplied for uploading'); throw new Error('No files were provided.');
return;
} }
let parallelJobsCount = Math.min(filesArray.length, MAX_PARALLEL_JOBS); let parallelJobsCount = Math.min(filesArray.length, MAX_PARALLEL_JOBS);
let completed = false; let completed = false;
@ -30,7 +23,7 @@ class DicomUploadService {
try { try {
if (chunk.length > 1) throw new Error('Not implemented'); if (chunk.length > 1) throw new Error('Not implemented');
if (chunk.length === 1) if (chunk.length === 1)
await this.simpleUpload(chunk[0], url, authToken); await this.simpleUpload(chunk[0], url);
} catch (err) { } catch (err) {
// It looks like a stupid bug of Babel that err is not an actual Exception object // It looks like a stupid bug of Babel that err is not an actual Exception object
error = httpErrorToStr(err); error = httpErrorToStr(err);
@ -51,13 +44,13 @@ class DicomUploadService {
}); });
} }
async simpleUpload(file, url, authToken) { async simpleUpload(file, url) {
/* eslint-disable */
const client = this.getClient(url); const client = this.getClient(url);
const loadedFile = await this.readFile(file); const loadedFile = await this.readFile(file);
const content = loadedFile.content; const content = loadedFile.content;
if (!checkDicomFile(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] }); await client.storeInstances({ datasets: [content] });
} }
@ -77,13 +70,17 @@ class DicomUploadService {
}); });
} }
setRetrieveAuthHeaderFunction(func) {
this.retrieveAuthHeaderFunc = func;
}
getClient(url) { getClient(url) {
if (!this.oidcStorageKey) throw new Error('OIDC storage key is not set'); const headers = this.retrieveAuthHeaderFunc();
const accessToken = getOidcToken(this.oidcStorageKey);
if (!accessToken) throw new Error('OIDC access_token is not set'); // TODO: a bit weird we are creating a new dicomweb client instance for every upload
return new api.DICOMwebClient({ return new api.DICOMwebClient({
url, url,
headers: { Authorization: 'Bearer ' + accessToken }, headers
}); });
} }
} }

View File

@ -94,13 +94,6 @@
background-color: #000000; background-color: #000000;
} }
.gcp-invisible-input {
position: absolute;
display: none;
z-index: -1000;
max-width: 0 !important;
max-height: 0 !important;
}
.gcp-hidden { .gcp-hidden {
display: none; display: none;
} }
@ -148,7 +141,3 @@
margin: 20px auto; margin: 20px auto;
} }
.gcp-dicom-uploader .button {
float: left;
margin: 5px;
}

View File

@ -26,11 +26,6 @@ export function httpErrorToStr(error) {
return error.message || 'Unknown 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 */ /* eslint-disable */
export function checkDicomFile(arrayBuffer) { export function checkDicomFile(arrayBuffer) {
if (arrayBuffer.length <= 132) return false; if (arrayBuffer.length <= 132) return false;

View File

@ -24,7 +24,14 @@ function getFilters({ search }) {
function StudyListRouting({ location }) { function StudyListRouting({ location }) {
const filters = location ? getFilters(location) : undefined; 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 = { StudyListRouting.propTypes = {

View File

@ -27,6 +27,11 @@ class StudyListWithData extends Component {
server: PropTypes.object, server: PropTypes.object,
user: PropTypes.object, user: PropTypes.object,
history: PropTypes.object, history: PropTypes.object,
studyListFunctionsEnabled: PropTypes.bool
};
static defaultProps = {
studyListFunctionsEnabled: true
}; };
static rowsPerPage = 25; static rowsPerPage = 25;
@ -147,7 +152,7 @@ class StudyListWithData extends Component {
}; };
onImport = () => { onImport = () => {
//console.log('onImport'); this.openModal('DicomFilesUploader')
}; };
openModal = modalComponentId => { openModal = modalComponentId => {
@ -176,9 +181,6 @@ class StudyListWithData extends Component {
render() { render() {
const onDrop = async acceptedFiles => { const onDrop = async acceptedFiles => {
// Do something with the files
console.warn(acceptedFiles);
try { try {
const studies = await filesToStudies(acceptedFiles); const studies = await filesToStudies(acceptedFiles);
@ -200,16 +202,10 @@ class StudyListWithData extends Component {
// TODO: This should probably be a prop // TODO: This should probably be a prop
if (window.config.enableGoogleCloudAdapter) { if (window.config.enableGoogleCloudAdapter) {
healthCareApiWindows = ( healthCareApiWindows = (
<>
<ConnectedDicomStorePicker <ConnectedDicomStorePicker
isOpen={this.state.modalComponentId === 'DicomStorePicker'} isOpen={this.state.modalComponentId === 'DicomStorePicker'}
onClose={this.closeModals} onClose={this.closeModals}
/> />
<ConnectedDicomFilesUploader
isOpen={this.state.modalComponentId === 'DicomFilesUploader'}
onClose={this.closeModals}
/>
</>
); );
healthCareApiButtons = ( healthCareApiButtons = (
@ -223,24 +219,16 @@ class StudyListWithData extends Component {
> >
{this.props.t('Change DICOM Store')} {this.props.t('Change DICOM Store')}
</button> </button>
<button
className="btn btn-primary"
onClick={() => this.openModal('DicomFilesUploader')}
>
{this.props.t('Upload Studies')}
</button>
</div> </div>
); );
} }
console.warn(this.state.studies);
const studyList = ( const studyList = (
<div className="paginationArea"> <div className="paginationArea">
{this.state.studies ? ( {this.state.studies ? (
<StudyList <StudyList
studies={this.state.studies} studies={this.state.studies}
studyListFunctionsEnabled={false} studyListFunctionsEnabled={this.props.studyListFunctionsEnabled}
onImport={this.onImport} onImport={this.onImport}
onSelectItem={this.onSelectItem} onSelectItem={this.onSelectItem}
pageSize={this.rowsPerPage} pageSize={this.rowsPerPage}
@ -250,6 +238,10 @@ class StudyListWithData extends Component {
} }
onSearch={this.onSearch} onSearch={this.onSearch}
> >
{this.props.studyListFunctionsEnabled ? <ConnectedDicomFilesUploader
isOpen={this.state.modalComponentId === 'DicomFilesUploader'}
onClose={this.closeModals}
/> : null}
{healthCareApiButtons} {healthCareApiButtons}
{healthCareApiWindows} {healthCareApiWindows}
</StudyList> </StudyList>

View File

@ -12700,10 +12700,10 @@ react-transition-group@^4.1.1:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" prop-types "^15.6.2"
react-viewerbase@0.17.0: react-viewerbase@0.17.6:
version "0.17.0" version "0.17.6"
resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.17.0.tgz#e741f94b24b6cef419fdd32b9bcfcdcfe81783dc" resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.17.6.tgz#21dcc3a2a48174d235bda47aad97c339dcf3135a"
integrity sha512-fy51pHrdhdXcPg6Pb1l8S5fzxlenFVhRUe80ZFq1aK9omrQXgaBF0uhm537WREyx2CS0/ZOD/wQnnbyjqEvYdQ== integrity sha512-8kbXBbz1sIqlpLBWGjeSyua/axeAxNqy28DLPVVZlBuoTjDwyhdliuZhkDiQOHpv0POuYn3zxZdZcus7jAd5iQ==
dependencies: dependencies:
"@babel/runtime" "7.2.0" "@babel/runtime" "7.2.0"
"@ohif/i18n" "0.2.1" "@ohif/i18n" "0.2.1"