Chore/kill yalc with fire (#629)
* init * more shifting * Move @babel dependencies up to workspace root * Begin updating dependent libraries to use sync'd webpack builds w/ watches * Commit changes before a long weekend * Split packages into platform and extensions * Consolidate how/where we specify file/module type entrypoints * Make it possible to pass in extensions as App props * Resolve viewer's modules * Target for UMD bundle * Add Webpack Stylus loader * Provide WebPack build options for microscopy, vtk, and ui * Better entrypoint for extensions * Push changes up to switch PCs * Fix ESM symlink build for Viewers * Shift docs to root * Specify additional lerna config props * Begin providing basic readme info * Rename example extension folder * Shift links to bottom of doc * First attempt at a netlify deploy preview * Attempt to fix deploy preview build * push workspace enabled to initial command; remove second yarn install; use npx to call gitbook cli commands * Try to use global gitbook-cli * Export node_modules path * More agressive with modifying PATH * Follow deploy log output advice * try again to set bin path * Try swapping bash with a new shell * Try bash -l instead of exec bash * Annd let's try again * and again * We may have figured it out johnny, boy * We need to pull cornerstone-wado-image-loader files from hoisted node_modules * Attempt to fix ticks/escapes * Change copy syntax; try running tests on viewers from root for circleci * Lower version to match circleci image * Try alternative jest-canvas-mock location and version file syntax * Run version command instead of calling node directly * Try alternative jest transform * Set path and enable workspaces * Add codecov flags * Use cpx so our copying finishes? * Get all platform unit tests to run * Run and report individually and in parallel * Try once relying on codecov yaml to split w/ flags * Add projects to split by flags * Generate separate reports * Trigger codecov after everything has finished running; these may not support flags * Use recommended flags from issue comments for codecov * Fix typo * Remove clear flag * Shift back to calling codecov from root * Generate example for codecov issue * Try fixing paths * Simplify * Try using codecov orb * Bump circleci version * Upload core and viewer * Use full string paths * Remove individual codecov calls * Fix dir * Combine files before upload * Escape anchors * tryfix syntax * Escape string literal * Combine lines to reduce path * long paths * Use home alias * Fix filename * Try to see the contents of our cat'd file * Also upload `core` * Try running with aliased folder * Fix path; split PR and Merge unit tests into separate jobs * Set default threshold * Fix typo * Changing to scoped package names * Adding note * Catch more updates * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.0 - @ohif/extension-dicom-html@0.0.4-alpha.0 - @ohif/extension-dicom-microscopy@0.0.9-alpha.0 - @ohif/extension-dicom-pdf@0.0.8-alpha.0 - @ohif/extension-vtk@0.1.4-alpha.0 - @ohif/core@0.11.1-alpha.0 - @ohif/i18n@0.2.3-alpha.0 - @ohif/ui@0.2.18-alpha.0 - @ohif/viewer@0.0.22-alpha.0 * Clean up primary readme * Support for dev and dev:* commands * Shift build command; satisfy default PWA build * Remove old scripts * Clean up UI to set webpack scripts * Tidy up project links * Clean duplicate code in UI project * Update UI project's docs * Clean up ui and i18n config * Reduce duplicate code in `@ohif/core` * Fix @ohif/ui versions * bump @ohif/core versions * Redux testkit dep * Remove unused/duplicate config files for projects/packages * ohif-core --> @ohif/core * Safer publish command * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.1 - @ohif/extension-dicom-html@0.0.4-alpha.1 - @ohif/extension-dicom-microscopy@0.0.9-alpha.1 - @ohif/extension-dicom-pdf@0.0.8-alpha.1 - @ohif/extension-vtk@0.1.4-alpha.1 - @ohif/core@0.11.1-alpha.1 - @ohif/i18n@0.2.3-alpha.1 - @ohif/ui@0.2.18-alpha.1 - @ohif/viewer@0.0.22-alpha.1 * test version bump * Add global prettier config * chore(release): publish %s [skip ci] - @ohif/viewer@0.0.22-alpha.2 * test lerna publish * Update package.jsons * Fix build scripts * catch changes * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.2 - @ohif/extension-dicom-html@0.0.4-alpha.2 - @ohif/extension-dicom-microscopy@0.0.9-alpha.2 - @ohif/extension-dicom-pdf@0.0.8-alpha.2 - @ohif/extension-vtk@0.1.4-alpha.2 - @ohif/core@0.11.1-alpha.2 - @ohif/i18n@0.2.3-alpha.2 - @ohif/ui@0.2.18-alpha.2 - @ohif/viewer@0.0.22-alpha.3 * fix eslint issues caused by eslint 6 and our react-app config * Publish with canary tag * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.3 - @ohif/extension-dicom-html@0.0.4-alpha.3 - @ohif/extension-dicom-microscopy@0.0.9-alpha.3 - @ohif/extension-dicom-pdf@0.0.8-alpha.3 - @ohif/extension-vtk@0.1.4-alpha.3 - @ohif/core@0.11.1-alpha.3 - @ohif/i18n@0.2.3-alpha.3 - @ohif/ui@0.2.18-alpha.3 - @ohif/viewer@0.0.22-alpha.4 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.4 - @ohif/extension-dicom-html@0.0.4-alpha.4 - @ohif/extension-dicom-microscopy@0.0.9-alpha.4 - @ohif/extension-dicom-pdf@0.0.8-alpha.4 - @ohif/extension-vtk@0.1.4-alpha.4 - @ohif/core@0.11.1-alpha.4 - @ohif/i18n@0.2.3-alpha.4 - @ohif/ui@0.2.18-alpha.4 - @ohif/viewer@0.0.22-alpha.5 * Simplify build * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.5 - @ohif/extension-dicom-html@0.0.4-alpha.5 - @ohif/extension-dicom-microscopy@0.0.9-alpha.5 - @ohif/extension-dicom-pdf@0.0.8-alpha.5 - @ohif/extension-vtk@0.1.4-alpha.5 - @ohif/core@0.11.1-alpha.5 - @ohif/i18n@0.2.3-alpha.5 - @ohif/ui@0.2.18-alpha.5 - @ohif/viewer@0.0.22-alpha.6 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.6 - @ohif/extension-dicom-html@0.0.4-alpha.6 - @ohif/extension-dicom-microscopy@0.0.9-alpha.6 - @ohif/extension-dicom-pdf@0.0.8-alpha.6 - @ohif/extension-vtk@0.1.4-alpha.6 - @ohif/core@0.11.1-alpha.6 - @ohif/i18n@0.2.3-alpha.6 - @ohif/ui@0.2.18-alpha.6 - @ohif/viewer@0.0.22-alpha.7 * Split version and publish * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.7 - @ohif/extension-dicom-html@0.0.4-alpha.7 - @ohif/extension-dicom-microscopy@0.0.9-alpha.7 - @ohif/extension-dicom-pdf@0.0.8-alpha.7 - @ohif/extension-vtk@0.1.4-alpha.7 - @ohif/core@0.11.1-alpha.7 - @ohif/i18n@0.2.3-alpha.7 - @ohif/ui@0.2.18-alpha.7 - @ohif/viewer@0.0.22-alpha.8 * Try simplify netlify build * react-viewerbase --> @ohif/ui * add test for stylus loader * Try new way to specify publish path * Use an output folder that won't be ignored * Try move command * fiddle with paths * Dig for some information * Make sure we're in root for our commands * Remove discovery * Setup script-tag example * Fix typo * Move config folder * Update cornerstone extension to use shared/root webpack configs * capture changes * Update extension webpack configs * Don't check in docz output * Fix babel runtime resolution issue * Simplify ui build steps * Push in progress * Shift directories * Get viewer dev path to work * Fix viewer commonjs build * Fix build pwa config for viewers * Make sure our package specific command can be called from root * Update notes * Fix imports and misc. merge notes * Bump wado-image-loader version * Switch up commands to remove webworkers copy * Fix CI config * Fix more broken package references * Add react-dropzone * Persist so e2e test can use * Fix lockfile * Use cypress orb to simplify CI * Fix config * Try again * Install yarn latest for orb * Install yarn a different way * Wait on correct port * Run tests in viewer project * Fix cypress config * Make Cypress happier - use default location * Simplify e2e testing * Capture UI changes since fork * Catch core up * Grab latest deps after merge * Fix broken import * Make sure cornerstone-wado-image-loader resolves to 3+ * Note regarding orbs * Fix refresh on 404 routes * Fix odd error for cornerstone-wado-image-loader * Different target for UMD * Simplify dev script * Try to get fancy with netlify output * Let's do it again * Try to fix measurementTools issue * See if we can get our lerna magic to work on merge to monorepo branch
This commit is contained in:
1 parent
cac911f08e
commit
08ff214ea6
919 files changed
+45652
-52097
No files matched your search
@@ -0,0 +1,26 @@
|
||||
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);
|
||||
|
||||
// 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 {
|
||||
retrieveAuthHeaderFunction,
|
||||
url: activeServer && activeServer.qidoRoot,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedDicomFileUploader = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(DicomFileUploaderModal);
|
||||
|
||||
export default ConnectedDicomFileUploader;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { connect } from 'react-redux';
|
||||
import DicomStorePickerModal from './DicomStorePickerModal.js';
|
||||
|
||||
const isActive = a => a.active === true;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeServer = state.servers.servers.find(isActive);
|
||||
|
||||
return {
|
||||
user: state.oidc && state.oidc.user,
|
||||
url: activeServer && activeServer.qidoRoot,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setServers: servers => {
|
||||
const action = {
|
||||
type: 'SET_SERVERS',
|
||||
servers,
|
||||
};
|
||||
dispatch(action);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedDicomStorePicker = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DicomStorePickerModal);
|
||||
|
||||
export default ConnectedDicomStorePicker;
|
||||
@@ -0,0 +1,55 @@
|
||||
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 {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
project: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
|
||||
const response = await api.loadDatasets(
|
||||
this.props.project.projectId,
|
||||
this.props.location.locationId
|
||||
);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
datasets: response.data.datasets || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { datasets, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<DatasetsList
|
||||
datasets={datasets}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import DicomStorePicker from './DicomStorePicker';
|
||||
import DatasetPicker from './DatasetPicker';
|
||||
import ProjectPicker from './ProjectPicker';
|
||||
import LocationPicker from './LocationPicker';
|
||||
import GoogleCloudApi from './api/GoogleCloudApi';
|
||||
import './googleCloud.css';
|
||||
|
||||
class DatasetSelector extends Component {
|
||||
state = {
|
||||
project: null,
|
||||
location: null,
|
||||
dataset: null,
|
||||
unloading: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
event: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
canClose: PropTypes.string,
|
||||
setServers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onProjectSelect = project => {
|
||||
this.setState({
|
||||
project,
|
||||
});
|
||||
};
|
||||
|
||||
onLocationSelect = location => {
|
||||
this.setState({
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
onDatasetSelect = dataset => {
|
||||
this.setState({
|
||||
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 accessToken = this.props.user.access_token;
|
||||
|
||||
const { project, location, dataset } = this.state;
|
||||
const {
|
||||
onProjectClick,
|
||||
onLocationClick,
|
||||
onDatasetClick,
|
||||
onProjectSelect,
|
||||
onLocationSelect,
|
||||
onDatasetSelect,
|
||||
onDicomStoreSelect,
|
||||
} = this;
|
||||
|
||||
let projectBreadcrumbs = (
|
||||
<div className="gcp-picker--path">
|
||||
<span>{this.props.t('Select a Project')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (project) {
|
||||
projectBreadcrumbs = (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectBreadcrumbs}
|
||||
{!project && (
|
||||
<ProjectPicker
|
||||
accessToken={accessToken}
|
||||
onSelect={onProjectSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
{project && !location && (
|
||||
<LocationPicker
|
||||
accessToken={accessToken}
|
||||
project={project}
|
||||
onSelect={onLocationSelect}
|
||||
/>
|
||||
)}
|
||||
{project && location && !dataset && (
|
||||
<DatasetPicker
|
||||
accessToken={accessToken}
|
||||
project={project}
|
||||
location={location}
|
||||
onSelect={onDatasetSelect}
|
||||
/>
|
||||
)}
|
||||
{project && location && dataset && (
|
||||
<DicomStorePicker
|
||||
accessToken={accessToken}
|
||||
dataset={dataset}
|
||||
onSelect={onDicomStoreSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DatasetSelector);
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class DatasetsList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
datasets: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = dataset => {
|
||||
return (
|
||||
<tr
|
||||
key={dataset.name}
|
||||
className={
|
||||
this.state.highlightedItem === dataset.name
|
||||
? 'noselect active'
|
||||
: '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() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="DatasetList">
|
||||
{this.props.datasets.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblDatasetList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Dataset')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.datasets && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DatasetsList);
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import DicomUploader from './DicomUploader';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class DicomFileUploaderModal extends Component {
|
||||
static propTypes = {
|
||||
url: PropTypes.string,
|
||||
retrieveAuthHeaderFunction: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
uploaded: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onClose}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="modal fade themed in"
|
||||
backdrop={false}
|
||||
size={'md'}
|
||||
keyboard={true}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{this.props.t('Upload DICOM Files')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DicomUploader
|
||||
url={this.props.url}
|
||||
retrieveAuthHeaderFunction={this.props.retrieveAuthHeaderFunction}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomFileUploaderModal);
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class DicomStoreList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
stores: PropTypes.array,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = store => {
|
||||
return (
|
||||
<tr
|
||||
key={store.name}
|
||||
className={
|
||||
this.state.highlightedItem === store.name
|
||||
? 'noselect active'
|
||||
: '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() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="StoreList">{this.props.stores.map(this.renderTableRow)}</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblStoreList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('DICOM Store')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.stores && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomStoreList);
|
||||
@@ -0,0 +1,53 @@
|
||||
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 {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
stores: [],
|
||||
locations: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dataset: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
|
||||
const response = await api.loadDicomStores(this.props.dataset.name);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
stores: response.data.dicomStores || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stores, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
|
||||
return (
|
||||
<DicomStoreList
|
||||
stores={stores}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import DatasetSelector from './DatasetSelector';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class DicomStorePickerModal extends Component {
|
||||
static propTypes = {
|
||||
url: PropTypes.string,
|
||||
user: PropTypes.object.isRequired,
|
||||
setServers: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
handleEvent = data => {
|
||||
const servers = [
|
||||
{
|
||||
name: data.dicomStore,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
qidoSupportsIncludeField: false,
|
||||
type: 'dicomWeb',
|
||||
qidoRoot: data.qidoRoot,
|
||||
wadoRoot: data.wadoRoot,
|
||||
wadoUriRoot: data.wadoUriRoot,
|
||||
active: true,
|
||||
supportsFuzzyMatching: false,
|
||||
},
|
||||
];
|
||||
|
||||
this.props.setServers(servers);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onClose}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="modal fade themed in"
|
||||
backdrop={false}
|
||||
size={'md'}
|
||||
keyboard={true}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{this.props.t('Google Cloud Healthcare API')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DatasetSelector
|
||||
setServers={this.handleEvent}
|
||||
user={this.props.user}
|
||||
url={this.props.url}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomStorePickerModal);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
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 './DicomUploader.css';
|
||||
|
||||
export default class DicomUploader extends Component {
|
||||
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 distinguish between different downloads
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
event: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
retrieveAuthHeaderFunction: PropTypes.func,
|
||||
};
|
||||
|
||||
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.setRetrieveAuthHeaderFunction(this.props.retrieveAuthHeaderFunction);
|
||||
|
||||
dicomUploader.smartUpload(
|
||||
files.target.files,
|
||||
this.props.url,
|
||||
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 });
|
||||
} 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 });
|
||||
}
|
||||
|
||||
renderTableRow = file => {
|
||||
let error = null;
|
||||
if (file.error !== null) {
|
||||
error = <p style={{ color: 'red' }}>{file.error}</p>;
|
||||
}
|
||||
return (
|
||||
<tr key={file.id}>
|
||||
<td className="project">
|
||||
{file.name} {error}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.files === null) {
|
||||
return (
|
||||
<div className="dicom-uploader">
|
||||
<div className="button">
|
||||
<label htmlFor="file">
|
||||
<img src="./assets/Button_File.svg" alt="upload file"></img>
|
||||
</label>
|
||||
<input
|
||||
className="invisible-input"
|
||||
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="invisible-input"
|
||||
type="file"
|
||||
onChange={this.uploadFiles}
|
||||
id="folder"
|
||||
webkitdirectory="true"
|
||||
mozdirectory="true"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table id="tblProjectList" className="table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="table-header">
|
||||
{this.percents()}% {this.filesLeft()}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ProjectList">
|
||||
{this.state.uploadedList.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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 {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
locations: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
project: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
|
||||
const response = await api.loadLocations(this.props.project.projectId);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
locations: response.data.locations || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { locations, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<LocationsList
|
||||
locations={locations}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class LocationsList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
locations: PropTypes.array,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = location => {
|
||||
return (
|
||||
<tr
|
||||
key={location.locationId}
|
||||
className={
|
||||
this.state.highlightedItem === location.locationId
|
||||
? 'noselect active'
|
||||
: '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() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="LocationList">
|
||||
{this.props.locations.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblLocationList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Location')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.locations && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(LocationsList);
|
||||
@@ -0,0 +1,49 @@
|
||||
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 {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
projects: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
const response = await api.loadProjects();
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
projects: response.data.projects || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { projects, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<ProjectsList
|
||||
projects={projects}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class ProjectsList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
highlightedItem: null,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
projects: PropTypes.array,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = project => {
|
||||
return (
|
||||
<tr
|
||||
key={project.projectId}
|
||||
className={
|
||||
this.state.highlightedItem === project.projectId
|
||||
? 'noselect active'
|
||||
: 'noselect'
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
this.onHighlightItem(project.projectId);
|
||||
}}
|
||||
onClick={() => {
|
||||
this.onHighlightItem(project.projectId);
|
||||
this.props.onSelect(project);
|
||||
}}
|
||||
>
|
||||
<td>{project.name}</td>
|
||||
<td>{project.projectId}</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
onHighlightItem(project) {
|
||||
this.setState({ highlightedItem: project });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="ProjectList">
|
||||
{this.props.projects.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblProjectList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Project')}</th>
|
||||
<th>{this.props.t('ID')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.projects && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(ProjectsList);
|
||||
@@ -0,0 +1,88 @@
|
||||
import { httpErrorToStr, checkDicomFile } from '../utils/helpers';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
class DicomUploadService {
|
||||
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) {
|
||||
throw new Error('No files were provided.');
|
||||
}
|
||||
|
||||
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);
|
||||
} 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) {
|
||||
const client = this.getClient(url);
|
||||
const loadedFile = await this.readFile(file);
|
||||
const content = loadedFile.content;
|
||||
if (!checkDicomFile(content))
|
||||
throw new Error('This is not a valid DICOM file.');
|
||||
|
||||
await client.storeInstances({ datasets: [content] });
|
||||
}
|
||||
|
||||
readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const 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);
|
||||
});
|
||||
}
|
||||
|
||||
setRetrieveAuthHeaderFunction(func) {
|
||||
this.retrieveAuthHeaderFunc = func;
|
||||
}
|
||||
|
||||
getClient(url) {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new DicomUploadService();
|
||||
@@ -0,0 +1,94 @@
|
||||
class GoogleCloudApi {
|
||||
setAccessToken(accessToken) {
|
||||
if (!accessToken) console.error('Access token is empty');
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
get fetchConfig() {
|
||||
if (!this.accessToken) throw new Error('OIDC access_token is not set');
|
||||
return {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.accessToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get urlBase() {
|
||||
return `https://healthcare.googleapis.com/v1beta1`;
|
||||
}
|
||||
|
||||
get urlBaseProject() {
|
||||
return this.urlBase + `/projects`;
|
||||
}
|
||||
|
||||
async doRequest(urlStr, config = {}, params = {}) {
|
||||
const 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 (let key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
data[key] = data[key].concat(subPage.data[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
isError: false,
|
||||
status: response.status,
|
||||
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();
|
||||
@@ -0,0 +1,143 @@
|
||||
.loading-icon-spin {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 30px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.gcp-table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.gcp-table > tr {
|
||||
height: 20px;
|
||||
}
|
||||
.gcp-table > thead {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gcp-table > thead tr th {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid var(--ui-border-color-active);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-top: 0;
|
||||
}
|
||||
.gcp-table > tbody tr {
|
||||
padding: 5px;
|
||||
background-color: var(--ui-gray-darker);
|
||||
}
|
||||
.gcp-table > tbody tr:nth-child(even) {
|
||||
background-color: var(--ui-gray-dark);
|
||||
}
|
||||
|
||||
.gcp-table > tbody tr td {
|
||||
padding: 8px;
|
||||
height: $body-cell-height;
|
||||
line-height: $body-cell-height;
|
||||
color: var(--table-text-primary-color);
|
||||
font-weight: 300;
|
||||
border-top: 1px solid var(--ui-gray-lighter);
|
||||
border-bottom: 1px solid var(--ui-gray-lighter);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gcp-table > tbody tr td.emptyCell {
|
||||
color: #516873;
|
||||
}
|
||||
.gcp-table > tbody tr td.emptyValue {
|
||||
color: var(--ui-gray-light);
|
||||
}
|
||||
.gcp-table > tbody tr:hover,
|
||||
.gcp-table > tbody tr:active,
|
||||
.gcp-table > tbody tr.active {
|
||||
background-color: var(--table-hover-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
.gcp-table > tbody tr:hover td,
|
||||
.gcp-table > tbody tr:active td,
|
||||
.gcp-table > tbody tr.active td {
|
||||
border-top: 1px solid var(--ui-gray-lighter);
|
||||
border-bottom: 1px solid var(--ui-gray-lighter);
|
||||
background-color: var(--table-hover-color);
|
||||
}
|
||||
|
||||
.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-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gcp-picker {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 650px;
|
||||
height: 600px;
|
||||
background-color: #161a1f;
|
||||
padding: 20px 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gcp-dicom-picker__exit {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
filter: opacity(0.5);
|
||||
}
|
||||
|
||||
.gcp-dicom-picker__exit:hover {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
.gcp-picker--btn {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.gcp-picker--title {
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.gcp-picker--path {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 28px;
|
||||
text-align: left;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export default class CancellationToken {
|
||||
constructor() {
|
||||
this.cancelled = false;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.cancelled = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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.';
|
||||
}
|
||||
|
||||
/* 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]);
|
||||
}
|
||||
Reference in new issue
Block a user