feat(DICOM Upload): Added new DICOM upload dialogue launched from the worklist page (#3326)

* feat(DICOM Upload)
OHIF #3297
- DicomWebDataSource.store.dicom now accepts an ArrayBuffer of data sets to store
- DicomWebDataSource.store.dicom now also accepts optional callbacks to track upload and an AbortSignal object to cancel upload
- Added DicomFileUploader class that performs and tracks the upload of a DICOM file
- Added various UI pieces for the upload: DicomUpload, DicomUploadProgress, DicomUploadProgressItem
- ProgressLoadingBar was extracted from LoadingIndicatorProgress so it can be reused
- Modal dialogues can now optionally prevent an outside click from closing the Modal

* Passing an XMLHttpRequest to the dataSource.store.dicom method instead of callbacks and AbortSignal.
Cleanup of various UI pieces to minimize code and increase readability.

* Made the DicomUpload component a customization module exported by the cornerstone extension.

* Exposed a copy of the data source configuration via the IWebApiDataSource interface.
Added dicomUploadEnabled to a data source's configuration.

* Code clean up.

* Upgraded the dicomweb-client version to one that provides the ability to
pass a custom HTTP request. DICOM upload uses that custom HTTP request
to track progress and cancel requests.

* Distinguished between failed and cancelled uploads.

* Allow no selection in the upload dialogue.
Fixed the styling of various progress information so that everything aligns.

* Switched from cornerstone wado image loader to cornerstone dicom image loader for DICOM upload.

* Added special cancelled icon to differentiate from failed.

* Added a bit of spacing between the upload progress bar and percentage.

* Fixed minor issue with upload rejection.

* Performance improvement for cancel all uploads:
- use React memo for each upload item progress (row)
- do not await each request of a cancel all

* Fixed various padding/spacing for the DICOM upload drop zone component.
Changed the border dashing for the DICOM upload drop zone to be a background image gradient.
Added hover and active effects to the 'Cancel All Uploads' text.
This commit is contained in:
Joe Boccanfuso authored and GitHub committed 2023-04-28 16:13:47 -04:00
1 parent f377153b60
commit 66f6e3eade
32 files changed
+1126 -350

No files matched your search

@@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M16 7.863a8.117 8.117 0 0 1-8 8.136 7.882 7.882 0 0 1-8-7.86A8.117 8.117 0 0 1 8 .002a7.883 7.883 0 0 1 8 7.862z" fill="#B70D11"/>
<g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M7.827 12.439a.313.313 0 0 0-.174.05c-.045.033-.07.076-.067.12.005.09.117.163.253.163h0c.066 0 .129-.02.174-.051.046-.032.07-.075.067-.12-.004-.088-.11-.16-.244-.162h-.005M7.836 8.667V4"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 601 B

@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M24 11.794c.017 6.667-5.333 12.108-12 12.205a11.823 11.823 0 0 1-12-11.79C-.019 5.541 5.331.1 12 .001a11.824 11.824 0 0 1 12 11.793z" fill="#B70D11"/>
<g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M11.494 17.158a.245.245 0 0 0-.241.255.254.254 0 0 0 .253.245h0a.246.246 0 0 0 .241-.255.253.253 0 0 0-.244-.245h-.005M11.503 13V6"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 566 B

@@ -0,0 +1,6 @@
<svg width="20" height="16" viewBox="0 0 20 16" xmlns="http://www.w3.org/2000/svg">
<g stroke="#5ACCE6" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="m1 6.467 2.222 3.31 2.556-3.06M19 9.898l-2.22-3.311-2.558 3.061"/>
<path d="M16.75 6.617a6.876 6.876 0 0 1-5.192 7.758A6.773 6.773 0 0 1 5.234 12.6M3.226 9.758a7.06 7.06 0 0 1 5.213-8.575 6.773 6.773 0 0 1 6.638 2.107"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 450 B

@@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(1 1)" stroke="#348CFD" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="8"/>
<path d="M8 3.273v9.454M4 7.273l4-4 4 4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 320 B

@@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g stroke="#5ACCE6" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 7.863a8.117 8.117 0 0 1-8 8.136 7.882 7.882 0 0 1-8-7.86A8.117 8.117 0 0 1 8 .002a7.883 7.883 0 0 1 8 7.862z" stroke-width="1.5"/>
<g stroke-width="2">
<path d="M7.827 12.439a.313.313 0 0 0-.174.05c-.045.033-.07.076-.067.12.005.09.117.163.253.163h0c.066 0 .129-.02.174-.051.046-.032.07-.075.067-.12-.004-.088-.11-.16-.244-.162h-.005M7.836 8.667V4"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 608 B

@@ -58,6 +58,8 @@ const variants = {
contained: {
default: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
primary: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
primaryDark:
'text-primary-active hover:opacity-80 active:opacity-100 focus:opacity-80',
secondary:
'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
white: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
@@ -93,6 +95,7 @@ const defaultVariantBackGroundColor = {
contained: {
default: 'bg-primary-light',
primary: 'bg-primary-main',
primaryDark: 'bg-primary-dark',
secondary: 'bg-secondary-light',
white: 'bg-white',
black: 'bg-black',
@@ -231,6 +234,7 @@ Button.propTypes = {
color: PropTypes.oneOf([
'default',
'primary',
'primaryDark',
'primaryActive',
'secondary',
'white',
@@ -50,6 +50,8 @@ import tracked from './../../assets/icons/tracked.svg';
import unlink from './../../assets/icons/unlink.svg';
import checkboxChecked from './../../assets/icons/checkbox-checked.svg';
import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg';
import iconAlertOutline from './../../assets/icons/icons-alert-outline.svg';
import iconAlertSmall from './../../assets/icons/icon-alert-small.svg';
import iconClose from './../../assets/icons/icon-close.svg';
import iconNextInactive from './../../assets/icons/icon-next-inactive.svg';
import iconNext from './../../assets/icons/icon-next.svg';
@@ -57,6 +59,9 @@ import iconPlay from './../../assets/icons/icon-play.svg';
import iconPause from './../../assets/icons/icon-pause.svg';
import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg';
import iconPrev from './../../assets/icons/icon-prev.svg';
import iconStatusAlert from './../../assets/icons/icon-status-alert.svg';
import iconTransferring from './../../assets/icons/icon-transferring.svg';
import iconUpload from './../../assets/icons/icon-upload.svg';
import navigationPanelRightHide from './../../assets/icons/navigation-panel-right-hide.svg';
import navigationPanelRightReveal from './../../assets/icons/navigation-panel-right-reveal.svg';
import tabLinear from './../../assets/icons/tab-linear.svg';
@@ -144,9 +149,13 @@ const ICONS = {
'external-link': externalLink,
'group-layers': groupLayers,
info: info,
'icon-alert-outline': iconAlertOutline,
'icon-alert-small': iconAlertSmall,
'icon-close': iconClose,
'icon-play': iconPlay,
'icon-pause': iconPause,
'icon-status-alert': iconStatusAlert,
'icon-transferring': iconTransferring,
'info-action': infoAction,
'info-link': infoLink,
'arrow-left': arrowLeft,
@@ -230,6 +239,7 @@ const ICONS = {
'icon-next': iconNext,
'icon-prev-inactive': iconPrevInactive,
'icon-prev': iconPrev,
'icon-upload': iconUpload,
'navigation-panel-right-hide': navigationPanelRightHide,
'navigation-panel-right-reveal': navigationPanelRightReveal,
'tab-linear': tabLinear,
@@ -1,8 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Icon } from '@ohif/ui';
import './LoadingIndicatorProgress.css';
import ProgressLoadingBar from '../ProgressLoadingBar';
/**
* A React component that renders a loading indicator.
@@ -19,18 +18,8 @@ function LoadingIndicatorProgress({ className, textBlock, progress }) {
)}
>
<Icon name="loading-ohif-mark" className="text-white w-12 h-12" />
<div className="loading">
{progress === undefined || progress === null ? (
<div className="infinite-loading-bar bg-primary-light"></div>
) : (
<div
className="bg-primary-light"
style={{
width: `${progress}%`,
height: '8px',
}}
></div>
)}
<div className="w-48">
<ProgressLoadingBar></ProgressLoadingBar>
</div>
{textBlock}
</div>
@@ -18,6 +18,7 @@ const Modal = ({
title,
onClose,
children,
shouldCloseOnOverlayClick,
}) => {
const { hide } = useModal();
@@ -56,6 +57,7 @@ const Modal = ({
onRequestClose={handleClose}
isOpen={isOpen}
title={title}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
>
{renderHeader()}
<section className="ohif-scrollbar modal-content overflow-y-auto px-4 py-6 rounded-bl rounded-br bg-primary-dark">
@@ -67,6 +69,7 @@ const Modal = ({
Modal.defaultProps = {
shouldCloseOnEsc: true,
shouldCloseOnOverlayClick: true,
};
Modal.propTypes = {
@@ -80,6 +83,7 @@ Modal.propTypes = {
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
shouldCloseOnOverlayClick: PropTypes.bool,
};
export default Modal;
@@ -4,7 +4,7 @@
border-radius: 4px;
overflow: hidden;
position: relative;
width: 12em;
width: 100%;
}
.infinite-loading-bar {
@@ -15,14 +15,6 @@
width: 50%;
}
.loading-bar {
animation: side2side 2s ease-in-out infinite;
height: 100%;
border-radius: 4px;
position: absolute;
width: 50%;
}
@keyframes side2side {
0%,
100% {
@@ -0,0 +1,34 @@
import React, { ReactElement } from 'react';
import './ProgressLoadingBar.css';
export type ProgressLoadingBarProps = {
progress?: number;
};
/**
* A React component that renders a loading progress bar.
* If progress is not provided, it will render an infinite loading bar
* If progress is provided, it will render a progress bar
* The progress text can be optionally displayed to the left of the bar.
*/
function ProgressLoadingBar({
progress,
}: ProgressLoadingBarProps): ReactElement {
return (
<div className="loading">
{progress === undefined || progress === null ? (
<div className="infinite-loading-bar bg-primary-light"></div>
) : (
<div
className="bg-primary-light"
style={{
width: `${progress}%`,
height: '8px',
}}
></div>
)}
</div>
);
}
export default ProgressLoadingBar;
@@ -0,0 +1,2 @@
import ProgressLoadingBar from './ProgressLoadingBar';
export default ProgressLoadingBar;
@@ -11,6 +11,7 @@ const StudyListFilter = ({
clearFilters,
isFiltering,
numOfStudies,
onUploadClick,
}) => {
const { t } = useTranslation('StudyList');
const { sortBy, sortDirection } = filterValues;
@@ -33,6 +34,15 @@ const StudyListFilter = ({
<Typography variant="h4" className="mr-6 text-primary-light">
{t('StudyList')}
</Typography>
{onUploadClick && (
<div
className="flex items-center gap-2 cursor-pointer text-primary-active text-lg self-center font-semibold"
onClick={onUploadClick}
>
<Icon name="icon-upload"></Icon>
<span>Upload</span>
</div>
)}
</div>
<div className="flex flex-row">
{isFiltering && (
@@ -119,6 +129,7 @@ StudyListFilter.propTypes = {
onChange: PropTypes.func.isRequired,
clearFilters: PropTypes.func.isRequired,
isFiltering: PropTypes.bool.isRequired,
onUploadClick: PropTypes.func,
};
export default StudyListFilter;
+2
View File
@@ -70,6 +70,7 @@ import CheckBox from './CheckBox';
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent';
import ViewportActionBar from './ViewportActionBar';
import ProgressLoadingBar from './ProgressLoadingBar';
export {
AboutModal,
@@ -110,6 +111,7 @@ export {
Modal,
NavBar,
Notification,
ProgressLoadingBar,
Select,
SegmentationTable,
SegmentationGroupTable,
@@ -32,6 +32,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
content: null,
contentProps: null,
shouldCloseOnEsc: true,
shouldCloseOnOverlayClick: true,
isOpen: true,
closeButton: true,
title: null,
@@ -39,7 +40,6 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
};
const { t } = useTranslation('Modals');
const [options, setOptions] = useState(DEFAULT_OPTIONS);
/**
@@ -57,9 +57,9 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
*
* @returns void
*/
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [
DEFAULT_OPTIONS,
]);
const hide = useCallback(() => {
setOptions(DEFAULT_OPTIONS);
}, [DEFAULT_OPTIONS]);
/**
* Sets the implementation of a modal service that can be used by extensions.
@@ -80,6 +80,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
customClassName,
shouldCloseOnEsc,
closeButton,
shouldCloseOnOverlayClick,
} = options;
return (
@@ -92,6 +93,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
title={t(title)}
closeButton={closeButton}
onClose={hide}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
>
<ModalContent {...contentProps} show={show} hide={hide} />
</Modal>
+1
View File
@@ -70,6 +70,7 @@ export {
Modal,
NavBar,
Notification,
ProgressLoadingBar,
Select,
SegmentationTable,
SegmentationGroupTable,