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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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