feat(studyPrefetcher): Study Prefetcher (#4206)
This commit is contained in:
parent
257cc477cd
commit
2048b19484
@ -25,6 +25,7 @@ import initCornerstoneTools from './initCornerstoneTools';
|
||||
|
||||
import { connectToolsToMeasurementService } from './initMeasurementService';
|
||||
import initCineService from './initCineService';
|
||||
import initStudyPrefetcherService from './initStudyPrefetcherService';
|
||||
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
||||
import nthLoader from './utils/nthLoader';
|
||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||
@ -98,6 +99,7 @@ export default async function init({
|
||||
hangingProtocolService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
studyPrefetcherService
|
||||
} = servicesManager.services;
|
||||
|
||||
window.services = servicesManager.services;
|
||||
@ -190,6 +192,7 @@ export default async function init({
|
||||
this.measurementServiceSource = connectToolsToMeasurementService(servicesManager);
|
||||
|
||||
initCineService(servicesManager);
|
||||
initStudyPrefetcherService(servicesManager);
|
||||
|
||||
// When a custom image load is performed, update the relevant viewports
|
||||
hangingProtocolService.subscribe(
|
||||
|
||||
33
extensions/cornerstone/src/initStudyPrefetcherService.ts
Normal file
33
extensions/cornerstone/src/initStudyPrefetcherService.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { cache, imageLoadPoolManager, imageLoader, Enums, eventTarget, EVENTS as csEvents } from '@cornerstonejs/core';
|
||||
|
||||
function initStudyPrefetcherService(servicesManager: AppTypes.ServicesManager) {
|
||||
const { studyPrefetcherService } = servicesManager.services;
|
||||
|
||||
studyPrefetcherService.requestType = Enums.RequestType.Prefetch;
|
||||
studyPrefetcherService.imageLoadPoolManager = imageLoadPoolManager;
|
||||
studyPrefetcherService.imageLoader = imageLoader;
|
||||
|
||||
studyPrefetcherService.cache = {
|
||||
isImageCached(imageId: string): boolean {
|
||||
return !!cache.getImageLoadObject(imageId);
|
||||
}
|
||||
}
|
||||
|
||||
studyPrefetcherService.imageLoadEventsManager = {
|
||||
addEventListeners(onImageLoaded, onImageLoadFailed) {
|
||||
eventTarget.addEventListener(csEvents.IMAGE_LOADED, onImageLoaded);
|
||||
eventTarget.addEventListener(csEvents.IMAGE_LOAD_FAILED, onImageLoadFailed);
|
||||
|
||||
return [
|
||||
{
|
||||
unsubscribe: () => eventTarget.removeEventListener(csEvents.IMAGE_LOADED, onImageLoaded)
|
||||
},
|
||||
{
|
||||
unsubscribe: () => eventTarget.removeEventListener(csEvents.IMAGE_LOAD_FAILED, onImageLoadFailed)
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default initStudyPrefetcherService;
|
||||
@ -17,12 +17,13 @@ const createRenderedRetrieve = (config, params) => {
|
||||
const { wadoRoot } = config;
|
||||
const { instance, tag = 'PixelData' } = params;
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||
const value = instance[tag];
|
||||
const bulkDataURI = instance[tag]?.BulkDataURI ?? '';
|
||||
|
||||
if (value?.BulkDataURI?.indexOf('?') !== -1) {
|
||||
if (bulkDataURI?.indexOf('?') !== -1) {
|
||||
// The value instance has parameters, so it should not revert to the rendered
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag === 'PixelData' || tag === 'EncapsulatedDocument') {
|
||||
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`;
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ function PanelStudyBrowserTracking({
|
||||
hangingProtocolService,
|
||||
uiNotificationService,
|
||||
measurementService,
|
||||
studyPrefetcherService,
|
||||
} = servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -43,6 +44,7 @@ function PanelStudyBrowserTracking({
|
||||
]);
|
||||
const [studyDisplayList, setStudyDisplayList] = useState([]);
|
||||
const [displaySets, setDisplaySets] = useState([]);
|
||||
const [displaySetsLoadingState, setDisplaySetsLoadingState] = useState({});
|
||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||
const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null);
|
||||
|
||||
@ -162,6 +164,7 @@ function PanelStudyBrowserTracking({
|
||||
|
||||
const mappedDisplaySets = _mapDisplaySets(
|
||||
currentDisplaySets,
|
||||
displaySetsLoadingState,
|
||||
thumbnailImageSrcMap,
|
||||
trackedSeries,
|
||||
viewports,
|
||||
@ -176,12 +179,30 @@ function PanelStudyBrowserTracking({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
displaySetService.activeDisplaySets,
|
||||
displaySetsLoadingState,
|
||||
trackedSeries,
|
||||
viewports,
|
||||
dataSource,
|
||||
thumbnailImageSrcMap,
|
||||
]);
|
||||
|
||||
// -- displaySetsLoadingState
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = studyPrefetcherService.subscribe(
|
||||
studyPrefetcherService.EVENTS.DISPLAYSET_LOAD_PROGRESS,
|
||||
updatedDisplaySetLoadingState => {
|
||||
const { displaySetInstanceUID, loadingProgress } = updatedDisplaySetLoadingState;
|
||||
|
||||
setDisplaySetsLoadingState(prevState => ({
|
||||
...prevState,
|
||||
[displaySetInstanceUID]: loadingProgress,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [studyPrefetcherService]);
|
||||
|
||||
// ~~ subscriptions --> displaySets
|
||||
useEffect(() => {
|
||||
// DISPLAY_SETS_ADDED returns an array of DisplaySets that were added
|
||||
@ -233,6 +254,7 @@ function PanelStudyBrowserTracking({
|
||||
changedDisplaySets => {
|
||||
const mappedDisplaySets = _mapDisplaySets(
|
||||
changedDisplaySets,
|
||||
displaySetsLoadingState,
|
||||
thumbnailImageSrcMap,
|
||||
trackedSeries,
|
||||
viewports,
|
||||
@ -252,6 +274,7 @@ function PanelStudyBrowserTracking({
|
||||
() => {
|
||||
const mappedDisplaySets = _mapDisplaySets(
|
||||
displaySetService.getActiveDisplaySets(),
|
||||
displaySetsLoadingState,
|
||||
thumbnailImageSrcMap,
|
||||
trackedSeries,
|
||||
viewports,
|
||||
@ -270,7 +293,14 @@ function PanelStudyBrowserTracking({
|
||||
SubscriptionDisplaySetsChanged.unsubscribe();
|
||||
SubscriptionDisplaySetMetaDataInvalidated.unsubscribe();
|
||||
};
|
||||
}, [thumbnailImageSrcMap, trackedSeries, viewports, dataSource, displaySetService]);
|
||||
}, [
|
||||
displaySetsLoadingState,
|
||||
thumbnailImageSrcMap,
|
||||
trackedSeries,
|
||||
viewports,
|
||||
dataSource,
|
||||
displaySetService,
|
||||
]);
|
||||
|
||||
const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets);
|
||||
|
||||
@ -454,6 +484,7 @@ function _mapDataSourceStudies(studies) {
|
||||
|
||||
function _mapDisplaySets(
|
||||
displaySets,
|
||||
displaySetLoadingState,
|
||||
thumbnailImageSrcMap,
|
||||
trackedSeriesInstanceUIDs,
|
||||
viewports, // TODO: make array of `displaySetInstanceUIDs`?
|
||||
@ -476,6 +507,7 @@ function _mapDisplaySets(
|
||||
componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
|
||||
|
||||
const { displaySetInstanceUID } = ds;
|
||||
const loadingProgress = displaySetLoadingState?.[displaySetInstanceUID];
|
||||
|
||||
const thumbnailProps = {
|
||||
displaySetInstanceUID,
|
||||
@ -484,6 +516,7 @@ function _mapDisplaySets(
|
||||
modality: ds.Modality,
|
||||
seriesDate: formatDate(ds.SeriesDate),
|
||||
numInstances: ds.numImageFrames,
|
||||
loadingProgress,
|
||||
countIcon: ds.countIcon,
|
||||
messages: ds.messages,
|
||||
StudyInstanceUID: ds.StudyInstanceUID,
|
||||
|
||||
@ -12,7 +12,14 @@ window.config = {
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
showCPUFallbackMessage: true,
|
||||
showLoadingIndicator: true,
|
||||
experimentalStudyBrowserSort: false,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
studyPrefetcher: {
|
||||
enabled: true,
|
||||
displaySetsCount: 2,
|
||||
maxNumPrefetchRequests: 10,
|
||||
order: 'closest',
|
||||
},
|
||||
defaultDataSourceName: 'dicomweb',
|
||||
dataSources: [
|
||||
{
|
||||
@ -47,4 +54,7 @@ window.config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
httpErrorHandler: error => {
|
||||
console.warn(`HTTP Error Handler (status: ${error.status})`, error);
|
||||
},
|
||||
};
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
CustomizationService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
// utils,
|
||||
} from '@ohif/core';
|
||||
|
||||
@ -71,6 +72,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
PanelService.REGISTRATION,
|
||||
WorkflowStepsService.REGISTRATION,
|
||||
StateSyncService.REGISTRATION,
|
||||
[StudyPrefetcherService.REGISTRATION, appConfig.studyPrefetcher],
|
||||
]);
|
||||
|
||||
errorHandler.getHTTPErrorHandler = () => {
|
||||
|
||||
@ -134,6 +134,12 @@ export default class ExtensionManager extends PubSubService {
|
||||
return [...this.registeredExtensionIds];
|
||||
}
|
||||
|
||||
private getUniqueServicesList(servicesManager: AppTypes.ServicesManager) {
|
||||
// Make sure only one service instance is returned because almost all services are
|
||||
// registered with different keys (eg: StudyPrefetcherService and studyPrefetcherService)
|
||||
return Array.from(new Set(Object.values(servicesManager.services)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls all the services and extension on mode enters.
|
||||
* The service onModeEnter is called first
|
||||
@ -148,11 +154,12 @@ export default class ExtensionManager extends PubSubService {
|
||||
_hotkeysManager,
|
||||
_extensionLifeCycleHooks,
|
||||
} = this;
|
||||
const services = this.getUniqueServicesList(_servicesManager);
|
||||
|
||||
// The onModeEnter of the service must occur BEFORE the extension
|
||||
// onModeEnter in order to reset the state to a standard state
|
||||
// before the extension restores and cached data.
|
||||
for (const service of Object.values(_servicesManager.services)) {
|
||||
for (const service of services) {
|
||||
service?.onModeEnter?.();
|
||||
}
|
||||
|
||||
@ -172,6 +179,7 @@ export default class ExtensionManager extends PubSubService {
|
||||
public onModeExit(): void {
|
||||
const { registeredExtensionIds, _servicesManager, _commandsManager, _extensionLifeCycleHooks } =
|
||||
this;
|
||||
const services = this.getUniqueServicesList(_servicesManager);
|
||||
|
||||
registeredExtensionIds.forEach(extensionId => {
|
||||
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
||||
@ -186,7 +194,7 @@ export default class ExtensionManager extends PubSubService {
|
||||
|
||||
// The service onModeExit calls must occur after the extension ones
|
||||
// so that extension ones can store/restore data.
|
||||
for (const service of Object.values(_servicesManager.services)) {
|
||||
for (const service of services) {
|
||||
try {
|
||||
service?.onModeExit?.();
|
||||
} catch (e) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as OHIF from './index';
|
||||
|
||||
describe('Top level exports', () => {
|
||||
test.only('have not changed', () => {
|
||||
test('have not changed', () => {
|
||||
const expectedExports = [
|
||||
'MODULE_TYPES',
|
||||
//
|
||||
@ -47,6 +47,7 @@ describe('Top level exports', () => {
|
||||
'PubSubService',
|
||||
'PanelService',
|
||||
'WorkflowStepsService',
|
||||
'StudyPrefetcherService',
|
||||
'useToolbar',
|
||||
].sort();
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
StateSyncService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
} from './services';
|
||||
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||
@ -85,6 +86,7 @@ const OHIF = {
|
||||
PanelService,
|
||||
useToolbar,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
};
|
||||
|
||||
export {
|
||||
@ -130,6 +132,7 @@ export {
|
||||
Types,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
useToolbar,
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,686 @@
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import ServicesManager from '../ServicesManager';
|
||||
import ViewportGridService from '../ViewportGridService';
|
||||
import { DisplaySet } from '../../types';
|
||||
|
||||
const IMAGE_REQUEST_TYPE = 'prefetch';
|
||||
|
||||
export const EVENTS = {
|
||||
SERVICE_STARTED: 'event::studyPrefetcherService:started',
|
||||
SERVICE_STOPPED: 'event::studyPrefetcherService:stopped',
|
||||
DISPLAYSET_LOAD_PROGRESS: 'event::studyPrefetcherService:displaySetLoadProgress',
|
||||
DISPLAYSET_LOAD_COMPLETE: 'event::studyPrefetcherService:displaySetLoadComplete',
|
||||
};
|
||||
|
||||
/**
|
||||
* Order used for prefetching display set
|
||||
*/
|
||||
enum StudyPrefetchOrder {
|
||||
closest = 'closest',
|
||||
downward = 'downward',
|
||||
upward = 'upward',
|
||||
}
|
||||
|
||||
/**
|
||||
* Study Prefetcher configuration
|
||||
*/
|
||||
type StudyPrefetcherConfig = {
|
||||
/* Enable/disable study prefetching service */
|
||||
enabled: boolean;
|
||||
/* Number of displaysets to be prefetched */
|
||||
displaySetsCount: number;
|
||||
/**
|
||||
* Max number of concurrent prefetch requests
|
||||
* High numbers may impact on the time to load a new dropped series because
|
||||
* the browser will be busy with all prefetching requests. As soon as the
|
||||
* prefetch requests get fulfilled the new ones from the new dropped series
|
||||
* are sent to the server.
|
||||
*
|
||||
* TODO: abort all prefetch requests when a new series is loaded on a viewport.
|
||||
* (need to add support for `AbortController` on Cornerstone)
|
||||
* */
|
||||
maxNumPrefetchRequests: number;
|
||||
/* Display sets prefetching order (closest, downward and upward) */
|
||||
order: StudyPrefetchOrder;
|
||||
};
|
||||
|
||||
type DisplaySetLoadingState = {
|
||||
displaySetInstanceUID: string;
|
||||
numInstances: number;
|
||||
pendingImageIds: Set<string>;
|
||||
loadedImageIds: Set<string>;
|
||||
failedImageIds: Set<string>;
|
||||
loadingProgress: number;
|
||||
};
|
||||
|
||||
type ImageRequest = {
|
||||
displaySetInstanceUID: string;
|
||||
imageId: string;
|
||||
aborted: boolean;
|
||||
};
|
||||
|
||||
type PubSubServiceSubscription = { unsubscribe: () => any };
|
||||
|
||||
interface ICache {
|
||||
isImageCached(imageId: string): boolean;
|
||||
}
|
||||
|
||||
interface IImageLoadPoolManager {
|
||||
addRequest (
|
||||
requestFn: () => Promise<any>,
|
||||
type: string,
|
||||
additionalDetails: Record<string, unknown>,
|
||||
priority?: number
|
||||
);
|
||||
clearRequestStack(type: string): void;
|
||||
}
|
||||
|
||||
interface IImageLoader {
|
||||
loadAndCacheImage(imageId: string, options: any): Promise<any>;
|
||||
}
|
||||
|
||||
type EventSubscription = {
|
||||
unsubscribe: () => void
|
||||
};
|
||||
|
||||
interface IImageLoadEventsManager {
|
||||
addEventListeners(
|
||||
onImageLoaded: (evt: any) => void,
|
||||
onImageLoadFailed: (evt: any) => void
|
||||
): EventSubscription[];
|
||||
}
|
||||
|
||||
class StudyPrefetcherService extends PubSubService {
|
||||
private _extensionManager: ExtensionManager;
|
||||
private _servicesManager: ServicesManager;
|
||||
private _subscriptions: PubSubServiceSubscription[];
|
||||
private _activeDisplaySetsInstanceUIDs: string[] = [];
|
||||
private _pendingRequests: ImageRequest[] = [];
|
||||
private _inflightRequests = new Map<string, ImageRequest>();
|
||||
private _isRunning = false;
|
||||
private _displaySetLoadingStates = new Map<string, DisplaySetLoadingState>();
|
||||
private _imageIdsToDisplaySetsMap = new Map<string, Set<string>>();
|
||||
private config: StudyPrefetcherConfig = {
|
||||
/* Enable/disable study prefetching service */
|
||||
enabled: false,
|
||||
/* Number of displaysets to be prefetched */
|
||||
displaySetsCount: 1,
|
||||
/**
|
||||
* Max number of concurrent prefetch requests
|
||||
* High numbers may impact on the time to load a new dropped series because
|
||||
* the browser will be busy with all prefetching requests. As soon as the
|
||||
* prefetch requests get fulfilled the new ones from the new dropped series
|
||||
* are sent to the server.
|
||||
*
|
||||
* TODO: abort all prefetch requests when a new series is loaded on a viewport.
|
||||
* (need to add support for `AbortController` on Cornerstone)
|
||||
* */
|
||||
maxNumPrefetchRequests: 10,
|
||||
/* Display sets prefetching order (closest, downward and upward) */
|
||||
order: StudyPrefetchOrder.downward,
|
||||
};
|
||||
|
||||
// Properties set by Cornerstone extension (initStudyPrefetcherService)
|
||||
public requestType: string = IMAGE_REQUEST_TYPE;
|
||||
public cache: ICache;
|
||||
public imageLoadPoolManager: IImageLoadPoolManager;
|
||||
public imageLoader: IImageLoader;
|
||||
public imageLoadEventsManager: IImageLoadEventsManager;
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'studyPrefetcherService',
|
||||
altName: 'StudyPrefetcherService',
|
||||
create: ({ configuration, servicesManager, extensionManager }): StudyPrefetcherService => {
|
||||
return new StudyPrefetcherService({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
constructor({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
}: {
|
||||
servicesManager: ServicesManager;
|
||||
extensionManager: ExtensionManager;
|
||||
configuration: StudyPrefetcherConfig;
|
||||
}) {
|
||||
super(EVENTS);
|
||||
|
||||
this._servicesManager = servicesManager;
|
||||
this._extensionManager = extensionManager;
|
||||
this._subscriptions = [];
|
||||
|
||||
Object.assign(this.config, configuration);
|
||||
}
|
||||
|
||||
public onModeEnter(): void {
|
||||
this._addEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* The onModeExit returns the service to the initial state.
|
||||
*/
|
||||
public onModeExit(): void {
|
||||
this._removeEventListeners();
|
||||
this._stopPrefetching();
|
||||
}
|
||||
|
||||
private _addImageLoadingEventsListeners() {
|
||||
const fnOnImageLoadCompleted = (imageId: string) => {
|
||||
// `sendNextRequests` must be called after image loaded/failed events
|
||||
// to make sure prefetch requests shall be sent as soon as the active
|
||||
// displaySets (active viewport) are loaded.
|
||||
//
|
||||
// PS: active display sets are not loaded by this service and that is why
|
||||
// the requests shall not be in the inflight queue.
|
||||
if (!this._inflightRequests.get(imageId)) {
|
||||
this._sendNextRequests();
|
||||
}
|
||||
}
|
||||
|
||||
const fnImageLoadedEventListener = (evt) => {
|
||||
const { image } = evt.detail;
|
||||
const { imageId } = image;
|
||||
|
||||
this._moveImageIdToLoadedSet(imageId);
|
||||
fnOnImageLoadCompleted(imageId);
|
||||
}
|
||||
|
||||
const fnImageLoadFailedEventListener = (evt) => {
|
||||
const { imageId } = evt.detail;
|
||||
|
||||
this._moveImageIdToFailedSet(imageId);
|
||||
fnOnImageLoadCompleted(imageId);
|
||||
}
|
||||
|
||||
return this.imageLoadEventsManager.addEventListeners(
|
||||
fnImageLoadedEventListener,
|
||||
fnImageLoadFailedEventListener
|
||||
);
|
||||
}
|
||||
|
||||
private _addServicesListeners() {
|
||||
const { displaySetService, viewportGridService } = this._servicesManager.services;
|
||||
|
||||
// Restart the prefetcher after any change to the displaySets
|
||||
// (eg: sorting the displaySets on StudyBrowser)
|
||||
const displaySetsChangedSubscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_CHANGED,
|
||||
() => this._syncWithActiveViewport({ forceRestart: true })
|
||||
);
|
||||
|
||||
// Loads new datasets when making a new viewport active
|
||||
const viewportGridActiveViewportIdSubscription = viewportGridService.subscribe(
|
||||
ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
({ viewportId }) => this._syncWithActiveViewport({ activeViewportId: viewportId })
|
||||
);
|
||||
|
||||
// Continue loading datasets after changing the layout (eg: from 1x1 to 2x1)
|
||||
const viewportGridLayoutChangedSubscription = viewportGridService.subscribe(
|
||||
ViewportGridService.EVENTS.LAYOUT_CHANGED,
|
||||
() => this._syncWithActiveViewport()
|
||||
);
|
||||
|
||||
// Loads new datasets after loading a new display set on a viewport
|
||||
const viewportGridStateChangedSubscription = viewportGridService.subscribe(
|
||||
ViewportGridService.EVENTS.GRID_STATE_CHANGED,
|
||||
() => this._syncWithActiveViewport()
|
||||
);
|
||||
|
||||
// Loads the first datasets right after opening the viewer
|
||||
const viewportGridViewportreadySubscription = viewportGridService.subscribe(
|
||||
ViewportGridService.EVENTS.VIEWPORTS_READY,
|
||||
() => {
|
||||
this._syncWithActiveViewport();
|
||||
this._startPrefetching();
|
||||
}
|
||||
);
|
||||
|
||||
return [
|
||||
displaySetsChangedSubscription,
|
||||
viewportGridActiveViewportIdSubscription,
|
||||
viewportGridLayoutChangedSubscription,
|
||||
viewportGridStateChangedSubscription,
|
||||
viewportGridViewportreadySubscription
|
||||
];
|
||||
}
|
||||
|
||||
private _addEventListeners() {
|
||||
const imageLoadingEventsSubscriptions = this._addImageLoadingEventsListeners();
|
||||
const servicesSubscriptions = this._addServicesListeners();
|
||||
|
||||
this._subscriptions.push(...imageLoadingEventsSubscriptions);
|
||||
this._subscriptions.push(...servicesSubscriptions);
|
||||
}
|
||||
|
||||
private _removeEventListeners() {
|
||||
this._subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
this._subscriptions = [];
|
||||
}
|
||||
|
||||
private _syncWithActiveViewport(
|
||||
{
|
||||
activeViewportId,
|
||||
forceRestart
|
||||
}:
|
||||
{
|
||||
activeViewportId?: string,
|
||||
forceRestart?: boolean
|
||||
} = {}) {
|
||||
const { viewportGridService } = this._servicesManager.services;
|
||||
const viewportGridServiceState = viewportGridService.getState();
|
||||
const { viewports } = viewportGridServiceState;
|
||||
|
||||
activeViewportId = activeViewportId ?? viewportGridServiceState.activeViewportId;
|
||||
|
||||
// If may be null when the viewer is loaded
|
||||
if (!activeViewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
const displaySetUpdated = this._setActiveDisplaySetsUIDs(activeViewport.displaySetInstanceUIDs);
|
||||
|
||||
if (forceRestart || displaySetUpdated) {
|
||||
this._restartPrefetching();
|
||||
}
|
||||
}
|
||||
|
||||
private _setActiveDisplaySetsUIDs(newActiveDisplaySetInstanceUIDs: string[]): boolean {
|
||||
const sameDisplaySets =
|
||||
newActiveDisplaySetInstanceUIDs.length === this._activeDisplaySetsInstanceUIDs.length &&
|
||||
newActiveDisplaySetInstanceUIDs.every(uid =>
|
||||
this._activeDisplaySetsInstanceUIDs.includes(uid)
|
||||
);
|
||||
|
||||
if (sameDisplaySets) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._activeDisplaySetsInstanceUIDs = [...newActiveDisplaySetInstanceUIDs];
|
||||
this._restartPrefetching();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _areActiveDisplaySetsLoaded() {
|
||||
const { _activeDisplaySetsInstanceUIDs: displaySetsInstanceUIDs } = this;
|
||||
|
||||
return displaySetsInstanceUIDs.length && displaySetsInstanceUIDs.every(displaySetsInstanceUID =>
|
||||
this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1);
|
||||
}
|
||||
|
||||
private _getClosestDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
|
||||
const sortedDisplaySets = [];
|
||||
let previousIndex = activeDisplaySetIndex - 1;
|
||||
let nextIndex = activeDisplaySetIndex + 1;
|
||||
|
||||
while (previousIndex >= 0 || nextIndex < displaySets.length) {
|
||||
if (previousIndex >= 0) {
|
||||
sortedDisplaySets.push(displaySets[previousIndex]);
|
||||
previousIndex--;
|
||||
}
|
||||
|
||||
if (nextIndex < displaySets.length) {
|
||||
sortedDisplaySets.push(displaySets[nextIndex]);
|
||||
nextIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return sortedDisplaySets;
|
||||
}
|
||||
|
||||
private _getDownwardDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
|
||||
const sortedDisplaySets = [];
|
||||
|
||||
for (let i = activeDisplaySetIndex + 1; i < displaySets.length; i++) {
|
||||
sortedDisplaySets.push(displaySets[i]);
|
||||
}
|
||||
|
||||
return sortedDisplaySets;
|
||||
}
|
||||
|
||||
private _getUpwardDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
|
||||
const sortedDisplaySets = [];
|
||||
|
||||
for (let i = activeDisplaySetIndex - 1; i >= 0 && i !== activeDisplaySetIndex; i--) {
|
||||
sortedDisplaySets.push(displaySets[i]);
|
||||
}
|
||||
|
||||
return sortedDisplaySets;
|
||||
}
|
||||
|
||||
private _getSortedDisplaySetsToPrefetch(displaySets: DisplaySet[]): DisplaySet[] {
|
||||
if (!this._activeDisplaySetsInstanceUIDs?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { displaySetsCount } = this.config;
|
||||
const activeDisplaySetsInstanceUIDs = this._activeDisplaySetsInstanceUIDs;
|
||||
const [activeDisplaySetUID] = activeDisplaySetsInstanceUIDs;
|
||||
const activeDisplaySetIndex = displaySets.findIndex(ds => ds.displaySetInstanceUID === activeDisplaySetUID);
|
||||
const getDisplaySetsFunctionsMap = {
|
||||
[StudyPrefetchOrder.closest]: this._getClosestDisplaySets,
|
||||
[StudyPrefetchOrder.downward]: this._getDownwardDisplaySets,
|
||||
[StudyPrefetchOrder.upward]: this._getUpwardDisplaySets,
|
||||
};
|
||||
const { order } = this.config;
|
||||
const fnGetDisplaySets = getDisplaySetsFunctionsMap[order];
|
||||
|
||||
if (!fnGetDisplaySets) {
|
||||
throw new Error(`Invalid order (${order})`);
|
||||
}
|
||||
|
||||
// Creates a `Set` to look for UIDs in O(1) instead of O(n)
|
||||
const uidsSet = new Set(activeDisplaySetsInstanceUIDs);
|
||||
|
||||
// Remove any active displaySet that may still be in the activeDisplaySetsInstanceUIDs.
|
||||
// That may happen when activeDisplaySetsInstanceUIDs has more than one element.
|
||||
return fnGetDisplaySets.call(this, displaySets, activeDisplaySetIndex).filter(
|
||||
ds => !uidsSet.has(ds.displaySetInstanceUID)
|
||||
).slice(0, displaySetsCount);
|
||||
}
|
||||
|
||||
private _getDisplaySets() {
|
||||
const { displaySetService } = this._servicesManager.services;
|
||||
const displaySets = [...displaySetService.getActiveDisplaySets()];
|
||||
let displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
|
||||
|
||||
return { displaySets, displaySetsToPrefetch };
|
||||
}
|
||||
|
||||
private _updateImageIdsDisplaySetMap(
|
||||
displaySetInstanceUID: string,
|
||||
imageIds: string[]
|
||||
): void {
|
||||
for (const imageId of imageIds) {
|
||||
let displaySetsInstanceUIDsMap = this._imageIdsToDisplaySetsMap.get(imageId);
|
||||
|
||||
if (!displaySetsInstanceUIDsMap) {
|
||||
displaySetsInstanceUIDsMap = new Set();
|
||||
this._imageIdsToDisplaySetsMap.set(imageId, displaySetsInstanceUIDsMap);
|
||||
}
|
||||
|
||||
displaySetsInstanceUIDsMap.add(displaySetInstanceUID);
|
||||
}
|
||||
}
|
||||
|
||||
private _getImageIdsForDisplaySet(displaySet: DisplaySet): string[] {
|
||||
const dataSource = this._extensionManager.getActiveDataSource()[0];
|
||||
|
||||
return dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
}
|
||||
|
||||
private _updateDisplaySetLoadingProgress(displaySetLoadingState: DisplaySetLoadingState) {
|
||||
const { numInstances, loadedImageIds, failedImageIds } = displaySetLoadingState;
|
||||
const loadingProgress = (loadedImageIds.size + failedImageIds.size) / numInstances;
|
||||
|
||||
displaySetLoadingState.loadingProgress = loadingProgress;
|
||||
}
|
||||
|
||||
private _addDisplaySetLoadingState(displaySet: DisplaySet): void {
|
||||
const { displaySetInstanceUID } = displaySet;
|
||||
const imageIds = this._getImageIdsForDisplaySet(displaySet);
|
||||
let displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
|
||||
|
||||
if (displaySetLoadingState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingImageIds = new Set<string>(imageIds);
|
||||
const loadedImageIds = new Set<string>();
|
||||
|
||||
// Needs to check which image is already loaded to update the progress properly
|
||||
// because some images may already be loaded (thumbnails and viewports).
|
||||
for (const imageId of imageIds) {
|
||||
if (this.cache.isImageCached(imageId)) {
|
||||
loadedImageIds.add(imageId);
|
||||
} else {
|
||||
pendingImageIds.add(imageId);
|
||||
}
|
||||
}
|
||||
|
||||
displaySetLoadingState = {
|
||||
displaySetInstanceUID,
|
||||
numInstances: imageIds.length,
|
||||
pendingImageIds,
|
||||
loadedImageIds,
|
||||
failedImageIds: new Set(),
|
||||
loadingProgress: 0
|
||||
};
|
||||
|
||||
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
|
||||
this._displaySetLoadingStates.set(displaySetInstanceUID, displaySetLoadingState);
|
||||
this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds)
|
||||
|
||||
// Notify the UI that something is already loaded (eg: update StudyBrowser)
|
||||
if (loadedImageIds.size) {
|
||||
this._triggerDisplaySetEvents(displaySetInstanceUID);
|
||||
}
|
||||
}
|
||||
|
||||
private _loadDisplaySets() {
|
||||
const { displaySets, displaySetsToPrefetch } = this._getDisplaySets();
|
||||
|
||||
displaySets.forEach(displaySet => this._addDisplaySetLoadingState(displaySet));
|
||||
displaySetsToPrefetch.forEach(displaySet => this._enqueueDisplaySetImagesRequests(displaySet));
|
||||
}
|
||||
|
||||
private _moveImageIdToLoadedSet(imageId: string): boolean {
|
||||
const displaySetsInstanceUIDs = this._imageIdsToDisplaySetsMap.get(imageId);
|
||||
|
||||
if (!displaySetsInstanceUIDs) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const displaySetInstanceUID of Array.from(displaySetsInstanceUIDs.values())) {
|
||||
const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
|
||||
const { pendingImageIds, loadedImageIds } = displaySetLoadingState;
|
||||
|
||||
pendingImageIds.delete(imageId);
|
||||
loadedImageIds.add(imageId);
|
||||
|
||||
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
|
||||
this._triggerDisplaySetEvents(displaySetInstanceUID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _moveImageIdToFailedSet(imageId: string): boolean {
|
||||
const displaySetsInstanceUIDs = this._imageIdsToDisplaySetsMap.get(imageId);
|
||||
|
||||
if (!displaySetsInstanceUIDs) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const displaySetInstanceUID of Array.from(displaySetsInstanceUIDs.values())) {
|
||||
const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
|
||||
const { pendingImageIds, failedImageIds } = displaySetLoadingState;
|
||||
|
||||
pendingImageIds.delete(imageId);
|
||||
failedImageIds.add(imageId);
|
||||
|
||||
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
|
||||
this._triggerDisplaySetEvents(displaySetInstanceUID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _triggerDisplaySetEvents(displaySetInstanceUID: string) {
|
||||
const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
|
||||
const { loadingProgress, numInstances } = displaySetLoadingState;
|
||||
|
||||
this._broadcastEvent(this.EVENTS.DISPLAYSET_LOAD_PROGRESS, {
|
||||
displaySetInstanceUID,
|
||||
numInstances,
|
||||
loadingProgress,
|
||||
});
|
||||
|
||||
if (loadingProgress >= 1) {
|
||||
this._broadcastEvent(this.EVENTS.DISPLAYSET_LOAD_COMPLETE, {
|
||||
displaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _onImagePrefetchSuccess(imageRequest: ImageRequest) {
|
||||
if (imageRequest.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageId } = imageRequest;
|
||||
|
||||
this._inflightRequests.delete(imageId);
|
||||
this._moveImageIdToLoadedSet(imageId);
|
||||
|
||||
// `sendNextRequests` must be called after removing the request from the inflight
|
||||
// queue otherwise it shall not be able to send the request (maxNumPrefetchRequests)
|
||||
this._sendNextRequests();
|
||||
}
|
||||
|
||||
private _onImagePrefetchFailed(imageRequest, error) {
|
||||
if (imageRequest.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`An error ocurred when trying to load "${imageRequest.imageId}"`, error);
|
||||
|
||||
const { imageId } = imageRequest;
|
||||
|
||||
this._inflightRequests.delete(imageId);
|
||||
this._moveImageIdToFailedSet(imageId);
|
||||
|
||||
// `sendNextRequests` must be called after removing the request from the inflight
|
||||
// queue otherwise it shall not be able to send the request (maxNumPrefetchRequests)
|
||||
this._sendNextRequests();
|
||||
}
|
||||
|
||||
private async _sendNextRequests() {
|
||||
// If the service has stopped with async requests in progress this method may
|
||||
// get called again when each of those requests are fulfilled.
|
||||
if (!this._isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Does not send any prefetch request until the active display sets are loaded
|
||||
if (!this._areActiveDisplaySetsLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { _pendingRequests: pendingRequests, _inflightRequests: inflightRequests } = this;
|
||||
const { maxNumPrefetchRequests } = this.config;
|
||||
|
||||
if (!pendingRequests.length || inflightRequests.size >= maxNumPrefetchRequests) {
|
||||
return;
|
||||
}
|
||||
|
||||
const numImageRequests = Math.min(
|
||||
pendingRequests.length,
|
||||
maxNumPrefetchRequests - inflightRequests.size
|
||||
);
|
||||
const imageRequests = this._pendingRequests.splice(0, numImageRequests);
|
||||
|
||||
imageRequests.forEach(imageRequest => {
|
||||
const { imageId } = imageRequest;
|
||||
const options = {
|
||||
priority: -5,
|
||||
requestType: this.requestType,
|
||||
additionalDetails: { imageId },
|
||||
preScale: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.imageLoadPoolManager.addRequest(
|
||||
async () =>
|
||||
this.imageLoader.loadAndCacheImage(imageId, options).then(
|
||||
_image => this._onImagePrefetchSuccess(imageRequest),
|
||||
error => this._onImagePrefetchFailed(imageRequest, error)
|
||||
),
|
||||
this.requestType,
|
||||
{ imageId }
|
||||
);
|
||||
|
||||
inflightRequests.set(imageId, imageRequest);
|
||||
});
|
||||
}
|
||||
|
||||
private _enqueueDisplaySetImagesRequests(displaySet: DisplaySet) {
|
||||
const { displaySetInstanceUID } = displaySet;
|
||||
const imageIds = this._getImageIdsForDisplaySet(displaySet);
|
||||
|
||||
imageIds.forEach(imageId => {
|
||||
if (this.cache.isImageCached(imageId)) {
|
||||
this._moveImageIdToLoadedSet(imageId);
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingRequests.push({
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
aborted: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start prefetching the display sets based on the active viewport and app configuration.
|
||||
*/
|
||||
private _startPrefetching(): void {
|
||||
if (this._isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.enabled) {
|
||||
console.log('StudyPrefetcher is not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
this._isRunning = true;
|
||||
|
||||
this._loadDisplaySets();
|
||||
this._sendNextRequests();
|
||||
this._broadcastEvent(this.EVENTS.SERVICE_STARTED, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop prefetching the display sets.
|
||||
* All internal variables are cleared but activeDisplaySetsInstanceUIDs otherwise restart would not work.
|
||||
*/
|
||||
private _stopPrefetching(): void {
|
||||
if (!this._isRunning) {
|
||||
return;
|
||||
}
|
||||
this._isRunning = false;
|
||||
|
||||
// Mark all inflight requests as aborted before clearing the map.
|
||||
this._inflightRequests.forEach(inflightRequest => (inflightRequest.aborted = true));
|
||||
|
||||
this._pendingRequests = [];
|
||||
this._displaySetLoadingStates.clear();
|
||||
this._imageIdsToDisplaySetsMap.clear();
|
||||
this._inflightRequests.clear();
|
||||
this.imageLoadPoolManager.clearRequestStack(IMAGE_REQUEST_TYPE);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.SERVICE_STOPPED, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart prefetching in case it is already running.
|
||||
*/
|
||||
private _restartPrefetching(): void {
|
||||
if (this._isRunning) {
|
||||
this._stopPrefetching();
|
||||
this._startPrefetching();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { StudyPrefetcherService as default, StudyPrefetcherService };
|
||||
@ -0,0 +1,3 @@
|
||||
import { StudyPrefetcherService } from './StudyPrefetcherService';
|
||||
|
||||
export { StudyPrefetcherService as default, StudyPrefetcherService };
|
||||
@ -105,8 +105,8 @@ class ViewportGridService extends PubSubService {
|
||||
this.setDisplaySetsForViewports([props]);
|
||||
}
|
||||
|
||||
public setDisplaySetsForViewports(props) {
|
||||
this.serviceImplementation._setDisplaySetsForViewports(props);
|
||||
public async setDisplaySetsForViewports(props) {
|
||||
await this.serviceImplementation._setDisplaySetsForViewports(props);
|
||||
const state = this.getState();
|
||||
const viewports = [];
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import CustomizationService from './CustomizationService';
|
||||
import StateSyncService from './StateSyncService';
|
||||
import PanelService from './PanelService';
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
import StudyPrefetcherService from './StudyPrefetcherService';
|
||||
|
||||
import type Services from '../types/Services';
|
||||
|
||||
@ -42,4 +43,5 @@ export {
|
||||
UserAuthenticationService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
};
|
||||
|
||||
@ -14,6 +14,7 @@ import UserAuthenticationServiceType from '../services/UserAuthenticationService
|
||||
import PanelServiceType from '../services/PanelService';
|
||||
import UIDialogServiceType from '../services/UIDialogService';
|
||||
import UIViewportDialogServiceType from '../services/UIViewportDialogService';
|
||||
import StudyPrefetcherServiceType from '../services/StudyPrefetcherService';
|
||||
|
||||
import ServicesManagerType from '../services/ServicesManager';
|
||||
import CommandsManagerType from '../classes/CommandsManager';
|
||||
@ -41,6 +42,7 @@ declare global {
|
||||
export type UIDialogService = UIDialogServiceType;
|
||||
export type UIViewportDialogService = UIViewportDialogServiceType;
|
||||
export type PanelService = PanelServiceType;
|
||||
export type StudyPrefetcherService = StudyPrefetcherServiceType;
|
||||
|
||||
export interface Managers {
|
||||
servicesManager?: ServicesManager;
|
||||
@ -64,6 +66,7 @@ declare global {
|
||||
uiDialogService?: UIDialogServiceType;
|
||||
uiViewportDialogService?: UIViewportDialogServiceType;
|
||||
panelService?: PanelServiceType;
|
||||
studyPrefetcherService?: StudyPrefetcherServiceType;
|
||||
}
|
||||
export interface Config {
|
||||
routerBasename?: string;
|
||||
@ -119,6 +122,12 @@ declare global {
|
||||
onConfiguration?: (dicomWebConfig: any, options: any) => any;
|
||||
dataSources?: any;
|
||||
oidc?: any;
|
||||
studyPrefetcher: {
|
||||
enabled: boolean;
|
||||
displaySetsCount: number;
|
||||
maxNumPrefetchRequests: number;
|
||||
order: 'closest' | 'downward' | 'upward';
|
||||
}
|
||||
}
|
||||
|
||||
export interface Test {
|
||||
|
||||
@ -258,6 +258,35 @@ This will result in two panels, one with `dicomSeg.panel` and `tracked.measureme
|
||||
|
||||
:::
|
||||
|
||||
### Study Prefetcher
|
||||
|
||||
You can enable the study prefetcher so that OHIF loads the next/previous series/display sets
|
||||
based on the proximity to the current series/display set. This can be useful to improve the user experience
|
||||
|
||||
|
||||
```js
|
||||
studyPrefetcher: {
|
||||
/* Enable/disable study prefetching service (default: false) */
|
||||
enabled: true,
|
||||
/* Number of displaysets to be prefetched (default: 2)*/
|
||||
displaySetCount: 2,
|
||||
/**
|
||||
* Max number of concurrent prefetch requests (default: 10)
|
||||
* High numbers may impact on the time to load a new dropped series because
|
||||
* the browser will be busy with all prefetching requests. As soon as the
|
||||
* prefetch requests get fulfilled the new ones from the new dropped series
|
||||
* are sent to the server.
|
||||
*
|
||||
* TODO: abort all prefetch requests when a new series is loaded on a viewport.
|
||||
* (need to add support for `AbortController` on Cornerstone)
|
||||
* */
|
||||
maxNumPrefetchRequests: 10,
|
||||
/* Display sets loading order (closest (deafult), downward or upward) */
|
||||
order: 'closest',
|
||||
},
|
||||
|
||||
```
|
||||
|
||||
### More on Accept Header Configuration
|
||||
In the previous section we showed that you can modify the `acceptHeader`
|
||||
configuration to request specific dicom transfer syntax. By default
|
||||
|
||||
@ -102,7 +102,7 @@ There are various configurations available to customize the viewer. Each configu
|
||||
OHIF v3 has a new configuration structure. The main difference is that the `servers` is renamed to `dataSources` and the configuration is now asynchronous. Datasources are more abstract and
|
||||
far more capable than servers. Read more about dataSources [here](../platform/extensions/modules/data-source.md).
|
||||
|
||||
- `StudyPrefetcher` is not currently supported in OHIF v3.
|
||||
- `StudyPrefetcher` is only available in OHIF v3.9 beta and will be available in the next stable 3.9 release.
|
||||
- The `servers` object has been replaced with a `dataSources` array containing objects representing different data sources.
|
||||
- The cornerstoneExtensionConfig property has been removed, you should use `customizationService` instead (you can read more [here](../platform/services/ui/customization-service.md))
|
||||
- The maxConcurrentMetadataRequests property has been removed in favor of `maxNumRequests`
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/react-hooks": "^3.2.1",
|
||||
"@testing-library/react": "^13.1.0",
|
||||
"browser-detect": "^0.2.28",
|
||||
"classnames": "^2.3.2",
|
||||
"d3-array": "3",
|
||||
@ -56,7 +56,7 @@
|
||||
"react-modal": "3.11.2",
|
||||
"react-outside-click-handler": "^1.3.0",
|
||||
"react-select": "5.7.4",
|
||||
"react-test-renderer": "^16.12.0",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"react-window": "^1.8.9",
|
||||
"react-with-direction": "^1.3.1",
|
||||
"swiper": "^8.4.2",
|
||||
|
||||
8
platform/ui/src/assets/icons/database.svg
Normal file
8
platform/ui/src/assets/icons/database.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="19" height="19" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="matrix(0.84873874,0,0,0.84873874,0.13586772,0.14249413)">
|
||||
<ellipse cx="11" cy="4" rx="9" ry="3"/>
|
||||
<path d="m 2,4 v 14 a 9,3 0 0 0 18,0 V 4" />
|
||||
<path d="m 2,11 a 9,3 0 0 0 18,0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 474 B |
@ -9,6 +9,7 @@ import { ReactComponent as calendar } from './../../assets/icons/calendar.svg';
|
||||
import { ReactComponent as cancel } from './../../assets/icons/cancel.svg';
|
||||
import { ReactComponent as clipboard } from './../../assets/icons/clipboard.svg';
|
||||
import { ReactComponent as close } from './../../assets/icons/closeIcon.svg';
|
||||
import { ReactComponent as database } from './../../assets/icons/database.svg';
|
||||
import { ReactComponent as dottedCircle } from './../../assets/icons/dotted-circle.svg';
|
||||
import { ReactComponent as circledCheckmark } from './../../assets/icons/circled-checkmark.svg';
|
||||
import { ReactComponent as chevronDown } from './../../assets/icons/chevron-down.svg';
|
||||
@ -214,6 +215,7 @@ const ICONS = {
|
||||
cancel: cancel,
|
||||
clipboard: clipboard,
|
||||
close: close,
|
||||
database: database,
|
||||
'dotted-circle': dottedCircle,
|
||||
'circled-checkmark': circledCheckmark,
|
||||
'chevron-down': chevronDown,
|
||||
|
||||
@ -17,6 +17,7 @@ const Thumbnail = ({
|
||||
description,
|
||||
seriesNumber,
|
||||
numInstances,
|
||||
loadingProgress,
|
||||
countIcon,
|
||||
messages,
|
||||
dragData = {},
|
||||
@ -94,6 +95,17 @@ const Thumbnail = ({
|
||||
/>
|
||||
{` ${numInstances}`}
|
||||
</div>
|
||||
<div className="flex mr-2 last:mr-0">
|
||||
{loadingProgress && loadingProgress < 1 && (
|
||||
<>{Math.round(loadingProgress * 100)}%</>
|
||||
)}
|
||||
{loadingProgress && loadingProgress === 1 && (
|
||||
<Icon
|
||||
name={'database'}
|
||||
className="w-3"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DisplaySetMessageListTooltip
|
||||
messages={messages}
|
||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||
@ -124,6 +136,7 @@ Thumbnail.propTypes = {
|
||||
description: PropTypes.string.isRequired,
|
||||
seriesNumber: StringNumber.isRequired,
|
||||
numInstances: PropTypes.number.isRequired,
|
||||
loadingProgress: PropTypes.number,
|
||||
messages: PropTypes.object,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
|
||||
@ -25,6 +25,7 @@ const ThumbnailList = ({
|
||||
dragData,
|
||||
seriesNumber,
|
||||
numInstances,
|
||||
loadingProgress,
|
||||
modality,
|
||||
componentType,
|
||||
seriesDate,
|
||||
@ -66,6 +67,7 @@ const ThumbnailList = ({
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
numInstances={numInstances}
|
||||
loadingProgress={loadingProgress}
|
||||
countIcon={countIcon}
|
||||
imageSrc={imageSrc}
|
||||
imageAltText={imageAltText}
|
||||
|
||||
@ -18,6 +18,7 @@ function ThumbnailTracked({
|
||||
description,
|
||||
seriesNumber,
|
||||
numInstances,
|
||||
loadingProgress,
|
||||
countIcon,
|
||||
messages,
|
||||
dragData,
|
||||
@ -87,6 +88,7 @@ function ThumbnailTracked({
|
||||
messages={messages}
|
||||
numInstances={numInstances}
|
||||
countIcon={countIcon}
|
||||
loadingProgress={loadingProgress}
|
||||
isActive={isActive}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
@ -114,6 +116,7 @@ ThumbnailTracked.propTypes = {
|
||||
description: PropTypes.string.isRequired,
|
||||
seriesNumber: StringNumber.isRequired,
|
||||
numInstances: PropTypes.number.isRequired,
|
||||
loadingProgress: PropTypes.number,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onDoubleClick: PropTypes.func.isRequired,
|
||||
onClickUntrack: PropTypes.func.isRequired,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useSessionStorage from './useSessionStorage';
|
||||
|
||||
const SESSION_STORAGE_KEY = 'test';
|
||||
|
||||
214
yarn.lock
214
yarn.lock
@ -2066,7 +2066,7 @@
|
||||
"@docusaurus/theme-search-algolia" "2.4.3"
|
||||
"@docusaurus/types" "2.4.3"
|
||||
|
||||
"@docusaurus/react-loadable@5.5.2":
|
||||
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||
@ -5086,13 +5086,28 @@
|
||||
dependencies:
|
||||
defer-to-connect "^2.0.1"
|
||||
|
||||
"@testing-library/react-hooks@^3.2.1":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.7.0.tgz#6d75c5255ef49bce39b6465bf6b49e2dac84919e"
|
||||
integrity sha512-TwfbY6BWtWIHitjT05sbllyLIProcysC0dF0q1bbDa7OHLC6A6rJOYJwZ13hzfz3O4RtOuInmprBozJRyyo7/g==
|
||||
"@testing-library/dom@^8.5.0":
|
||||
version "8.20.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f"
|
||||
integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/aria-query" "^5.0.1"
|
||||
aria-query "5.1.3"
|
||||
chalk "^4.1.0"
|
||||
dom-accessibility-api "^0.5.9"
|
||||
lz-string "^1.5.0"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/react@^13.1.0":
|
||||
version "13.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966"
|
||||
integrity sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/testing-library__react-hooks" "^3.4.0"
|
||||
"@testing-library/dom" "^8.5.0"
|
||||
"@types/react-dom" "^18.0.0"
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
@ -5117,6 +5132,11 @@
|
||||
"@tufjs/canonical-json" "1.0.0"
|
||||
minimatch "^9.0.0"
|
||||
|
||||
"@types/aria-query@^5.0.1":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
|
||||
integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
|
||||
|
||||
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.18.0":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
|
||||
@ -5536,6 +5556,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/react-dom@^18.0.0":
|
||||
version "18.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
|
||||
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-config@*", "@types/react-router-config@^5.0.6":
|
||||
version "5.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.11.tgz#2761a23acc7905a66a94419ee40294a65aaa483a"
|
||||
@ -5562,13 +5589,6 @@
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-test-renderer@*":
|
||||
version "18.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.3.0.tgz#839502eae70058a4ae161f63385a8e7929cef4c0"
|
||||
integrity sha512-HW4MuEYxfDbOHQsVlY/XtOvNHftCVEPhJF2pQXXwcUiUF+Oyb0usgp48HSgpK5rt8m9KZb22yqOeZm+rrVG8gw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.4.0":
|
||||
version "4.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac"
|
||||
@ -5669,13 +5689,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.12.tgz#bc2cab12e87978eee89fb21576b670350d6d86ab"
|
||||
integrity sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==
|
||||
|
||||
"@types/testing-library__react-hooks@^3.4.0":
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.1.tgz#b8d7311c6c1f7db3103e94095fe901f8fef6e433"
|
||||
integrity sha512-G4JdzEcq61fUyV6wVW9ebHWEiLK2iQvaBuCHHn9eMSbZzVh4Z4wHnUGIvQOYCCYeu5DnUtFyNYuAAgbSaO/43Q==
|
||||
dependencies:
|
||||
"@types/react-test-renderer" "*"
|
||||
|
||||
"@types/tough-cookie@*":
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
|
||||
@ -6439,6 +6452,13 @@ aria-hidden@^1.1.1:
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
aria-query@5.1.3:
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e"
|
||||
integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==
|
||||
dependencies:
|
||||
deep-equal "^2.0.5"
|
||||
|
||||
aria-query@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
|
||||
@ -6446,7 +6466,7 @@ aria-query@^5.3.0:
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
array-buffer-byte-length@^1.0.1:
|
||||
array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f"
|
||||
integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==
|
||||
@ -9028,6 +9048,30 @@ deep-equal@^1.0.1:
|
||||
object-keys "^1.1.1"
|
||||
regexp.prototype.flags "^1.5.1"
|
||||
|
||||
deep-equal@^2.0.5:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1"
|
||||
integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==
|
||||
dependencies:
|
||||
array-buffer-byte-length "^1.0.0"
|
||||
call-bind "^1.0.5"
|
||||
es-get-iterator "^1.1.3"
|
||||
get-intrinsic "^1.2.2"
|
||||
is-arguments "^1.1.1"
|
||||
is-array-buffer "^3.0.2"
|
||||
is-date-object "^1.0.5"
|
||||
is-regex "^1.1.4"
|
||||
is-shared-array-buffer "^1.0.2"
|
||||
isarray "^2.0.5"
|
||||
object-is "^1.1.5"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.4"
|
||||
regexp.prototype.flags "^1.5.1"
|
||||
side-channel "^1.0.4"
|
||||
which-boxed-primitive "^1.0.2"
|
||||
which-collection "^1.0.1"
|
||||
which-typed-array "^1.1.13"
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
@ -9414,6 +9458,11 @@ docusaurus-plugin-image-zoom@^1.0.1:
|
||||
medium-zoom "^1.0.6"
|
||||
validate-peer-dependencies "^2.2.0"
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||
|
||||
dom-converter@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
|
||||
@ -9852,6 +9901,21 @@ es-errors@^1.2.1, es-errors@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-get-iterator@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
|
||||
integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
get-intrinsic "^1.1.3"
|
||||
has-symbols "^1.0.3"
|
||||
is-arguments "^1.1.1"
|
||||
is-map "^2.0.2"
|
||||
is-set "^2.0.2"
|
||||
is-string "^1.0.7"
|
||||
isarray "^2.0.5"
|
||||
stop-iteration-iterator "^1.0.0"
|
||||
|
||||
es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.19:
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8"
|
||||
@ -11207,7 +11271,7 @@ get-caller-file@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
|
||||
get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
|
||||
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
|
||||
@ -12317,7 +12381,7 @@ inquirer@^8.2.0, inquirer@^8.2.4:
|
||||
through "^2.3.6"
|
||||
wrap-ansi "^6.0.1"
|
||||
|
||||
internal-slot@^1.0.7:
|
||||
internal-slot@^1.0.4, internal-slot@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
|
||||
integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==
|
||||
@ -12407,7 +12471,7 @@ is-arguments@^1.0.4, is-arguments@^1.1.1:
|
||||
call-bind "^1.0.2"
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-array-buffer@^3.0.4:
|
||||
is-array-buffer@^3.0.2, is-array-buffer@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
|
||||
integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==
|
||||
@ -12605,7 +12669,7 @@ is-lambda@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
|
||||
integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==
|
||||
|
||||
is-map@^2.0.3:
|
||||
is-map@^2.0.2, is-map@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
|
||||
integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==
|
||||
@ -12751,7 +12815,7 @@ is-root@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c"
|
||||
integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==
|
||||
|
||||
is-set@^2.0.3:
|
||||
is-set@^2.0.2, is-set@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d"
|
||||
integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==
|
||||
@ -14263,6 +14327,11 @@ lucide-react@^0.379.0:
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.379.0.tgz#29e34eeffae7fb241b64b09868cbe3ab888ef7cc"
|
||||
integrity sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg==
|
||||
|
||||
lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||
|
||||
magic-string@^0.25.0, magic-string@^0.25.1, magic-string@^0.25.2, magic-string@^0.25.7:
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||
@ -17255,7 +17324,7 @@ pretty-format@^24.9.0:
|
||||
ansi-styles "^3.2.0"
|
||||
react-is "^16.8.4"
|
||||
|
||||
pretty-format@^27.0.0, pretty-format@^27.5.1:
|
||||
pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||
@ -17847,7 +17916,12 @@ react-is@18.1.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
|
||||
integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==
|
||||
|
||||
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.8.6:
|
||||
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", "react-is@^17.0.1 || ^18.0.0", react-is@^18.0.0, react-is@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
@ -17857,11 +17931,6 @@ react-is@^17.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
"react-is@^17.0.1 || ^18.0.0", react-is@^18.0.0:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react-json-view@^1.21.3:
|
||||
version "1.21.3"
|
||||
resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.21.3.tgz#f184209ee8f1bf374fb0c41b0813cff54549c475"
|
||||
@ -17897,14 +17966,6 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.3"
|
||||
|
||||
"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-modal@3.11.2:
|
||||
version "3.11.2"
|
||||
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4"
|
||||
@ -18036,6 +18097,14 @@ react-select@5.7.4:
|
||||
react-transition-group "^4.3.0"
|
||||
use-isomorphic-layout-effect "^1.1.2"
|
||||
|
||||
react-shallow-renderer@^16.15.0:
|
||||
version "16.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457"
|
||||
integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==
|
||||
dependencies:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.12.0 || ^17.0.0 || ^18.0.0"
|
||||
|
||||
react-simple-code-editor@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373"
|
||||
@ -18050,15 +18119,14 @@ react-style-singleton@^2.2.1:
|
||||
invariant "^2.2.4"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-test-renderer@^16.12.0:
|
||||
version "16.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae"
|
||||
integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==
|
||||
react-test-renderer@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.3.1.tgz#e693608a1f96283400d4a3afead6893f958b80b4"
|
||||
integrity sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==
|
||||
dependencies:
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
react-is "^16.8.6"
|
||||
scheduler "^0.19.1"
|
||||
react-is "^18.3.1"
|
||||
react-shallow-renderer "^16.15.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-textarea-autosize@^8.3.2:
|
||||
version "8.5.3"
|
||||
@ -18861,14 +18929,6 @@ saxes@^6.0.0:
|
||||
dependencies:
|
||||
xmlchars "^2.2.0"
|
||||
|
||||
scheduler@^0.19.1:
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
|
||||
integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
scheduler@^0.23.2:
|
||||
version "0.23.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||
@ -19626,6 +19686,13 @@ std-env@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
||||
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
|
||||
|
||||
stop-iteration-iterator@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"
|
||||
integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==
|
||||
dependencies:
|
||||
internal-slot "^1.0.4"
|
||||
|
||||
store2@^2.14.2:
|
||||
version "2.14.3"
|
||||
resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.3.tgz#24077d7ba110711864e4f691d2af941ec533deb5"
|
||||
@ -19697,7 +19764,7 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@ -19715,15 +19782,6 @@ string-width@^1.0.1:
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
@ -19810,7 +19868,7 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -19831,13 +19889,6 @@ strip-ansi@^4.0.0:
|
||||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.0, strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
@ -21665,7 +21716,7 @@ which-collection@^1.0.1:
|
||||
is-weakmap "^2.0.2"
|
||||
is-weakset "^2.0.3"
|
||||
|
||||
which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2, which-typed-array@^1.1.9:
|
||||
which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2, which-typed-array@^1.1.9:
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
|
||||
integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==
|
||||
@ -21910,7 +21961,7 @@ worker-loader@3.0.8, worker-loader@^3.0.8:
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^3.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@ -21936,15 +21987,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user