diff --git a/src/app.css b/src/app.css index 465ddd8f6..3f06cfe39 100644 --- a/src/app.css +++ b/src/app.css @@ -62,29 +62,6 @@ padding-block-end: 0; } -.series-actions { - margin: -1em -1em 1em -1em; - height: 8em; - overflow: hidden; - position: relative; -} - -.action-button-container { - position: absolute; - top: 0; - width: 100%; - height: 100%; - padding: 1em; - text-align: center; -} - -.action-button-container[class~="hover"] { - opacity: 0; - transition: opacity 0.25s ease; - &:hover { - opacity: 1; - } -} .small-button { width: 20px; diff --git a/src/components/Pacs/components/PatientCard.tsx b/src/components/Pacs/components/PatientCard.tsx index e4d579165..5fa718f13 100644 --- a/src/components/Pacs/components/PatientCard.tsx +++ b/src/components/Pacs/components/PatientCard.tsx @@ -9,7 +9,7 @@ import { Tooltip, Skeleton, } from "@patternfly/react-core"; -import StudyCard from "./StudyCardCopy"; +import StudyCard from "./StudyCard"; import { CardHeaderComponent } from "./SettingsComponents"; import { PacsQueryContext } from "../context"; import useSettings from "../useSettings"; diff --git a/src/components/Pacs/components/SeriesCard.tsx b/src/components/Pacs/components/SeriesCard.tsx index d1c04134d..6b825912c 100644 --- a/src/components/Pacs/components/SeriesCard.tsx +++ b/src/components/Pacs/components/SeriesCard.tsx @@ -1,340 +1,235 @@ -import { useEffect, useContext, useCallback, useState, useMemo } from "react"; -import { Steps, Alert } from "antd"; -import { useNavigate } from "react-router"; +import { PACSFile, PACSFileList } from "@fnndsc/chrisapi"; import { + Badge, + Button, Card, CardBody, CardHeader, - Button, - Badge, + HelperText, + HelperTextItem, Modal, - Tooltip, - Skeleton, ModalVariant, + Progress, + ProgressMeasureLocation, + ProgressSize, + ProgressVariant, + Skeleton, + Tooltip, + pluralize, } from "@patternfly/react-core"; -import FileDetailView from "../../Preview/FileDetailView"; -import { DotsIndicator } from "../../Common"; +import { useQuery } from "@tanstack/react-query"; +import { Alert } from "antd"; +import { useContext, useEffect, useMemo, useState, Suspense } from "react"; +import { useNavigate } from "react-router"; import ChrisAPIClient from "../../../api/chrisapiclient"; +import { DotsIndicator } from "../../Common"; +import { + CodeBranchIcon, + DownloadIcon, + LibraryIcon, + PreviewIcon, +} from "../../Icons"; +import FileDetailView from "../../Preview/FileDetailView"; import { PacsQueryContext, Types } from "../context"; +import PFDCMClient, { DataFetchQuery } from "../pfdcmClient"; +import useSettings from "../useSettings"; import { CardHeaderComponent } from "./SettingsComponents"; -import PFDCMClient, { - type DataFetchQuery, - type ImageStatusType, -} from "../pfdcmClient"; -import { QueryStages, getIndex } from "../context"; -import FaEye from "@patternfly/react-icons/dist/esm/icons/eye-icon"; -import FaBranch from "@patternfly/react-icons/dist/esm/icons/code-branch-icon"; -import { pluralize } from "../../../api/common"; -import LibraryIcon from "@patternfly/react-icons/dist/esm/icons/database-icon"; import { MainRouterContext } from "../../../routes"; -import useInterval from "./useInterval"; -import useSettings from "../useSettings"; -const client = new PFDCMClient(); +async function getPACSData( + pacsIdentifier: string, + pullQuery: DataFetchQuery, + additionalParams = {}, +) { + const cubeClient = ChrisAPIClient.getClient(); + try { + const data: PACSFileList = await cubeClient.getPACSFiles({ + pacs_identifier: pacsIdentifier, + ...pullQuery, + ...additionalParams, + }); + return data; + } catch (error) { + throw error; + } +} -const SeriesCard = ({ series }: { series: any }) => { +const SeriesCardCopy = ({ series }: { series: any }) => { + const navigate = useNavigate(); + const { data: userData, isLoading, error: queryError } = useSettings(); + const { state, dispatch } = useContext(PacsQueryContext); + const createFeed = useContext(MainRouterContext).actions.createFeedWithData; + const { selectedPacsService, pullStudy, preview } = state; + const client = new PFDCMClient(); const { SeriesInstanceUID, StudyInstanceUID, NumberOfSeriesRelatedInstances, } = series; - const { data, isLoading, error: queryError } = useSettings(); - const navigate = useNavigate(); - const { state, dispatch } = useContext(PacsQueryContext); - const createFeed = useContext(MainRouterContext).actions.createFeedWithData; - const [cubeFilePreview, setCubeFilePreview] = useState(); - const [fetchNextStatus, setFetchNextStatus] = useState(false); - const [openSeriesPreview, setOpenSeriesPreview] = useState(false); - const [error, setError] = useState(""); - const [stepperStatus, setStepperStatus] = useState([]); - const [currentProgressStep, setCurrentProgressStep] = useState({ - currentStep: "none", - currentProgress: 0, - }); - - const { - seriesUpdate, - selectedPacsService, - preview, - seriesPreviews, - pullStudy, - } = state; - - const userPreferences = data?.series; - - const { currentStep, currentProgress } = currentProgressStep; - - const [requestCounter, setRequestCounter] = useState<{ - [key: string]: number; - }>({}); const [isFetching, setIsFetching] = useState(false); + const [openSeriesPreview, setOpenSeriesPreview] = useState(false); + const [isPreviewFileAvailable, setIsPreviewFileAvailable] = useState(false); - const queryStage = - (seriesUpdate && - Object.keys(seriesUpdate).length > 0 && - seriesUpdate[StudyInstanceUID.value] && - seriesUpdate[StudyInstanceUID.value][SeriesInstanceUID.value]) || - "none"; + const seriesInstances = parseInt(NumberOfSeriesRelatedInstances.value); const pullQuery: DataFetchQuery = useMemo(() => { return { SeriesInstanceUID: SeriesInstanceUID.value, - StudyInstanceUID: StudyInstanceUID.value, }; - }, [SeriesInstanceUID.value, StudyInstanceUID.value]); + }, [SeriesInstanceUID.value]); + + const { data, isPending, isError, error } = useQuery({ + queryKey: ["pacsFiles", SeriesInstanceUID.value], + queryFn: fetchCubeFiles, + refetchInterval: () => { + if (isFetching) return 2000; + return false; + }, + refetchOnMount: true, + }); - const fetchCubeFilePreview = useCallback( - async function fetchCubeSeries() { - const middleValue = Math.floor( - parseInt(NumberOfSeriesRelatedInstances.value) / 2, - ); + useEffect(() => { + if (pullStudy) { + handleRetrieve(); + } + }, [pullStudy]); + + useEffect(() => { + if ( + data && + data.totalFilesCount > 0 && + data.totalFilesCount !== seriesInstances && + !isFetching + ) { + setIsFetching(true); + } + }, [data]); - const cubeClient = ChrisAPIClient.getClient(); + async function fetchCubeFiles() { + try { + const middleValue = Math.floor(seriesInstances / 2); + const files = await getPACSData(selectedPacsService, pullQuery, { + limit: 1, + offset: isPreviewFileAvailable ? middleValue : 0, + }); - try { - const files = await cubeClient.getPACSFiles({ - ...pullQuery, + const seriesRelatedInstance = await getPACSData( + "org.fnndsc.oxidicom", + pullQuery, + { limit: 1, - offset: middleValue, - }); + ProtocolName: "NumberOfSeriesRelatedInstances", + }, + ); - const fileItems = files.getItems(); + const pushCountInstance = await getPACSData( + "org.fnndsc.oxidicom", + pullQuery, + { + limit: 1, + ProtocolName: "OxidicomAttemptedPushCount", + }, + ); + + const seriesCountCheck = seriesRelatedInstance.getItems(); + const pushCountCheck = pushCountInstance.getItems(); + + if (seriesCountCheck && seriesCountCheck.length > 0) { + const seriesCount = +seriesCountCheck[0].data.SeriesDescription; - if (fileItems && fileItems.length > 0) { - setCubeFilePreview(fileItems[0]); - } else { - setError("Failed to locate the dataset in the cube backend"); + if (seriesCount !== seriesInstances) { + throw new Error( + "The number of series related instances in cube does not match the number in pfdcm.", + ); } - } catch { - setError("Could not fetch this file from storage"); } - }, - [pullQuery, NumberOfSeriesRelatedInstances.value], - ); - useEffect(() => { - if (preview && cubeFilePreview) { - dispatch({ - type: Types.SET_SERIES_PREVIEWS, - payload: { - seriesID: SeriesInstanceUID.value, - preview: true, - }, - }); - } else if ( - preview === false && - seriesPreviews && - Object.keys(seriesPreviews).length > 0 - ) { - dispatch({ - type: Types.RESET_SERIES_PREVIEWS, - payload: { - clearSeriesPreview: true, - }, - }); - } - }, [ - preview, - cubeFilePreview, - SeriesInstanceUID.value, - seriesPreviews, - dispatch, - ]); + if (pushCountCheck && pushCountCheck.length > 0) { + const pushCount = +pushCountCheck[0].data.SeriesDescription; - useEffect(() => { - async function fetchStatusForTheFirstTime() { - const stepperStatus = await client.stepperStatus( - pullQuery, - selectedPacsService, - SeriesInstanceUID.value, - NumberOfSeriesRelatedInstances.value, - false, - ); + if (pushCount !== seriesInstances) { + throw new Error( + "The attempted push count does not match the number of series related instances.", + ); + } + } + + const fileItems: PACSFile[] = files.getItems() as never as PACSFile[]; + let fileToPreview: PACSFile | null = null; + if (fileItems) { + fileToPreview = fileItems[0]; + } - const status = stepperStatus.get(SeriesInstanceUID.value); + const totalFilesCount = files.totalCount; - if (status) { - const { progress, newImageStatus } = status; - setStepperStatus(newImageStatus); - setCurrentProgressStep(progress); + if (totalFilesCount >= middleValue) { + // Preview is the middle image of the stack + setIsPreviewFileAvailable(true); + } + if (pullStudy) { dispatch({ - type: Types.SET_SERIES_UPDATE, + type: Types.SET_STUDY_PULL_TRACKER, payload: { - currentStep: progress.currentStep, seriesInstanceUID: SeriesInstanceUID.value, studyInstanceUID: StudyInstanceUID.value, + currentProgress: totalFilesCount === seriesInstances, }, }); - - progress.currentStep === "completed" && (await fetchCubeFilePreview()); - } - } - - if (pullStudy) { - setFetchNextStatus(true); - } else { - fetchStatusForTheFirstTime(); - } - }, [ - fetchCubeFilePreview, - dispatch, - pullQuery, - SeriesInstanceUID.value, - StudyInstanceUID.value, - selectedPacsService, - NumberOfSeriesRelatedInstances.value, - pullStudy, - ]); - - const executeNextStepForTheSeries = async (nextStep: string) => { - try { - if (nextStep === "retrieve") { - await client.findRetrieve(selectedPacsService, pullQuery); } - if (nextStep === "push") { - await client.findPush(selectedPacsService, pullQuery); - } - if (nextStep === "register") { - await client.findRegister(selectedPacsService, pullQuery); + if (totalFilesCount === seriesInstances && isFetching) { + setIsFetching(false); } - } catch (error: any) { - setError(error.message); + return { + fileToPreview, + totalFilesCount, + }; + } catch (error) { + setIsFetching(false); + throw error; } - }; - - function setSeriesUpdate(step: string) { - dispatch({ - type: Types.SET_SERIES_UPDATE, - payload: { - currentStep: step, - seriesInstanceUID: SeriesInstanceUID.value, - studyInstanceUID: series.StudyInstanceUID.value, - }, - }); } - useInterval( - async () => { - if (fetchNextStatus && !isFetching) { - setIsFetching(true); - - try { - const stepperStatus = await client.stepperStatus( - pullQuery, - selectedPacsService, - SeriesInstanceUID.value, - NumberOfSeriesRelatedInstances.value, - currentStep === "none" ? true : false, - ); - - const status = stepperStatus.get(SeriesInstanceUID.value); - - if (status) { - const { progress, newImageStatus } = status; - const { currentStep, currentProgress } = progress; - - setStepperStatus(newImageStatus); - setCurrentProgressStep(progress); - - if (!requestCounter[currentStep]) { - setRequestCounter({ - ...requestCounter, - [currentStep]: 1, - }); - } else if (requestCounter[currentStep] === 1) { - setRequestCounter({ - ...requestCounter, - [currentStep]: requestCounter[currentStep] + 1, - }); - } - - if ( - requestCounter[currentStep] === 1 && - (currentProgress === 0 || currentProgress === 1) - ) { - const index = getIndex(currentStep); - const nextStep = QueryStages[index + 1]; - currentStep !== "completed" && - executeNextStepForTheSeries(nextStep); - setSeriesUpdate(nextStep); - } + const handleRetrieve = async () => { + await client.findRetrieve(selectedPacsService, pullQuery); + setIsFetching(true); + }; - if (currentStep === "completed") { - setSeriesUpdate(currentStep); - await fetchCubeFilePreview(); - setFetchNextStatus(!fetchNextStatus); - setIsFetching(false); - } - } - } catch (error) { - // Handle error if needed - setIsFetching(false); - setFetchNextStatus(!fetchNextStatus); - } finally { - setIsFetching(false); - } - } - }, - fetchNextStatus && pullStudy ? 5000 : fetchNextStatus ? 1000 : null, + const helperText = ( + + {error?.message} + ); - let nextQueryStage = ""; - if (queryStage) { - const index = getIndex(queryStage); - nextQueryStage = QueryStages[index + 1]; - } - - const showProcessingWithButton = - stepperStatus.length > 0 && - ((currentProgress > 0 && fetchNextStatus) || - (fetchNextStatus && currentStep !== "completed")); - - const buttonContainer = ( - <> - {currentStep && - currentStep !== "completed" && - nextQueryStage && - currentProgress === 0 && - stepperStatus.length > 0 && ( - - )} - - {currentStep !== "completed" && - currentProgress > 0 && - fetchNextStatus === false && ( - - )} - + const largeFilePreview = data?.fileToPreview && ( + setOpenSeriesPreview(false)} + > + + ); const filePreviewButton = ( - <> +
+ ); + + const filePreviewLayout = ( + + +
+
+ + {series.SeriesDescription.value} + + {filePreviewButton} +
+
+
); + const userPreferences = userData?.series; const userPreferencesArray = userPreferences && Object.keys(userPreferences); + const retrieveButton = ( +