init
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
|
||||
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col text-white"
|
||||
data-cy={'dynamic-volume-panel'}
|
||||
>
|
||||
<PanelGenerateImage
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
></PanelGenerateImage>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicDataPanel;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Button, Icons } from '@ohif/ui-next';
|
||||
import { useSegmentations } from '@ohif/extension-cornerstone';
|
||||
|
||||
function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
|
||||
const segmentations = useSegmentations({ servicesManager });
|
||||
|
||||
if (!segmentations?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Export />
|
||||
<span className="pl-1">Time Data</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Export />
|
||||
<span className="pl-1">ROI Stats</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicExport;
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
PanelSection,
|
||||
ButtonGroup,
|
||||
IconButton,
|
||||
InputNumber,
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { DoubleSlider } from '@ohif/ui-next';
|
||||
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const controlClassNames = {
|
||||
sizeClassName: 'w-[58px] h-[28px]',
|
||||
arrowsDirection: 'horizontal',
|
||||
labelPosition: 'bottom',
|
||||
};
|
||||
|
||||
const Header = ({ title, tooltip }) => (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Tooltip
|
||||
content={<div className="text-white">{tooltip}</div>}
|
||||
position="bottom-left"
|
||||
tight={true}
|
||||
tooltipBoxClassName="max-w-xs p-2"
|
||||
>
|
||||
<Icon
|
||||
name="info-link"
|
||||
className="text-primary-active h-[14px] w-[14px]"
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DynamicVolumeControls = ({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
// fps
|
||||
fps,
|
||||
onFpsChange,
|
||||
minFps,
|
||||
maxFps,
|
||||
// Frames
|
||||
currentFrameIndex,
|
||||
onFrameChange,
|
||||
framesLength,
|
||||
onGenerate,
|
||||
onDoubleRangeChange,
|
||||
onDynamicClick,
|
||||
}) => {
|
||||
const [computedView, setComputedView] = useState(false);
|
||||
|
||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
|
||||
|
||||
const handleSliderChange = newValues => {
|
||||
onDoubleRangeChange(newValues);
|
||||
setSliderRangeValues(newValues);
|
||||
};
|
||||
|
||||
const formatLabel = value => Math.round(value);
|
||||
|
||||
return (
|
||||
<div className="flex select-none flex-col">
|
||||
<PanelSection
|
||||
title="Controls"
|
||||
childrenClassName="space-y-4 pb-5 px-5"
|
||||
>
|
||||
<div className="mt-2">
|
||||
<Header
|
||||
title="View"
|
||||
tooltip={
|
||||
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||
}
|
||||
/>
|
||||
<ButtonGroup className="mt-2 w-full">
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(false);
|
||||
onDynamicClick?.();
|
||||
}}
|
||||
>
|
||||
4D
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(true);
|
||||
}}
|
||||
>
|
||||
Computed
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<FrameControls
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
isPlaying={isPlaying}
|
||||
computedView={computedView}
|
||||
// fps
|
||||
fps={fps}
|
||||
onFpsChange={onFpsChange}
|
||||
minFps={minFps}
|
||||
maxFps={maxFps}
|
||||
//
|
||||
framesLength={framesLength}
|
||||
onFrameChange={onFrameChange}
|
||||
currentFrameIndex={currentFrameIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
|
||||
<Header
|
||||
title="Computed Operation"
|
||||
tooltip={
|
||||
<div>
|
||||
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
|
||||
applied to the data set.
|
||||
<br></br> Range Slider: Choose the numeric range within which the operation will be
|
||||
performed.
|
||||
<br></br>Generate Button: Execute the chosen operation on the specified range of
|
||||
data.{' '}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ButtonGroup
|
||||
className={`mt-2 w-full`}
|
||||
separated={true}
|
||||
>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<div className="mt-2 w-full">
|
||||
<DoubleSlider
|
||||
min={0}
|
||||
max={framesLength - 1}
|
||||
step={1}
|
||||
defaultValue={sliderRangeValues}
|
||||
onValueChange={handleSliderChange}
|
||||
formatLabel={formatLabel}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
|
||||
onClick={() => {
|
||||
onGenerate(computeViewMode);
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicVolumeControls;
|
||||
|
||||
function FrameControls({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
fps,
|
||||
minFps,
|
||||
maxFps,
|
||||
onFpsChange,
|
||||
framesLength,
|
||||
onFrameChange,
|
||||
currentFrameIndex,
|
||||
computedView,
|
||||
}) {
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
return (
|
||||
<div className={computedView && 'ohif-disabled'}>
|
||||
<Header
|
||||
title="4D Controls"
|
||||
tooltip={
|
||||
<div>
|
||||
Play/Pause Button: Begin or pause the animation of the 4D visualization. <br></br> Frame
|
||||
Selector: Navigate through individual frames of the 4D data. <br></br> FPS (Frames Per
|
||||
Second) Selector: Adjust the playback speed of the animation.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="mt-3 flex justify-between">
|
||||
<IconButton
|
||||
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
/>
|
||||
</IconButton>
|
||||
<InputNumber
|
||||
value={currentFrameIndex}
|
||||
onChange={onFrameChange}
|
||||
minValue={0}
|
||||
maxValue={framesLength - 1}
|
||||
label="Frame"
|
||||
{...controlClassNames}
|
||||
/>
|
||||
<InputNumber
|
||||
value={fps}
|
||||
onChange={onFpsChange}
|
||||
minValue={minFps}
|
||||
maxValue={maxFps}
|
||||
{...controlClassNames}
|
||||
label="FPS"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { InputDoubleRange } from '@ohif/ui';
|
||||
import { Select } from '@ohif/ui';
|
||||
import { Button } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const GenerateVolume = ({
|
||||
rangeValues,
|
||||
handleSliderChange,
|
||||
operationsUI,
|
||||
options,
|
||||
handleGenerateOptionsChange,
|
||||
onGenerateImage,
|
||||
returnTo4D,
|
||||
displayingComputedVolume,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mb-2 text-white">Computed Image</div>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mr-2 bg-black text-white "
|
||||
options={operationsUI}
|
||||
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
|
||||
value={options.Operation}
|
||||
onChange={({ value }) => {
|
||||
handleGenerateOptionsChange({
|
||||
Operation: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<InputDoubleRange
|
||||
values={rangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={rangeValues[0] || 1}
|
||||
maxValue={rangeValues[1] || 2}
|
||||
showLabel={true}
|
||||
step={1}
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={onGenerateImage}
|
||||
className="w-1/2"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
<Button
|
||||
onClick={returnTo4D}
|
||||
disabled={!displayingComputedVolume}
|
||||
className="w-1/2"
|
||||
>
|
||||
Return To 4D
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
GenerateVolume.propTypes = {
|
||||
rangeValues: PropTypes.array.isRequired,
|
||||
handleSliderChange: PropTypes.func.isRequired,
|
||||
operationsUI: PropTypes.array.isRequired,
|
||||
options: PropTypes.object.isRequired,
|
||||
handleGenerateOptionsChange: PropTypes.func.isRequired,
|
||||
onGenerateImage: PropTypes.func.isRequired,
|
||||
returnTo4D: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GenerateVolume;
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
|
||||
const SOPClassHandlerId = '@ohif/extension-default.sopClassHandlerModule.stack';
|
||||
|
||||
export default function PanelGenerateImage({ servicesManager, commandsManager }: withAppTypes) {
|
||||
const { cornerstoneViewportService, viewportGridService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const [{ isCineEnabled }, cineService] = useCine();
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
//
|
||||
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
|
||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||
const [frameRate, setFrameRate] = useState(20);
|
||||
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
|
||||
const [timePointRendered, setTimePointRendered] = useState(null);
|
||||
const [displayingComputed, setDisplayingComputed] = useState(false);
|
||||
|
||||
//
|
||||
const uuidComputedVolume = useRef(csUtils.uuidv4());
|
||||
const uuidDynamicVolume = useRef(null);
|
||||
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||
|
||||
useEffect(() => {
|
||||
const viewportDataChangedEvt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
const cineStateChangedEvt = servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED;
|
||||
|
||||
const viewportDataChangedCallback = evtDetails => {
|
||||
evtDetails.viewportData.data.forEach(volumeData => {
|
||||
if (volumeData.volume?.isDynamicVolume()) {
|
||||
setDynamicVolume(volumeData.volume);
|
||||
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||
const newRange = [1, volumeData.volume.numTimePoints];
|
||||
setTimePointsRange(newRange);
|
||||
setTimePointsRangeToUseForGenerate(newRange);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cineStateChangedCallback = evt => {
|
||||
setIsPlaying(evt.isPlaying);
|
||||
};
|
||||
|
||||
const { unsubscribe: unsubscribeViewportData } = cornerstoneViewportService.subscribe(
|
||||
viewportDataChangedEvt,
|
||||
viewportDataChangedCallback
|
||||
);
|
||||
const { unsubscribe: unsubscribeCineState } = servicesManager.services.cineService.subscribe(
|
||||
cineStateChangedEvt,
|
||||
cineStateChangedCallback
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeViewportData();
|
||||
unsubscribeCineState();
|
||||
};
|
||||
}, [cornerstoneViewportService, cineService, servicesManager.services.cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||
|
||||
if (!displaySetUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||
|
||||
if (!dynamicVolumeDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicVolume = cache
|
||||
.getVolumes()
|
||||
.find(volume => volume.volumeId.includes(dynamicVolumeDisplaySet.displaySetInstanceUID));
|
||||
|
||||
if (!dynamicVolume) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDynamicVolume(dynamicVolume);
|
||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||
const newRange = [1, dynamicVolume.numTimePoints];
|
||||
setTimePointsRange(newRange);
|
||||
setTimePointsRangeToUseForGenerate(newRange);
|
||||
}, [
|
||||
activeViewportId,
|
||||
viewportGridService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
cineService,
|
||||
]);
|
||||
|
||||
function renderGeneratedImage(displaySet) {
|
||||
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||
displaySet,
|
||||
});
|
||||
|
||||
setDisplayingComputed(true);
|
||||
}
|
||||
|
||||
function renderDynamicImage(displaySet) {
|
||||
commandsManager.runCommand('swapComputedWithDynamicDisplaySet');
|
||||
}
|
||||
|
||||
// Get computed volume from cache, calculate the data across the time frames,
|
||||
// set the scalar data to the computedVolume, and create displaySet
|
||||
async function onGenerateImage(operationName) {
|
||||
const dynamicVolumeId = dynamicVolume.volumeId;
|
||||
|
||||
if (!dynamicVolumeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let computedVolume = cache.getVolume(computedVolumeId);
|
||||
|
||||
if (!computedVolume) {
|
||||
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
}
|
||||
const [start, end] = timePointsRangeToUseForGenerate;
|
||||
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
|
||||
|
||||
const options = {
|
||||
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
|
||||
targetVolume: computedVolume,
|
||||
};
|
||||
|
||||
cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
|
||||
|
||||
// If computed display set does not exist, create an object to be used as
|
||||
// the displaySet. If it does exist, update the image data and vtkTexture
|
||||
if (!computedDisplaySet) {
|
||||
const displaySet = {
|
||||
volumeLoaderSchema: computedVolume.volumeId.split(':')[0],
|
||||
displaySetInstanceUID: uuidComputedVolume.current,
|
||||
SOPClassHandlerId: SOPClassHandlerId,
|
||||
Modality: dynamicVolume.metadata.Modality,
|
||||
isMultiFrame: false,
|
||||
numImageFrames: 1,
|
||||
uid: uuidComputedVolume.current,
|
||||
referenceDisplaySetUID: dynamicVolume.volumeId.split(':')[1],
|
||||
madeInClient: true,
|
||||
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||
isDerived: true,
|
||||
imageIds: computedVolume.imageIds,
|
||||
};
|
||||
setComputedDisplaySet(displaySet);
|
||||
renderGeneratedImage(displaySet);
|
||||
} else {
|
||||
commandsManager.runCommand('updateVolumeData', {
|
||||
volume: computedVolume,
|
||||
});
|
||||
cornerstoneViewportService.getRenderingEngine().render();
|
||||
renderGeneratedImage(computedDisplaySet);
|
||||
}
|
||||
}
|
||||
|
||||
const onPlayPauseChange = isPlaying => {
|
||||
isPlaying ? handlePlay() : handleStop();
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
setIsPlaying(true);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = viewportInfo;
|
||||
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
setIsPlaying(false);
|
||||
const { element } = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
cineService.stopClip(element);
|
||||
};
|
||||
|
||||
const handleSetFrameRate = newFrameRate => {
|
||||
setFrameRate(newFrameRate);
|
||||
handleStop();
|
||||
handlePlay();
|
||||
};
|
||||
|
||||
function handleSliderChange(newValues) {
|
||||
if (
|
||||
newValues[0] === timePointsRangeToUseForGenerate[0] &&
|
||||
newValues[1] === timePointsRangeToUseForGenerate[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimePointsRangeToUseForGenerate(newValues);
|
||||
}
|
||||
|
||||
if (!dynamicVolume || timePointsRange.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicVolumeControls
|
||||
fps={frameRate}
|
||||
isPlaying={isPlaying}
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
minFps={1}
|
||||
maxFps={50}
|
||||
currentFrameIndex={timePointRendered}
|
||||
onFpsChange={handleSetFrameRate}
|
||||
framesLength={timePointsRange[1]}
|
||||
onFrameChange={timePointIndex => {
|
||||
dynamicVolume.timePointIndex = timePointIndex;
|
||||
}}
|
||||
onGenerate={onGenerateImage}
|
||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||
onDoubleRangeChange={handleSliderChange}
|
||||
initialRangeValues={timePointsRangeToUseForGenerate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import type { ServicesManager } from '@ohif/core';
|
||||
|
||||
function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }) {
|
||||
const ProgressDropdownWithService =
|
||||
servicesManager.services.customizationService.getCustomization(
|
||||
'progressDropdownWithServiceComponent'
|
||||
).component;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy={'workflow-panel'}
|
||||
className="bg-secondary-dark mb-1 px-3 py-4"
|
||||
>
|
||||
<div className="mb-1">Workflow</div>
|
||||
<div>
|
||||
<ProgressDropdownWithService servicesManager={servicesManager} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowPanel;
|
||||
@@ -0,0 +1,5 @@
|
||||
import DynamicDataPanel from './DynamicDataPanel';
|
||||
import WorkflowPanel from './WorkflowPanel';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
|
||||
export { DynamicDataPanel, WorkflowPanel, PanelGenerateImage };
|
||||
Reference in New Issue
Block a user