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:
parent
f377153b60
commit
66f6e3eade
@ -0,0 +1,6 @@
|
||||
.dicom-upload-drop-area-border-dash {
|
||||
background-image: repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%);
|
||||
background-position: left top, left bottom, left top, right top;
|
||||
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
|
||||
background-size: 20px 3px, 20px 3px, 3px 20px, 3px 20px;
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ReactElement } from 'react';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import DicomFileUploader from '../../utils/DicomFileUploader';
|
||||
import DicomUploadProgress from './DicomUploadProgress';
|
||||
import { Button } from '@ohif/ui';
|
||||
import './DicomUpload.css';
|
||||
|
||||
type DicomUploadProps = {
|
||||
dataSource;
|
||||
onComplete: () => void;
|
||||
onStarted: () => void;
|
||||
};
|
||||
|
||||
function DicomUpload({
|
||||
dataSource,
|
||||
onComplete,
|
||||
onStarted,
|
||||
}: DicomUploadProps): ReactElement {
|
||||
const baseClassNames = 'min-h-[520px] flex flex-col bg-black select-none';
|
||||
const [dicomFileUploaderArr, setDicomFileUploaderArr] = useState([]);
|
||||
|
||||
const onDrop = useCallback(async acceptedFiles => {
|
||||
onStarted();
|
||||
setDicomFileUploaderArr(
|
||||
acceptedFiles.map(file => new DicomFileUploader(file, dataSource))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getDropZoneComponent = (): ReactElement => {
|
||||
return (
|
||||
<Dropzone
|
||||
onDrop={acceptedFiles => {
|
||||
onDrop(acceptedFiles);
|
||||
}}
|
||||
noClick
|
||||
>
|
||||
{({ getRootProps }) => (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="m-5 dicom-upload-drop-area-border-dash flex flex-col items-center justify-center h-full"
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<Dropzone onDrop={onDrop} noDrag>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div {...getRootProps()}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={false}
|
||||
onClick={() => {}}
|
||||
>
|
||||
{'Add files'}
|
||||
<input {...getInputProps()} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
<Dropzone onDrop={onDrop} noDrag>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div {...getRootProps()}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primaryDark"
|
||||
border="primaryActive"
|
||||
disabled={false}
|
||||
onClick={() => {}}
|
||||
>
|
||||
{'Add folder'}
|
||||
<input
|
||||
{...getInputProps()}
|
||||
webkitdirectory="true"
|
||||
mozdirectory="true"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</div>
|
||||
<div className="pt-5">or drag images or folders here</div>
|
||||
<div className="pt-3 text-aqua-pale text-lg">
|
||||
(DICOM files supported)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{dicomFileUploaderArr.length ? (
|
||||
<div className={classNames('h-[calc(100vh-300px)]', baseClassNames)}>
|
||||
<DicomUploadProgress
|
||||
dicomFileUploaderArr={Array.from(dicomFileUploaderArr)}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classNames('h-[520px]', baseClassNames)}>
|
||||
{getDropZoneComponent()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
DicomUpload.propTypes = {
|
||||
dataSource: PropTypes.object.isRequired,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
onStarted: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DicomUpload;
|
||||
@ -0,0 +1,436 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon, ProgressLoadingBar } from '@ohif/ui';
|
||||
import DicomFileUploader, {
|
||||
EVENTS,
|
||||
UploadStatus,
|
||||
DicomFileUploaderProgressEvent,
|
||||
UploadRejection,
|
||||
} from '../../utils/DicomFileUploader';
|
||||
import DicomUploadProgressItem from './DicomUploadProgressItem';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type DicomUploadProgressProps = {
|
||||
dicomFileUploaderArr: DicomFileUploader[];
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
const ONE_SECOND = 1000;
|
||||
const ONE_MINUTE = ONE_SECOND * 60;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
|
||||
// The base/initial interval time length used to calculate the
|
||||
// rate of the upload and in turn estimate the
|
||||
// the amount of time remaining for the upload. This is the length
|
||||
// of the very first interval to get a reasonable estimate on screen in
|
||||
// a reasonable amount of time. The length of each interval after the first
|
||||
// is based on the upload rate calculated. Faster rates use this base interval
|
||||
// length. Slower rates below UPLOAD_RATE_THRESHOLD get longer interval times
|
||||
// to obtain more accurate upload rates.
|
||||
const BASE_INTERVAL_TIME = 15000;
|
||||
|
||||
// The upload rate threshold to determine the length of the interval to
|
||||
// calculate the upload rate.
|
||||
const UPLOAD_RATE_THRESHOLD = 75;
|
||||
|
||||
const NO_WRAP_ELLIPSIS_CLASS_NAMES =
|
||||
'text-ellipsis whitespace-nowrap overflow-hidden';
|
||||
|
||||
function DicomUploadProgress({
|
||||
dicomFileUploaderArr,
|
||||
onComplete,
|
||||
}: DicomUploadProgressProps): ReactElement {
|
||||
const [totalUploadSize] = useState(
|
||||
dicomFileUploaderArr.reduce(
|
||||
(acc, fileUploader) => acc + fileUploader.getFileSize(),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
const currentUploadSizeRef = useRef<number>(0);
|
||||
|
||||
const uploadRateRef = useRef(0);
|
||||
|
||||
const [timeRemaining, setTimeRemaining] = useState<number>(null);
|
||||
|
||||
const [percentComplete, setPercentComplete] = useState(0);
|
||||
|
||||
const [numFilesCompleted, setNumFilesCompleted] = useState(0);
|
||||
|
||||
const [numFails, setNumFails] = useState(0);
|
||||
|
||||
const [showFailedOnly, setShowFailedOnly] = useState(false);
|
||||
|
||||
const progressBarContainerRef = useRef<HTMLElement>();
|
||||
|
||||
/**
|
||||
* The effect for measuring and setting the current upload rate. This is
|
||||
* done by measuring the amount of data uploaded in a set interval time.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
// The amount of data already uploaded at the start of the interval.
|
||||
let intervalStartUploadSize = 0;
|
||||
|
||||
// The starting time of the interval.
|
||||
let intervalStartTime = Date.now();
|
||||
|
||||
const setUploadRateRef = () => {
|
||||
const uploadSizeFromStartOfInterval =
|
||||
currentUploadSizeRef.current - intervalStartUploadSize;
|
||||
|
||||
const now = Date.now();
|
||||
const timeSinceStartOfInterval = now - intervalStartTime;
|
||||
|
||||
// Calculate and set the upload rate (ref)
|
||||
uploadRateRef.current =
|
||||
uploadSizeFromStartOfInterval / timeSinceStartOfInterval;
|
||||
|
||||
// Reset the interval starting values.
|
||||
intervalStartUploadSize = currentUploadSizeRef.current;
|
||||
intervalStartTime = now;
|
||||
|
||||
// Only start a new interval if there is more to upload.
|
||||
if (totalUploadSize - currentUploadSizeRef.current > 0) {
|
||||
if (uploadRateRef.current >= UPLOAD_RATE_THRESHOLD) {
|
||||
timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME);
|
||||
} else {
|
||||
// The current upload rate is relatively slow, so use a larger
|
||||
// time interval to get a better upload rate estimate.
|
||||
timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME * 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The very first interval is just the base time interval length.
|
||||
timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* The effect for: updating the overall percentage complete; setting the
|
||||
* estimated time remaining; updating the number of files uploaded; and
|
||||
* detecting if any error has occurred.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let currentTimeRemaining = null;
|
||||
|
||||
// For each uploader, listen for the progress percentage complete and
|
||||
// add promise catch/finally callbacks to detect errors and count number
|
||||
// of uploads complete.
|
||||
const subscriptions = dicomFileUploaderArr.map(fileUploader => {
|
||||
let currentFileUploadSize = 0;
|
||||
|
||||
const updateProgress = (percentComplete: number) => {
|
||||
const previousFileUploadSize = currentFileUploadSize;
|
||||
|
||||
currentFileUploadSize = Math.round(
|
||||
(percentComplete / 100) * fileUploader.getFileSize()
|
||||
);
|
||||
|
||||
currentUploadSizeRef.current = Math.min(
|
||||
totalUploadSize,
|
||||
currentUploadSizeRef.current -
|
||||
previousFileUploadSize +
|
||||
currentFileUploadSize
|
||||
);
|
||||
|
||||
setPercentComplete(
|
||||
(currentUploadSizeRef.current / totalUploadSize) * 100
|
||||
);
|
||||
|
||||
if (uploadRateRef.current !== 0) {
|
||||
const uploadSizeRemaining =
|
||||
totalUploadSize - currentUploadSizeRef.current;
|
||||
|
||||
const timeRemaining = Math.round(
|
||||
uploadSizeRemaining / uploadRateRef.current
|
||||
);
|
||||
|
||||
if (currentTimeRemaining === null) {
|
||||
currentTimeRemaining = timeRemaining;
|
||||
setTimeRemaining(currentTimeRemaining);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not show an increase in the time remaining by two seconds or minutes
|
||||
// so as to prevent jumping the time remaining up and down constantly
|
||||
// due to rounding, inaccuracies in the estimate and slight variations
|
||||
// in upload rates over time.
|
||||
if (timeRemaining < ONE_MINUTE) {
|
||||
const currentSecondsRemaining = Math.ceil(
|
||||
currentTimeRemaining / ONE_SECOND
|
||||
);
|
||||
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
|
||||
const delta = secondsRemaining - currentSecondsRemaining;
|
||||
if (delta < 0 || delta > 2) {
|
||||
currentTimeRemaining = timeRemaining;
|
||||
setTimeRemaining(currentTimeRemaining);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeRemaining < ONE_HOUR) {
|
||||
const currentMinutesRemaining = Math.ceil(
|
||||
currentTimeRemaining / ONE_MINUTE
|
||||
);
|
||||
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
|
||||
const delta = minutesRemaining - currentMinutesRemaining;
|
||||
if (delta < 0 || delta > 2) {
|
||||
currentTimeRemaining = timeRemaining;
|
||||
setTimeRemaining(currentTimeRemaining);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Hours remaining...
|
||||
currentTimeRemaining = timeRemaining;
|
||||
setTimeRemaining(currentTimeRemaining);
|
||||
}
|
||||
};
|
||||
|
||||
const progressCallback = (
|
||||
progressEvent: DicomFileUploaderProgressEvent
|
||||
) => {
|
||||
updateProgress(progressEvent.percentComplete);
|
||||
};
|
||||
|
||||
// Use the uploader promise to flag any error and count the number of
|
||||
// uploads completed.
|
||||
fileUploader
|
||||
.load()
|
||||
.catch((rejection: UploadRejection) => {
|
||||
if (rejection.status === UploadStatus.Failed) {
|
||||
setNumFails(numFails => numFails + 1);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
// If any error occurred, the percent complete progress stops firing
|
||||
// but this call to updateProgress nicely puts all finished uploads at 100%.
|
||||
updateProgress(100);
|
||||
setNumFilesCompleted(numCompleted => numCompleted + 1);
|
||||
});
|
||||
|
||||
return fileUploader.subscribe(EVENTS.PROGRESS, progressCallback);
|
||||
});
|
||||
return () => {
|
||||
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const cancelAllUploads = useCallback(async () => {
|
||||
for (const dicomFileUploader of dicomFileUploaderArr) {
|
||||
// Important: we need a non-blocking way to cancel every upload,
|
||||
// otherwise the UI will freeze and the user will not be able
|
||||
// to interact with the app and progress will not be updated.
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
dicomFileUploader.cancel();
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getFormattedTimeRemaining = useCallback((): string => {
|
||||
if (timeRemaining == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (timeRemaining < ONE_MINUTE) {
|
||||
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
|
||||
return `${secondsRemaining} ${
|
||||
secondsRemaining === 1 ? 'second' : 'seconds'
|
||||
}`;
|
||||
}
|
||||
|
||||
if (timeRemaining < ONE_HOUR) {
|
||||
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
|
||||
return `${minutesRemaining} ${
|
||||
minutesRemaining === 1 ? 'minute' : 'minutes'
|
||||
}`;
|
||||
}
|
||||
|
||||
const hoursRemaining = Math.ceil(timeRemaining / ONE_HOUR);
|
||||
return `${hoursRemaining} ${hoursRemaining === 1 ? 'hour' : 'hours'}`;
|
||||
}, [timeRemaining]);
|
||||
|
||||
const getPercentCompleteRounded = useCallback(
|
||||
() => Math.min(100, Math.round(percentComplete)),
|
||||
[percentComplete]
|
||||
);
|
||||
|
||||
/**
|
||||
* Determines if the progress bar should show the infinite animation or not.
|
||||
* Show the infinite animation for progress less than 1% AND if less than
|
||||
* one pixel of the progress bar would be displayed.
|
||||
*/
|
||||
const showInfiniteProgressBar = useCallback((): boolean => {
|
||||
return (
|
||||
getPercentCompleteRounded() < 1 &&
|
||||
(progressBarContainerRef?.current?.offsetWidth ?? 0) *
|
||||
(percentComplete / 100) <
|
||||
1
|
||||
);
|
||||
}, [getPercentCompleteRounded, percentComplete]);
|
||||
|
||||
/**
|
||||
* Gets the css style for the 'n of m' (files completed) text. The only css attribute
|
||||
* of the style is width such that the 'n of m' is always a fixed width and thus
|
||||
* as each file completes uploading the text on screen does not constantly shift
|
||||
* left and right.
|
||||
*/
|
||||
const getNofMFilesStyle = useCallback(() => {
|
||||
// the number of digits accounts for the digits being on each side of the ' of '
|
||||
const numDigits = 2 * dicomFileUploaderArr.length.toString().length;
|
||||
// the number of digits + 2 spaces and 2 characters for ' of '
|
||||
const numChars = numDigits + 4;
|
||||
return { width: `${numChars}ch` };
|
||||
}, []);
|
||||
|
||||
const getNumCompletedAndTimeRemainingComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="text-lg px-1 pb-4 h-14 flex bg-primary-dark items-center">
|
||||
{numFilesCompleted === dicomFileUploaderArr.length ? (
|
||||
<>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${
|
||||
dicomFileUploaderArr.length
|
||||
} ${
|
||||
dicomFileUploaderArr.length > 1 ? 'files' : 'file'
|
||||
} completed.`}</span>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={false}
|
||||
className="ml-auto"
|
||||
onClick={onComplete}
|
||||
>
|
||||
{'Open Viewer'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
style={getNofMFilesStyle()}
|
||||
className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES, 'text-end')}
|
||||
>
|
||||
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}
|
||||
</span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
|
||||
{' files completed.'}
|
||||
</span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
|
||||
{timeRemaining
|
||||
? `Less than ${getFormattedTimeRemaining()} remaining. `
|
||||
: ''}
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
NO_WRAP_ELLIPSIS_CLASS_NAMES,
|
||||
'cursor-pointer text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto'
|
||||
)}
|
||||
onClick={cancelAllUploads}
|
||||
>
|
||||
Cancel All Uploads
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getShowFailedOnlyIconComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="ml-auto flex justify-center w-6">
|
||||
{numFails > 0 && (
|
||||
<div
|
||||
onClick={() =>
|
||||
setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)
|
||||
}
|
||||
>
|
||||
<Icon className="cursor-pointer" name="icon-status-alert"></Icon>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getPercentCompleteComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="overflow-y-scroll ohif-scrollbar px-2 border-b border-secondary-light">
|
||||
<div className="flex w-full p-2.5 items-center min-h-14">
|
||||
{numFilesCompleted === dicomFileUploaderArr.length ? (
|
||||
<>
|
||||
<div className="text-xl text-primary-light">
|
||||
{numFails > 0
|
||||
? `Completed with ${numFails} ${
|
||||
numFails > 1 ? 'errors' : 'error'
|
||||
}!`
|
||||
: 'Completed!'}
|
||||
</div>
|
||||
{getShowFailedOnlyIconComponent()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div ref={progressBarContainerRef} className="flex-grow">
|
||||
<ProgressLoadingBar
|
||||
progress={
|
||||
showInfiniteProgressBar()
|
||||
? undefined
|
||||
: Math.min(100, percentComplete)
|
||||
}
|
||||
></ProgressLoadingBar>
|
||||
</div>
|
||||
<div className="w-24 ml-1 flex items-center">
|
||||
<div className="w-10 text-right">{`${getPercentCompleteRounded()}%`}</div>
|
||||
{getShowFailedOnlyIconComponent()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col grow">
|
||||
{getNumCompletedAndTimeRemainingComponent()}
|
||||
<div className="flex flex-col bg-black text-lg overflow-hidden grow">
|
||||
{getPercentCompleteComponent()}
|
||||
<div className="overflow-y-scroll ohif-scrollbar px-2 grow h-1">
|
||||
{dicomFileUploaderArr
|
||||
.filter(
|
||||
dicomFileUploader =>
|
||||
!showFailedOnly ||
|
||||
dicomFileUploader.getStatus() === UploadStatus.Failed
|
||||
)
|
||||
.map(dicomFileUploader => (
|
||||
<DicomUploadProgressItem
|
||||
key={dicomFileUploader.getFileId()}
|
||||
dicomFileUploader={dicomFileUploader}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DicomUploadProgress.propTypes = {
|
||||
dicomFileUploaderArr: PropTypes.arrayOf(
|
||||
PropTypes.instanceOf(DicomFileUploader)
|
||||
).isRequired,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DicomUploadProgress;
|
||||
@ -0,0 +1,117 @@
|
||||
import React, {
|
||||
ReactElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DicomFileUploader, {
|
||||
DicomFileUploaderProgressEvent,
|
||||
EVENTS,
|
||||
UploadRejection,
|
||||
UploadStatus,
|
||||
} from '../../utils/DicomFileUploader';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
type DicomUploadProgressItemProps = {
|
||||
dicomFileUploader: DicomFileUploader;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const DicomUploadProgressItem = memo(
|
||||
({ dicomFileUploader }: DicomUploadProgressItemProps): ReactElement => {
|
||||
const [percentComplete, setPercentComplete] = useState(
|
||||
dicomFileUploader.getPercentComplete()
|
||||
);
|
||||
const [failedReason, setFailedReason] = useState('');
|
||||
const [status, setStatus] = useState(dicomFileUploader.getStatus());
|
||||
|
||||
console.info(`${dicomFileUploader.getFileId()}`);
|
||||
const isComplete = useCallback(() => {
|
||||
return (
|
||||
status === UploadStatus.Failed ||
|
||||
status === UploadStatus.Cancelled ||
|
||||
status === UploadStatus.Success
|
||||
);
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
const progressSubscription = dicomFileUploader.subscribe(
|
||||
EVENTS.PROGRESS,
|
||||
(dicomFileUploaderProgressEvent: DicomFileUploaderProgressEvent) => {
|
||||
setPercentComplete(dicomFileUploaderProgressEvent.percentComplete);
|
||||
}
|
||||
);
|
||||
|
||||
dicomFileUploader
|
||||
.load()
|
||||
.catch((reason: UploadRejection) => {
|
||||
setStatus(reason.status);
|
||||
setFailedReason(reason.message ?? '');
|
||||
})
|
||||
.finally(() => setStatus(dicomFileUploader.getStatus()));
|
||||
|
||||
return () => progressSubscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const cancelUpload = useCallback(() => {
|
||||
dicomFileUploader.cancel();
|
||||
}, []);
|
||||
|
||||
const getStatusIcon = (): ReactElement => {
|
||||
switch (dicomFileUploader.getStatus()) {
|
||||
case UploadStatus.Success:
|
||||
return (
|
||||
<Icon name="status-tracked" className="text-primary-light"></Icon>
|
||||
);
|
||||
case UploadStatus.InProgress:
|
||||
return <Icon name="icon-transferring"></Icon>;
|
||||
case UploadStatus.Failed:
|
||||
return <Icon name="icon-alert-small"></Icon>;
|
||||
case UploadStatus.Cancelled:
|
||||
return <Icon name="icon-alert-outline"></Icon>;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full p-2.5 text-lg min-h-14 items-center border-b border-secondary-light overflow-hidden">
|
||||
<div className="flex flex-col gap-1 self-top w-0 grow shrink">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex w-6 justify-center items-center shrink-0">
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
{dicomFileUploader.getFileName()}
|
||||
</div>
|
||||
</div>
|
||||
{failedReason && <div className="pl-10">{failedReason}</div>}
|
||||
</div>
|
||||
<div className="w-24 flex items-center">
|
||||
{!isComplete() && (
|
||||
<>
|
||||
{dicomFileUploader.getStatus() === UploadStatus.InProgress && (
|
||||
<div className="w-10 text-right">{percentComplete}%</div>
|
||||
)}
|
||||
<div className="flex cursor-pointer ml-auto">
|
||||
<Icon
|
||||
className="w-6 h-6 self-center text-primary-active"
|
||||
name="close"
|
||||
onClick={cancelUpload}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DicomUploadProgressItem.propTypes = {
|
||||
dicomFileUploader: PropTypes.instanceOf(DicomFileUploader).isRequired,
|
||||
};
|
||||
|
||||
export default DicomUploadProgressItem;
|
||||
@ -1,5 +1,6 @@
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
import DicomUpload from './components/DicomUpload/DicomUpload';
|
||||
|
||||
const tools = {
|
||||
active: [
|
||||
@ -22,6 +23,13 @@ const tools = {
|
||||
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'cornerstoneDicomUploadComponent',
|
||||
value: {
|
||||
id: 'dicomUploadComponent',
|
||||
component: DicomUpload,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
|
||||
220
extensions/cornerstone/src/utils/DicomFileUploader.ts
Normal file
220
extensions/cornerstone/src/utils/DicomFileUploader.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
||||
|
||||
import { PubSubService } from '@ohif/core';
|
||||
|
||||
export const EVENTS = {
|
||||
PROGRESS: 'event:DicomFileUploader:progress',
|
||||
};
|
||||
|
||||
export interface DicomFileUploaderEvent {
|
||||
fileId: number;
|
||||
}
|
||||
|
||||
export interface DicomFileUploaderProgressEvent extends DicomFileUploaderEvent {
|
||||
percentComplete: number;
|
||||
}
|
||||
|
||||
export enum UploadStatus {
|
||||
NotStarted,
|
||||
InProgress,
|
||||
Success,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
type CancelOrFailed = UploadStatus.Cancelled | UploadStatus.Failed;
|
||||
|
||||
export class UploadRejection {
|
||||
message: string;
|
||||
status: CancelOrFailed;
|
||||
|
||||
constructor(status: CancelOrFailed, message: string) {
|
||||
this.message = message;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export default class DicomFileUploader extends PubSubService {
|
||||
private _file;
|
||||
private _fileId;
|
||||
private _dataSource;
|
||||
private _loadPromise;
|
||||
private _abortController = new AbortController();
|
||||
private _status: UploadStatus = UploadStatus.NotStarted;
|
||||
private _percentComplete = 0;
|
||||
|
||||
constructor(file, dataSource) {
|
||||
super(EVENTS);
|
||||
this._file = file;
|
||||
this._fileId = dicomImageLoader.wadouri.fileManager.add(file);
|
||||
this._dataSource = dataSource;
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this._fileId;
|
||||
}
|
||||
|
||||
getFileName(): string {
|
||||
return this._file.name;
|
||||
}
|
||||
|
||||
getFileSize(): number {
|
||||
return this._file.size;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this._abortController.abort();
|
||||
}
|
||||
|
||||
getStatus(): UploadStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
getPercentComplete(): number {
|
||||
return this._percentComplete;
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
if (this._loadPromise) {
|
||||
// Already started loading, return the load promise.
|
||||
return this._loadPromise;
|
||||
}
|
||||
|
||||
this._loadPromise = new Promise<void>((resolve, reject) => {
|
||||
// The upload listeners: fire progress events and/or settle the promise.
|
||||
const uploadCallbacks = {
|
||||
progress: evt => {
|
||||
if (!evt.lengthComputable) {
|
||||
// Progress computation is not possible.
|
||||
return;
|
||||
}
|
||||
|
||||
this._status = UploadStatus.InProgress;
|
||||
|
||||
this._percentComplete = Math.round((100 * evt.loaded) / evt.total);
|
||||
this._broadcastEvent(EVENTS.PROGRESS, {
|
||||
fileId: this._fileId,
|
||||
percentComplete: this._percentComplete,
|
||||
});
|
||||
},
|
||||
timeout: () => {
|
||||
this._reject(
|
||||
reject,
|
||||
new UploadRejection(UploadStatus.Failed, 'The request timed out.')
|
||||
);
|
||||
},
|
||||
abort: () => {
|
||||
this._reject(
|
||||
reject,
|
||||
new UploadRejection(UploadStatus.Cancelled, 'Cancelled')
|
||||
);
|
||||
},
|
||||
error: () => {
|
||||
this._reject(
|
||||
reject,
|
||||
new UploadRejection(UploadStatus.Failed, 'The request failed.')
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// First try to load the file.
|
||||
dicomImageLoader.wadouri
|
||||
.loadFileRequest(this._fileId)
|
||||
.then(dicomFile => {
|
||||
if (this._abortController.signal.aborted) {
|
||||
this._reject(
|
||||
reject,
|
||||
new UploadRejection(UploadStatus.Cancelled, 'Cancelled')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._checkDicomFile(dicomFile)) {
|
||||
// The file is not DICOM
|
||||
this._reject(
|
||||
reject,
|
||||
new UploadRejection(
|
||||
UploadStatus.Failed,
|
||||
'Not a valid DICOM file.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
this._addRequestCallbacks(request, uploadCallbacks);
|
||||
|
||||
// Do the actual upload by supplying the DICOM file and upload callbacks/listeners.
|
||||
return this._dataSource.store
|
||||
.dicom(dicomFile, request)
|
||||
.then(() => {
|
||||
this._status = UploadStatus.Success;
|
||||
resolve();
|
||||
})
|
||||
.catch(reason => {
|
||||
this._reject(reject, reason);
|
||||
});
|
||||
})
|
||||
.catch(reason => {
|
||||
this._reject(reject, reason);
|
||||
});
|
||||
});
|
||||
|
||||
return this._loadPromise;
|
||||
}
|
||||
|
||||
private _isRejected(): boolean {
|
||||
return (
|
||||
this._status === UploadStatus.Failed ||
|
||||
this._status === UploadStatus.Cancelled
|
||||
);
|
||||
}
|
||||
|
||||
private _reject(reject: (reason?: any) => void, reason: any) {
|
||||
if (this._isRejected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason instanceof UploadRejection) {
|
||||
this._status = reason.status;
|
||||
reject(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
this._status = UploadStatus.Failed;
|
||||
|
||||
if (reason.message) {
|
||||
reject(new UploadRejection(UploadStatus.Failed, reason.message));
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new UploadRejection(UploadStatus.Failed, reason));
|
||||
}
|
||||
|
||||
private _addRequestCallbacks(request: XMLHttpRequest, uploadCallbacks) {
|
||||
const abortCallback = () => request.abort();
|
||||
this._abortController.signal.addEventListener('abort', abortCallback);
|
||||
|
||||
for (const [eventName, callback] of Object.entries(uploadCallbacks)) {
|
||||
request.upload.addEventListener(eventName, callback);
|
||||
}
|
||||
|
||||
const cleanUpCallback = () => {
|
||||
this._abortController.signal.removeEventListener('abort', abortCallback);
|
||||
|
||||
for (const [eventName, callback] of Object.entries(uploadCallbacks)) {
|
||||
request.upload.removeEventListener(eventName, callback);
|
||||
}
|
||||
|
||||
request.removeEventListener('loadend', cleanUpCallback);
|
||||
};
|
||||
request.addEventListener('loadend', cleanUpCallback);
|
||||
}
|
||||
|
||||
private _checkDicomFile(arrayBuffer: 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]);
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/i18n": "^1.0.0",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicomweb-client": "^0.8.4",
|
||||
"dicomweb-client": "^0.10.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@ -61,6 +61,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
singlepart,
|
||||
} = dicomWebConfig;
|
||||
|
||||
const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig));
|
||||
|
||||
const qidoConfig = {
|
||||
url: qidoRoot,
|
||||
staticWado,
|
||||
@ -230,36 +232,47 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
},
|
||||
|
||||
store: {
|
||||
dicom: async dataset => {
|
||||
dicom: async (dataset, request) => {
|
||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||
if (headers) {
|
||||
wadoDicomWebClient.headers = headers;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
FileMetaInformationVersion:
|
||||
dataset._meta.FileMetaInformationVersion.Value,
|
||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
||||
ImplementationClassUID,
|
||||
ImplementationVersionName,
|
||||
};
|
||||
if (dataset instanceof ArrayBuffer) {
|
||||
const options = {
|
||||
datasets: [dataset],
|
||||
request,
|
||||
};
|
||||
|
||||
const denaturalized = denaturalizeDataset(meta);
|
||||
const dicomDict = new DicomDict(denaturalized);
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
} else {
|
||||
const meta = {
|
||||
FileMetaInformationVersion:
|
||||
dataset._meta.FileMetaInformationVersion.Value,
|
||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
||||
ImplementationClassUID,
|
||||
ImplementationVersionName,
|
||||
};
|
||||
|
||||
dicomDict.dict = denaturalizeDataset(dataset);
|
||||
const denaturalized = denaturalizeDataset(meta);
|
||||
const dicomDict = new DicomDict(denaturalized);
|
||||
|
||||
const part10Buffer = dicomDict.write();
|
||||
dicomDict.dict = denaturalizeDataset(dataset);
|
||||
|
||||
const options = {
|
||||
datasets: [part10Buffer],
|
||||
};
|
||||
const part10Buffer = dicomDict.write();
|
||||
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
const options = {
|
||||
datasets: [part10Buffer],
|
||||
request,
|
||||
};
|
||||
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
_retrieveSeriesMetadataSync: async (
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
@ -482,6 +495,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
});
|
||||
return imageIds;
|
||||
},
|
||||
getConfig() {
|
||||
return dicomWebConfigCopy;
|
||||
},
|
||||
};
|
||||
|
||||
if (supportsReject) {
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicomweb-client": "^0.8.4",
|
||||
"dicomweb-client": "^0.10.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.merge": "^4.6.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
|
||||
@ -21,6 +21,7 @@ function create({
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet,
|
||||
getImageIdsForInstance,
|
||||
getConfig,
|
||||
}) {
|
||||
const defaultQuery = {
|
||||
studies: {
|
||||
@ -57,6 +58,11 @@ function create({
|
||||
};
|
||||
|
||||
const defaultReject = {};
|
||||
|
||||
const defaultGetConfig = () => {
|
||||
return { dicomUploadEnabled: false };
|
||||
};
|
||||
|
||||
return {
|
||||
query: query || defaultQuery,
|
||||
retrieve: retrieve || defaultRetrieve,
|
||||
@ -66,6 +72,7 @@ function create({
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet,
|
||||
getImageIdsForInstance,
|
||||
getConfig: getConfig || defaultGetConfig,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
8
platform/ui/src/assets/icons/icon-alert-small.svg
Normal file
8
platform/ui/src/assets/icons/icon-alert-small.svg
Normal file
@ -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 |
8
platform/ui/src/assets/icons/icon-status-alert.svg
Normal file
8
platform/ui/src/assets/icons/icon-status-alert.svg
Normal file
@ -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 |
6
platform/ui/src/assets/icons/icon-transferring.svg
Normal file
6
platform/ui/src/assets/icons/icon-transferring.svg
Normal file
@ -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 |
6
platform/ui/src/assets/icons/icon-upload.svg
Normal file
6
platform/ui/src/assets/icons/icon-upload.svg
Normal file
@ -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 |
8
platform/ui/src/assets/icons/icons-alert-outline.svg
Normal file
8
platform/ui/src/assets/icons/icons-alert-outline.svg
Normal file
@ -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;
|
||||
2
platform/ui/src/components/ProgressLoadingBar/index.js
Normal file
2
platform/ui/src/components/ProgressLoadingBar/index.js
Normal file
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -70,6 +70,7 @@ export {
|
||||
Modal,
|
||||
NavBar,
|
||||
Notification,
|
||||
ProgressLoadingBar,
|
||||
Select,
|
||||
SegmentationTable,
|
||||
SegmentationGroupTable,
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
showStudyList: true,
|
||||
extensions: [],
|
||||
modes: [],
|
||||
@ -26,6 +30,7 @@ window.config = {
|
||||
requestOptions: {
|
||||
auth: 'admin:admin',
|
||||
},
|
||||
dicomUploadEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -3,6 +3,10 @@ window.config = {
|
||||
// whiteLabelling: {},
|
||||
extensions: [],
|
||||
modes: [],
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
showStudyList: true,
|
||||
maxNumberOfWebWorkers: 3,
|
||||
showLoadingIndicator: true,
|
||||
@ -28,6 +32,7 @@ window.config = {
|
||||
useBulkDataURI: false,
|
||||
supportsFuzzyMatching: true,
|
||||
supportsWildcard: true,
|
||||
dicomUploadEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -56,13 +56,14 @@ function DataSourceWrapper(props) {
|
||||
// But only for LayoutTemplate type of 'list'?
|
||||
// Or no data fetching here, and just hand down my source
|
||||
const STUDIES_LIMIT = 101;
|
||||
const [data, setData] = useState({
|
||||
const DEFAULT_DATA = {
|
||||
studies: [],
|
||||
total: 0,
|
||||
resultsPerPage: 25,
|
||||
pageNumber: 1,
|
||||
location: 'Not a valid location, causes first load to occur',
|
||||
});
|
||||
};
|
||||
const [data, setData] = useState(DEFAULT_DATA);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -125,6 +126,8 @@ function DataSourceWrapper(props) {
|
||||
dataTotal={data.total}
|
||||
dataSource={dataSource}
|
||||
isLoadingData={isLoading}
|
||||
// To refresh the data, simply reset it to DEFAULT_DATA which invalidates it and triggers a new query to fetch the data.
|
||||
onRefresh={() => setData(DEFAULT_DATA)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import filtersMeta from './filtersMeta.js';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useDebounce, useSearchParams } from '@hooks';
|
||||
import { utils, hotkeys } from '@ohif/core';
|
||||
import { utils, hotkeys, ServicesManager } from '@ohif/core';
|
||||
|
||||
import {
|
||||
Icon,
|
||||
@ -47,6 +47,8 @@ function WorkList({
|
||||
dataSource,
|
||||
hotkeysManager,
|
||||
dataPath,
|
||||
onRefresh,
|
||||
servicesManager,
|
||||
}) {
|
||||
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
|
||||
const { show, hide } = useModal();
|
||||
@ -429,6 +431,33 @@ function WorkList({
|
||||
});
|
||||
}
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
const { component: dicomUploadComponent } =
|
||||
customizationService.get('dicomUploadComponent') ?? {};
|
||||
const uploadProps =
|
||||
dicomUploadComponent && dataSource.getConfig().dicomUploadEnabled
|
||||
? {
|
||||
title: 'Upload files',
|
||||
closeButton: true,
|
||||
shouldCloseOnEsc: false,
|
||||
shouldCloseOnOverlayClick: false,
|
||||
content: dicomUploadComponent.bind(null, {
|
||||
dataSource,
|
||||
onComplete: () => {
|
||||
hide();
|
||||
onRefresh();
|
||||
},
|
||||
onStarted: () => {
|
||||
show({
|
||||
...uploadProps,
|
||||
// when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself
|
||||
closeButton: false,
|
||||
});
|
||||
},
|
||||
}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="bg-black h-screen flex flex-col ">
|
||||
<Header
|
||||
@ -445,6 +474,7 @@ function WorkList({
|
||||
onChange={setFilterValues}
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
onUploadClick={uploadProps ? () => show(uploadProps) : undefined}
|
||||
/>
|
||||
{hasStudies ? (
|
||||
<>
|
||||
@ -478,8 +508,10 @@ WorkList.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
dataSource: PropTypes.shape({
|
||||
query: PropTypes.object.isRequired,
|
||||
getConfig: PropTypes.func,
|
||||
}).isRequired,
|
||||
isLoadingData: PropTypes.bool.isRequired,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
};
|
||||
|
||||
const defaultFilterValues = {
|
||||
|
||||
@ -25,12 +25,6 @@ const bakedInRoutes = [
|
||||
|
||||
// NOT FOUND (404)
|
||||
const notFoundRoute = { component: NotFound };
|
||||
const WorkListRoute = {
|
||||
path: '/',
|
||||
children: DataSourceWrapper,
|
||||
private: true,
|
||||
props: { children: WorkList },
|
||||
};
|
||||
|
||||
const createRoutes = ({
|
||||
modes,
|
||||
@ -54,6 +48,13 @@ const createRoutes = ({
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const WorkListRoute = {
|
||||
path: '/',
|
||||
children: DataSourceWrapper,
|
||||
private: true,
|
||||
props: { children: WorkList, servicesManager },
|
||||
};
|
||||
|
||||
const customRoutes = customizationService.getGlobalCustomization(
|
||||
'customRoutes'
|
||||
);
|
||||
|
||||
305
yarn.lock
305
yarn.lock
@ -1272,7 +1272,7 @@
|
||||
core-js-pure "^3.25.1"
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@7.17.9", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@7.17.9", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.21.0"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
|
||||
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
|
||||
@ -1387,34 +1387,19 @@
|
||||
resolved "https://registry.npmjs.org/@cornerstonejs/calculate-suv/-/calculate-suv-1.0.3.tgz#6d99a72032c0f90cebf44dc6f0b12a5f1102e884"
|
||||
integrity sha512-2SwVJKzC1DzyxdxJtCht9dhTND2GFjLwhhkDyyC7vJq5tIgbhxgPk1CSwovO1pxmoybAXzjOxnaubllxLgoT+w==
|
||||
|
||||
"@cornerstonejs/codec-charls@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-charls/-/codec-charls-0.1.1.tgz#e55d4aa908732d0cc902888b7f3856c5a996df7f"
|
||||
integrity sha512-Y250DGVzmownJ7WgpHxNqWvfTnv4/malaKm/tWm0xE1FxhQE8iErMWFpKxpNDk3MdfXO4/98piVsUwmJMiWoDQ==
|
||||
|
||||
"@cornerstonejs/codec-charls@^1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-charls/-/codec-charls-1.2.3.tgz#6952c420486822ac8404409ae0ed5a559aff6e25"
|
||||
resolved "https://registry.npmjs.org/@cornerstonejs/codec-charls/-/codec-charls-1.2.3.tgz#6952c420486822ac8404409ae0ed5a559aff6e25"
|
||||
integrity sha512-qKUe6DN0dnGzhhfZLYhH9UZacMcudjxcaLXCrpxJImT/M/PQvZCT2rllu6VGJbWKJWG+dMVV2zmmleZcdJ7/cA==
|
||||
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit@^0.0.7":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-0.0.7.tgz#2ea9b575eed19e6e7e3701b7a50a4ae0ffbef0c4"
|
||||
integrity sha512-qgm6BuVAy5mNP8SJ+A6+VbmPnqgj8jPvJrw4HbUoAzndmf9/VHjTYwawn3kmZWya5ErFAsXQ6c0U0noB1LKAiA==
|
||||
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-1.2.2.tgz#ae384b149d6655e3dd6e18b9891fab479ab5e144"
|
||||
resolved "https://registry.npmjs.org/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-1.2.2.tgz#ae384b149d6655e3dd6e18b9891fab479ab5e144"
|
||||
integrity sha512-aAUMK2958YNpOb/7G6e2/aG7hExTiFTASlMt/v90XA0pRHdWiNg5ny4S5SAju0FbIw4zcMnR0qfY+yW3VG2ivg==
|
||||
|
||||
"@cornerstonejs/codec-openjpeg@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.1.1.tgz#5bd1c52a33a425299299e970312731fa0cc2711b"
|
||||
integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg==
|
||||
|
||||
"@cornerstonejs/codec-openjpeg@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-1.2.2.tgz#f0b524235b5551426b46db197a37b06f8ac805d7"
|
||||
resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-1.2.2.tgz#f0b524235b5551426b46db197a37b06f8ac805d7"
|
||||
integrity sha512-b1O7lZacKXelgeV9n8XWZ7pTw3i4Bq4qQ26G5ahBjWoOw4QNcCrb5hPxWBxNB/I8AoNbJxAe+lyLtyQGfdrTbw==
|
||||
|
||||
"@cornerstonejs/codec-openjph@^2.4.2":
|
||||
@ -3423,35 +3408,6 @@
|
||||
npmlog "^4.1.2"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@mapbox/jsonlint-lines-primitives@~2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234"
|
||||
integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==
|
||||
|
||||
"@mapbox/mapbox-gl-style-spec@^13.23.1":
|
||||
version "13.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz#2ec226320a0f77856046e000df9b419303a56458"
|
||||
integrity sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==
|
||||
dependencies:
|
||||
"@mapbox/jsonlint-lines-primitives" "~2.0.2"
|
||||
"@mapbox/point-geometry" "^0.1.0"
|
||||
"@mapbox/unitbezier" "^0.0.0"
|
||||
csscolorparser "~1.0.2"
|
||||
json-stringify-pretty-compact "^2.0.0"
|
||||
minimist "^1.2.6"
|
||||
rw "^1.3.3"
|
||||
sort-object "^0.3.2"
|
||||
|
||||
"@mapbox/point-geometry@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2"
|
||||
integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==
|
||||
|
||||
"@mapbox/unitbezier@^0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz#15651bd553a67b8581fb398810c98ad86a34524e"
|
||||
integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==
|
||||
|
||||
"@mdx-js/mdx@^1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
|
||||
@ -3713,11 +3669,6 @@
|
||||
resolved "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.24.0.tgz#b6e83333c437ac106386e10a776dc823c1a90f33"
|
||||
integrity sha512-kfYxX0rHP5N2Da6HyfjRCVaeNahAO9XV5WD4SKWKKjdKVkV/Z5/XjVgSKlTBLSYxnWDzYJJ4UHZV43Mw+facMA==
|
||||
|
||||
"@petamoriken/float16@^3.4.7":
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.7.1.tgz#4a0cc0854a3a101cc2d697272f120e1a05975ce5"
|
||||
integrity sha512-oXZOc+aePd0FnhTWk15pyqK+Do87n0TyLV1nxdEougE95X/WXWDqmQobfhgnSY7QsWn5euZUWuDVeTQvoQ5VNw==
|
||||
|
||||
"@philpl/buble@^0.19.7":
|
||||
version "0.19.7"
|
||||
resolved "https://registry.npmjs.org/@philpl/buble/-/buble-0.19.7.tgz#27231e6391393793b64bc1c982fc7b593198b893"
|
||||
@ -7663,13 +7614,6 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.19:
|
||||
resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||
|
||||
colormap@^2.3:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/colormap/-/colormap-2.3.2.tgz#4422c1178ce563806e265b96782737be85815abf"
|
||||
integrity sha512-jDOjaoEEmA9AgA11B/jCSAvYE95r3wRoAyTf3LEHGiUVlNHJaL1mRkf5AyLSpQBVGfTEPwGEqCIzL+kgr2WgNA==
|
||||
dependencies:
|
||||
lerp "^1.0.3"
|
||||
|
||||
colors@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
||||
@ -7753,11 +7697,6 @@ compare-func@^2.0.0:
|
||||
array-ify "^1.0.0"
|
||||
dot-prop "^5.1.0"
|
||||
|
||||
complex.js@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31"
|
||||
integrity sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==
|
||||
|
||||
component-emitter@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
||||
@ -8393,11 +8332,6 @@ css-what@^6.0.1, css-what@^6.1.0:
|
||||
resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
|
||||
csscolorparser@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b"
|
||||
integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==
|
||||
|
||||
cssdb@^7.1.0:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.npmjs.org/cssdb/-/cssdb-7.5.4.tgz#e34dafee5184d67634604e345e389ca79ac179ea"
|
||||
@ -8746,17 +8680,6 @@ dayjs@^1.10.4:
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
|
||||
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
|
||||
|
||||
dcmjs@^0.27:
|
||||
version "0.27.0"
|
||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.27.0.tgz#2662818c8b20494e366583e6dd3577c20d04d6ff"
|
||||
integrity sha512-26wtatOLh+0b0aFy9iOg7PdOLG9EHevn9nEOn7Aoo5l7P9aFAMZ3XAa9Q+NULzLE2Q7DcIf2TQvfyVtdzhQzeg==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs2" "^7.17.8"
|
||||
gl-matrix "^3.1.0"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
loglevelnext "^3.0.1"
|
||||
ndarray "^1.0.19"
|
||||
|
||||
dcmjs@^0.29.5:
|
||||
version "0.29.6"
|
||||
resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.29.6.tgz#6ca1543e74bae29657d1f7f2273407e23266c011"
|
||||
@ -8820,11 +8743,6 @@ decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0:
|
||||
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||
|
||||
decimal.js@^10.4.3:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
|
||||
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
|
||||
|
||||
decode-named-character-reference@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
|
||||
@ -9150,37 +9068,15 @@ dezalgo@^1.0.0:
|
||||
asap "^2.0.0"
|
||||
wrappy "1"
|
||||
|
||||
dicom-microscopy-viewer@^0.44.0:
|
||||
version "0.44.0"
|
||||
resolved "https://registry.npmjs.org/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.44.0.tgz#d4a9e985acb23c5b82a9aedbe379b39198b8fd55"
|
||||
integrity sha512-7rcm8bXTcOLsXrhBPwWe5gf4Sj1Rz1lRh+ECcaznOEr/uvX6BTAYLqNJNmeAUMoKcadboEeDrmLihCwCteRSJQ==
|
||||
dependencies:
|
||||
"@cornerstonejs/codec-charls" "^0.1.1"
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^0.0.7"
|
||||
"@cornerstonejs/codec-openjpeg" "^0.1.1"
|
||||
colormap "^2.3"
|
||||
dcmjs "^0.27"
|
||||
dicomicc "^0.1"
|
||||
dicomweb-client "^0.8"
|
||||
image-type "^4.1"
|
||||
mathjs "^11.2"
|
||||
ol "^7.1"
|
||||
uuid "^9.0"
|
||||
|
||||
dicom-parser@^1.8.9:
|
||||
version "1.8.21"
|
||||
resolved "https://registry.npmjs.org/dicom-parser/-/dicom-parser-1.8.21.tgz#916fdc77776367976b8457cad462b5b7cf74eaea"
|
||||
integrity sha512-lYCweHQDsC8UFpXErPlg86Px2A8bay0HiUY+wzoG3xv5GzgqVHU3lziwSc/Gzn7VV7y2KeP072SzCviuOoU02w==
|
||||
|
||||
dicomicc@^0.1:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dicomicc/-/dicomicc-0.1.0.tgz#c73acc60a8e2d73a20f462c8c7d0e1e0d977c486"
|
||||
integrity sha512-kZejPGjLQ9NsgovSyVsiAuCpq6LofNR9Erc8Tt/vQAYGYCoQnTyWDlg5D0TJJQATKul7cSr9k/q0TF8G9qdDkQ==
|
||||
|
||||
dicomweb-client@^0.8, dicomweb-client@^0.8.4:
|
||||
version "0.8.4"
|
||||
resolved "https://registry.npmjs.org/dicomweb-client/-/dicomweb-client-0.8.4.tgz#3da814cedb9415facb50bc5f43af8d961a991c74"
|
||||
integrity sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw==
|
||||
dicomweb-client@^0.10.2:
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.10.2.tgz#9c2e466264a5d3b56c18edaafd360e89912cac13"
|
||||
integrity sha512-sGjq3TxM7jgbe7cBpqddT1VlfOlwXmE7Q12qCSGDOkEL7fgn+Ak/5oW/LiRP2FzmfYzcbpO32r+hC8i4ITXQLw==
|
||||
|
||||
didyoumean@^1.2.2:
|
||||
version "1.2.2"
|
||||
@ -9495,11 +9391,6 @@ duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0:
|
||||
readable-stream "^2.0.0"
|
||||
stream-shift "^1.0.0"
|
||||
|
||||
earcut@^2.2.3:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a"
|
||||
integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
@ -9812,11 +9703,6 @@ escape-html@^1.0.3, escape-html@~1.0.3:
|
||||
resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||
|
||||
escape-latex@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1"
|
||||
integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==
|
||||
|
||||
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
@ -10780,11 +10666,6 @@ file-system-cache@^2.0.0:
|
||||
fs-extra "^11.1.0"
|
||||
ramda "^0.28.0"
|
||||
|
||||
file-type@^10.10.0:
|
||||
version "10.11.0"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.11.0.tgz#2961d09e4675b9fb9a3ee6b69e9cd23f43fd1890"
|
||||
integrity sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
@ -11252,19 +11133,6 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
|
||||
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
geotiff@^2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.npmjs.org/geotiff/-/geotiff-2.0.7.tgz#358e578233af70bfb0b4dee62d599ad78fc5cfca"
|
||||
integrity sha512-FKvFTNowMU5K6lHYY2f83d4lS2rsCNdpUC28AX61x9ZzzqPNaWFElWv93xj0eJFaNyOYA63ic5OzJ88dHpoA5Q==
|
||||
dependencies:
|
||||
"@petamoriken/float16" "^3.4.7"
|
||||
lerc "^3.0.0"
|
||||
pako "^2.0.4"
|
||||
parse-headers "^2.0.2"
|
||||
quick-lru "^6.1.1"
|
||||
web-worker "^1.2.0"
|
||||
xml-utils "^1.0.2"
|
||||
|
||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
@ -12283,7 +12151,7 @@ identity-obj-proxy@3.0.x:
|
||||
dependencies:
|
||||
harmony-reflect "^1.4.6"
|
||||
|
||||
ieee754@^1.1.12, ieee754@^1.1.13:
|
||||
ieee754@^1.1.13:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
@ -12317,13 +12185,6 @@ image-size@^1.0.1:
|
||||
dependencies:
|
||||
queue "6.0.2"
|
||||
|
||||
image-type@^4.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/image-type/-/image-type-4.1.0.tgz#72a88d64ff5021371ed67b9a466442100be57cd1"
|
||||
integrity sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==
|
||||
dependencies:
|
||||
file-type "^10.10.0"
|
||||
|
||||
immer@^9.0.7:
|
||||
version "9.0.21"
|
||||
resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
|
||||
@ -13266,11 +13127,6 @@ jake@^10.8.5:
|
||||
filelist "^1.0.1"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
javascript-natural-sort@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
|
||||
integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==
|
||||
|
||||
jest-canvas-mock@^2.1.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.0.tgz#3e60f87f77ddfa273cf8e7e4ea5f86fa827c7117"
|
||||
@ -13896,11 +13752,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json-stringify-pretty-compact@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz#e77c419f52ff00c45a31f07f4c820c2433143885"
|
||||
integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==
|
||||
|
||||
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
@ -14077,11 +13928,6 @@ left-pad@^1.3.0:
|
||||
resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
|
||||
integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
|
||||
|
||||
lerc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lerc/-/lerc-3.0.0.tgz#36f36fbd4ba46f0abf4833799fff2e7d6865f5cb"
|
||||
integrity sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==
|
||||
|
||||
lerna@^3.15.0:
|
||||
version "3.22.1"
|
||||
resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62"
|
||||
@ -14106,11 +13952,6 @@ lerna@^3.15.0:
|
||||
import-local "^2.0.0"
|
||||
npmlog "^4.1.2"
|
||||
|
||||
lerp@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lerp/-/lerp-1.0.3.tgz#a18c8968f917896de15ccfcc28d55a6b731e776e"
|
||||
integrity sha512-70Rh4rCkJDvwWiTsyZ1HmJGvnyfFah4m6iTux29XmasRiZPDBpT9Cfa4ai73+uLZxnlKruUS62jj2lb11wURiA==
|
||||
|
||||
leven@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||
@ -14635,11 +14476,6 @@ map-visit@^1.0.0:
|
||||
dependencies:
|
||||
object-visit "^1.0.0"
|
||||
|
||||
mapbox-to-css-font@^2.4.1:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/mapbox-to-css-font/-/mapbox-to-css-font-2.4.2.tgz#a9e31b363ad8ca881cd339ca99f2d2a6b02ea5dd"
|
||||
integrity sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA==
|
||||
|
||||
markdown-escapes@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
|
||||
@ -14660,21 +14496,6 @@ material-colors@^1.2.1:
|
||||
resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
||||
integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
|
||||
|
||||
mathjs@^11.2:
|
||||
version "11.8.0"
|
||||
resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-11.8.0.tgz#b02e66461ec068fadf1e90c221121704dc14d8f5"
|
||||
integrity sha512-I7r8HCoqUGyEiHQdeOCF2m2k9N+tcOHO3cZQ3tyJkMMBQMFqMR7dMQEboBMJAiFW2Um3PEItGPwcOc4P6KRqwg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
complex.js "^2.1.1"
|
||||
decimal.js "^10.4.3"
|
||||
escape-latex "^1.2.0"
|
||||
fraction.js "^4.2.0"
|
||||
javascript-natural-sort "^0.7.1"
|
||||
seedrandom "^3.0.5"
|
||||
tiny-emitter "^2.1.0"
|
||||
typed-function "^4.1.0"
|
||||
|
||||
mdast-squeeze-paragraphs@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97"
|
||||
@ -16135,25 +15956,6 @@ oidc-client@1.11.5:
|
||||
crypto-js "^4.0.0"
|
||||
serialize-javascript "^4.0.0"
|
||||
|
||||
ol-mapbox-style@^9.2.0:
|
||||
version "9.7.0"
|
||||
resolved "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-9.7.0.tgz#38a4f7abc8f0a94f378dcdb7cefdcc69ca3f6287"
|
||||
integrity sha512-YX3u8FBJHsRHaoGxmd724Mp5WPTuV7wLQW6zZhcihMuInsSdCX1EiZfU+8IAL7jG0pbgl5YgC0aWE/MXJcUXxg==
|
||||
dependencies:
|
||||
"@mapbox/mapbox-gl-style-spec" "^13.23.1"
|
||||
mapbox-to-css-font "^2.4.1"
|
||||
|
||||
ol@^7.1:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.npmjs.org/ol/-/ol-7.3.0.tgz#7ffb5f258dafa4a3e218208aad9054d61f6fe786"
|
||||
integrity sha512-08vJE4xITKPazQ9qJjeqYjRngnM9s+1eSv219Pdlrjj3LpLqjEH386ncq+76Dw1oGPGR8eLVEePk7FEd9XqqMw==
|
||||
dependencies:
|
||||
earcut "^2.2.3"
|
||||
geotiff "^2.0.7"
|
||||
ol-mapbox-style "^9.2.0"
|
||||
pbf "3.2.1"
|
||||
rbush "^3.0.1"
|
||||
|
||||
on-finished@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||
@ -16499,11 +16301,6 @@ parse-github-repo-url@^1.3.0:
|
||||
resolved "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50"
|
||||
integrity sha512-bSWyzBKqcSL4RrncTpGsEKoJ7H8a4L3++ifTAbTFeMHyq2wRV+42DGmQcHIrJIvdcacjIOxEuKH/w4tthF17gg==
|
||||
|
||||
parse-headers@^2.0.2:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.5.tgz#069793f9356a54008571eb7f9761153e6c770da9"
|
||||
integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==
|
||||
|
||||
parse-json@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
|
||||
@ -16704,14 +16501,6 @@ pause-stream@0.0.11:
|
||||
dependencies:
|
||||
through "~2.3"
|
||||
|
||||
pbf@3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a"
|
||||
integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==
|
||||
dependencies:
|
||||
ieee754 "^1.1.12"
|
||||
resolve-protobuf-schema "^2.1.0"
|
||||
|
||||
peek-stream@^1.1.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67"
|
||||
@ -17939,11 +17728,6 @@ proto-list@~1.2.1:
|
||||
resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==
|
||||
|
||||
protocol-buffers-schema@^3.3.1:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03"
|
||||
integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==
|
||||
|
||||
protocols@^1.4.0:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8"
|
||||
@ -18135,16 +17919,6 @@ quick-lru@^5.1.1:
|
||||
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
||||
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
||||
|
||||
quick-lru@^6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz#f8e5bf9010376c126c80c1a62827a526c0e60adf"
|
||||
integrity sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==
|
||||
|
||||
quickselect@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018"
|
||||
integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==
|
||||
|
||||
raf@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||
@ -18184,13 +17958,6 @@ raw-body@2.5.1:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
rbush@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf"
|
||||
integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==
|
||||
dependencies:
|
||||
quickselect "^2.0.0"
|
||||
|
||||
rc@1.2.8, rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
@ -19276,13 +19043,6 @@ resolve-pathname@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
||||
|
||||
resolve-protobuf-schema@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758"
|
||||
integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==
|
||||
dependencies:
|
||||
protocol-buffers-schema "^3.3.1"
|
||||
|
||||
resolve-url@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
||||
@ -19459,11 +19219,6 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
rw@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
|
||||
integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
|
||||
|
||||
rxjs@^6.3.3, rxjs@^6.4.0:
|
||||
version "6.6.7"
|
||||
resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
|
||||
@ -19613,7 +19368,7 @@ section-matter@^1.0.0:
|
||||
extend-shallow "^2.0.1"
|
||||
kind-of "^6.0.0"
|
||||
|
||||
seedrandom@3.0.5, seedrandom@^3.0.5:
|
||||
seedrandom@3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
|
||||
integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
|
||||
@ -20058,21 +19813,11 @@ socks@~2.3.2:
|
||||
ip "1.1.5"
|
||||
smart-buffer "^4.1.0"
|
||||
|
||||
sort-asc@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.1.0.tgz#ab799df61fc73ea0956c79c4b531ed1e9e7727e9"
|
||||
integrity sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==
|
||||
|
||||
sort-css-media-queries@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz#7c85e06f79826baabb232f5560e9745d7a78c4ce"
|
||||
integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA==
|
||||
|
||||
sort-desc@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/sort-desc/-/sort-desc-0.1.1.tgz#198b8c0cdeb095c463341861e3925d4ee359a9ee"
|
||||
integrity sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==
|
||||
|
||||
sort-keys@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
||||
@ -20087,14 +19832,6 @@ sort-keys@^2.0.0:
|
||||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
sort-object@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/sort-object/-/sort-object-0.3.2.tgz#98e0d199ede40e07c61a84403c61d6c3b290f9e2"
|
||||
integrity sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==
|
||||
dependencies:
|
||||
sort-asc "^0.1.0"
|
||||
sort-desc "^0.1.1"
|
||||
|
||||
source-list-map@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||
@ -21086,11 +20823,6 @@ timsort@^0.3.0:
|
||||
resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
|
||||
|
||||
tiny-emitter@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
||||
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
|
||||
|
||||
tiny-invariant@^1.0.2:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
|
||||
@ -21352,11 +21084,6 @@ typed-array-length@^1.0.4:
|
||||
for-each "^0.3.3"
|
||||
is-typed-array "^1.1.9"
|
||||
|
||||
typed-function@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz#da4bdd8a6d19a89e22732f75e4a410860aaf9712"
|
||||
integrity sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg==
|
||||
|
||||
typedarray-to-buffer@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||
@ -21853,7 +21580,7 @@ uuid@^8.3.2:
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuid@^9.0, uuid@^9.0.0:
|
||||
uuid@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||
@ -22052,11 +21779,6 @@ web-streams-polyfill@^3.0.3:
|
||||
resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||
|
||||
web-worker@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da"
|
||||
integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==
|
||||
|
||||
webgl-constants@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855"
|
||||
@ -22777,11 +22499,6 @@ xml-name-validator@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
|
||||
|
||||
xml-utils@^1.0.2:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.3.0.tgz#f1043534e3ac3deda12ddab39f8442e16da98ebb"
|
||||
integrity sha512-i4PIrX33Wd66dvwo4syicwlwmnr6wuvvn4f2ku9hA67C2Uk62Xubczuhct+Evnd12/DV71qKNeDdJwES8HX1RA==
|
||||
|
||||
xml@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user