From c0c6a107e38c9e97f0316a5b5d194f5ea588c484 Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Fri, 6 Jun 2025 12:24:48 -0400 Subject: [PATCH 01/10] initial --- src/libs/ajax/teaspoons/Teaspoons.ts | 6 +++ src/libs/ajax/teaspoons/teaspoons-models.ts | 30 +++++++++++ .../pipelines/views/JobHistory.tsx | 54 ++++++++++++++++--- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/libs/ajax/teaspoons/Teaspoons.ts b/src/libs/ajax/teaspoons/Teaspoons.ts index 2f8f20b8e7..77a3920a18 100644 --- a/src/libs/ajax/teaspoons/Teaspoons.ts +++ b/src/libs/ajax/teaspoons/Teaspoons.ts @@ -5,6 +5,7 @@ import { fetchTeaspoons } from 'src/libs/ajax/ajax-common'; import { GetPipelineRunsResponse, PipelineList, + PipelineRunResponse, PipelineWithDetails, PreparePipelineRunResponse, StartPipelineResponse, @@ -77,6 +78,11 @@ export const Teaspoons = (signal?: AbortSignal) => ({ ); return res.json(); }, + + getPipelineRunResult: async (jobId: string): Promise => { + const res = await fetchTeaspoons(`pipelineruns/v1/result/${jobId}`, _.merge(authOpts(), { signal })); + return res.json(); + }, }); export type TeaspoonsContract = ReturnType; diff --git a/src/libs/ajax/teaspoons/teaspoons-models.ts b/src/libs/ajax/teaspoons/teaspoons-models.ts index 94fadaade3..02bb4789dd 100644 --- a/src/libs/ajax/teaspoons/teaspoons-models.ts +++ b/src/libs/ajax/teaspoons/teaspoons-models.ts @@ -81,4 +81,34 @@ export interface StartPipelineResponse { }; } +export interface PipelineRunResponse { + jobReport: PipelineJobReport; + errorReport?: PipelineRunErrorReport; + pipelineRunReport: PipelineRunReport; +} + +export interface PipelineJobReport { + id: string; + description?: string; + status: PipelineRunStatus; + statusCode?: number; + submitted: string; + completed?: string; + resultURL?: string; +} + +export interface PipelineRunErrorReport { + message: string; + errorCode: number; + causes: string[]; +} + +export interface PipelineRunReport { + pipelineName: string; + pipelineVersion: number; + toolVersion: string; + outputs?: Record; + outputExpirationDate?: string; +} + export type PipelineRunStatus = 'PREPARING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED'; diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.tsx index a7f00e0086..e67374418f 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.tsx @@ -6,7 +6,12 @@ import { AutoSizer } from 'react-virtualized'; import FooterWrapper from 'src/components/FooterWrapper'; import { FlexTable, HeaderCell, Paginator, TooltipCell } from 'src/components/table'; import { Teaspoons } from 'src/libs/ajax/teaspoons/Teaspoons'; -import { GetPipelineRunsResponse, PipelineRun, PipelineRunStatus } from 'src/libs/ajax/teaspoons/teaspoons-models'; +import { + GetPipelineRunsResponse, + PipelineRun, + PipelineRunResponse, + PipelineRunStatus, +} from 'src/libs/ajax/teaspoons/teaspoons-models'; import { useCancellation } from 'src/libs/react-utils'; import { pipelinesTopBar } from 'src/pages/scientificServices/pipelines/common/scientific-services-common'; @@ -244,19 +249,56 @@ const QuotaUsedCell = (props: CellProps): ReactNode => { }; const ActionCell = ({ pipelineRun }: CellProps): ReactNode => { + const signal = useCancellation(); + const [pipelineRunResult, setPipelineRunResult] = useState(); + + async function fetchPipelineRunResults() { + const results = await Teaspoons(signal).getPipelineRunResult(pipelineRun.jobId); + setPipelineRunResult(results); + } + + console.log(pipelineRunResult?.pipelineRunReport.outputs); + // TODO these actions will be implemented in a later ticket return (
{pipelineRun.status === 'SUCCEEDED' && ( - // eslint-disable-next-line jsx-a11y/anchor-is-valid - - Download Output - + <> + {!pipelineRunResult ? ( + { + e.preventDefault(); + fetchPipelineRunResults(); + }} + > + View Outputs + + ) : ( + <> + {pipelineRunResult.pipelineRunReport.outputs && + Object.entries(pipelineRunResult.pipelineRunReport.outputs).map(([key, url]) => ( + + ))} + + )} + )} {pipelineRun.status === 'FAILED' && ( // eslint-disable-next-line jsx-a11y/anchor-is-valid - See Details + View Error )}
From 3cf337552ed797d93b8f53e4ecfefe98053e323b Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Fri, 6 Jun 2025 17:05:33 -0400 Subject: [PATCH 02/10] implement some modals --- .../pipelines/views/JobHistory.tsx | 242 +++++++++++++++--- 1 file changed, 205 insertions(+), 37 deletions(-) diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.tsx index e67374418f..6e6862c551 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.tsx @@ -1,4 +1,4 @@ -import { Icon, Spinner } from '@terra-ui-packages/components'; +import { ButtonPrimary, Icon, Modal, Spinner, useModalHandler } from '@terra-ui-packages/components'; import _, { capitalize } from 'lodash'; import pluralize from 'pluralize'; import React, { ReactNode, useEffect, useRef, useState } from 'react'; @@ -252,54 +252,58 @@ const ActionCell = ({ pipelineRun }: CellProps): ReactNode => { const signal = useCancellation(); const [pipelineRunResult, setPipelineRunResult] = useState(); + const outputsModal = useModalHandler((args: { jobId: string; result: PipelineRunResponse | undefined }, close) => { + return ; + }); + + const errorModal = useModalHandler((args: { jobId: string; result: PipelineRunResponse | undefined }, close) => { + return ; + }); + async function fetchPipelineRunResults() { const results = await Teaspoons(signal).getPipelineRunResult(pipelineRun.jobId); setPipelineRunResult(results); } - console.log(pipelineRunResult?.pipelineRunReport.outputs); - - // TODO these actions will be implemented in a later ticket return (
{pipelineRun.status === 'SUCCEEDED' && ( <> - {!pipelineRunResult ? ( - { - e.preventDefault(); - fetchPipelineRunResults(); - }} - > - View Outputs - - ) : ( - <> - {pipelineRunResult.pipelineRunReport.outputs && - Object.entries(pipelineRunResult.pipelineRunReport.outputs).map(([key, url]) => ( - - ))} - - )} + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + { + e.preventDefault(); + if (!pipelineRunResult) { + await fetchPipelineRunResults(); + } + outputsModal.open({ jobId: pipelineRun.jobId, result: pipelineRunResult }); + }} + > + View Outputs + + {outputsModal.maybeRender()} )} {pipelineRun.status === 'FAILED' && ( - // eslint-disable-next-line jsx-a11y/anchor-is-valid - - View Error - + <> + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + { + e.preventDefault(); + if (!pipelineRunResult) { + await fetchPipelineRunResults(); + } + errorModal.open({ jobId: pipelineRun.jobId, result: pipelineRunResult }); + }} + > + View Error + + {errorModal.maybeRender()} + )}
); @@ -325,3 +329,167 @@ const getRunStatusIcon = (status: PipelineRunStatus): ReactNode => { return
{capitalize(status)}
; } }; + +/** + * Modal component for displaying pipeline outputs + */ +interface OutputsModalProps { + jobId: string; + result: PipelineRunResponse | undefined; + onDismiss: () => void; +} + +const OutputsModal = ({ jobId, result, onDismiss }: OutputsModalProps): ReactNode => { + return ( + +
+

Available Output Files

+ + {!result ? ( +
+ +
Loading outputs...
+
+ ) : ( +
+ {result.pipelineRunReport.outputs && Object.entries(result.pipelineRunReport.outputs).length > 0 ? ( +
+ {Object.entries(result.pipelineRunReport.outputs).map(([key, url]) => ( +
+
{key}
+ { + window.open(url, '_blank'); + }} + style={{ marginLeft: '1rem' }} + > +
+ + Download +
+
+
+ ))} +
+ ) : ( +
No output files found for this job.
+ )} +
+ )} + +
+ Close +
+
+
+ ); +}; + +/** + * Modal component for displaying pipeline errors + */ +interface ErrorModalProps { + jobId: string; + result: PipelineRunResponse | undefined; + onDismiss: () => void; +} + +const ErrorModal = ({ jobId, result, onDismiss }: ErrorModalProps): ReactNode => { + return ( + +
+

Error Details

+ + {!result ? ( +
+ +
Loading error details...
+
+ ) : ( +
+ {result.errorReport ? ( +
+
+
Error Message:
+
{result.errorReport.message}
+
+ + {result.errorReport.causes && result.errorReport.causes.length > 0 && ( +
+
Error Causes:
+
+ {result.errorReport.causes.map((cause, index) => ( + // eslint-disable-next-line react/no-array-index-key +
+ {cause} +
+ ))} +
+
+ )} + +
+
Error Code:
+
{result.errorReport.errorCode}
+
+
+ ) : ( +
+ No detailed error information available for this job. +
+ )} +
+ )} + +
+ Close +
+
+
+ ); +}; From dfa9e0b989d07d92bd7582350e1cf449da34e523 Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Mon, 9 Jun 2025 10:52:05 -0400 Subject: [PATCH 03/10] more --- .../pipelines/views/JobHistory.tsx | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.tsx index 6e6862c551..30f917347b 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.tsx @@ -387,6 +387,28 @@ const OutputsModal = ({ jobId, result, onDismiss }: OutputsModalProps): ReactNod ))} + {result.pipelineRunReport.outputExpirationDate && ( +
+ All output files for this job will be automatically deleted on{' '} + + {new Date(result.pipelineRunReport.outputExpirationDate).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + . Please download them before this date. +
+ )} ) : (
No output files found for this job.
@@ -466,11 +488,6 @@ const ErrorModal = ({ jobId, result, onDismiss }: ErrorModalProps): ReactNode => )} - -
-
Error Code:
-
{result.errorReport.errorCode}
-
) : (
From 707a45ab55e3f96f029f58415b0a608c672a0db1 Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Mon, 9 Jun 2025 16:50:22 -0400 Subject: [PATCH 04/10] break modals out --- .../ScientificServicesDescription.tsx | 9 +- .../pipelines/views/JobHistory.tsx | 228 +++--------------- .../pipelines/views/modals/ViewErrorModal.tsx | 84 +++++++ .../views/modals/ViewOutputsModal.tsx | 103 ++++++++ 4 files changed, 228 insertions(+), 196 deletions(-) create mode 100644 src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx create mode 100644 src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx diff --git a/src/pages/scientificServices/landingPage/ScientificServicesDescription.tsx b/src/pages/scientificServices/landingPage/ScientificServicesDescription.tsx index 20536ed36d..e5849403cf 100644 --- a/src/pages/scientificServices/landingPage/ScientificServicesDescription.tsx +++ b/src/pages/scientificServices/landingPage/ScientificServicesDescription.tsx @@ -15,7 +15,14 @@ export const ScientificServicesDescription = () => {
Our current offerings:
- All of Us + AnVIL Imputation Service + + All of Us + AnVIL Imputation Service +
First time using this service?
{buttonVisible && ( diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.tsx index 30f917347b..92b0629a3e 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.tsx @@ -1,4 +1,5 @@ -import { ButtonPrimary, Icon, Modal, Spinner, useModalHandler } from '@terra-ui-packages/components'; +import { Icon, Spinner, useModalHandler } from '@terra-ui-packages/components'; +import { formatDate, formatDatetime } from '@terra-ui-packages/core-utils'; import _, { capitalize } from 'lodash'; import pluralize from 'pluralize'; import React, { ReactNode, useEffect, useRef, useState } from 'react'; @@ -14,6 +15,8 @@ import { } from 'src/libs/ajax/teaspoons/teaspoons-models'; import { useCancellation } from 'src/libs/react-utils'; import { pipelinesTopBar } from 'src/pages/scientificServices/pipelines/common/scientific-services-common'; +import { ViewErrorModal } from 'src/pages/scientificServices/pipelines/views/modals/ViewErrorModal'; +import { ViewOutputsModal } from 'src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal'; /* Right now, this will show all pipeline runs. Once we support more than one pipeline, @@ -52,7 +55,9 @@ export const JobHistory = () => { >

Job History

-
All files associated with jobs will be automatically deleted after 2 weeks from completed.
+
+ All files associated with jobs will be automatically deleted after 2 weeks from completed. +
For support, email{' '} { cellRenderer: ({ rowIndex }) => { return ; }, - size: { basis: 90 }, + size: { basis: 80 }, }, { field: 'completed', @@ -151,7 +156,15 @@ const getColumns = (paginatedRuns: PipelineRun[]) => { cellRenderer: ({ rowIndex }) => { return ; }, - size: { basis: 90 }, + size: { basis: 80 }, + }, + { + field: 'dataDeletionDate', + headerRenderer: () => Deletion Date, + cellRenderer: ({ rowIndex }) => { + return ; + }, + size: { basis: 80 }, }, { field: 'quotaUsed', @@ -219,15 +232,8 @@ const StatusCell = ({ pipelineRun }: CellProps): ReactNode => { return
{getRunStatusIcon(pipelineRun.status)}
; }; -/** Format date like "Feb 15, 2025", and enable tooltip with precise time */ const MediumDateWithTooltip = ({ date }: { date: string | Date }): React.JSX.Element => { - const mediumDate = new Date(date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); - - return {mediumDate}; + return {formatDate(date)}; }; const SubmittedCell = ({ pipelineRun }: CellProps): ReactNode => { @@ -238,6 +244,19 @@ const CompletedCell = ({ pipelineRun }: CellProps): ReactNode => { return
{pipelineRun.timeCompleted ? : ''}
; }; +const DataDeletionDateCell = ({ pipelineRun }: CellProps): ReactNode => { + if (!pipelineRun.timeCompleted || !(pipelineRun.status === 'SUCCEEDED')) { + return
N/A
; + } + + // Calculate deletion date as 14 days after completion + const completionDate = new Date(pipelineRun?.timeCompleted); + const deletionDate = new Date(completionDate); + deletionDate.setDate(deletionDate.getDate() + 14); + + return ; +}; + const QuotaUsedCell = (props: CellProps): ReactNode => { return (
@@ -253,11 +272,11 @@ const ActionCell = ({ pipelineRun }: CellProps): ReactNode => { const [pipelineRunResult, setPipelineRunResult] = useState(); const outputsModal = useModalHandler((args: { jobId: string; result: PipelineRunResponse | undefined }, close) => { - return ; + return ; }); const errorModal = useModalHandler((args: { jobId: string; result: PipelineRunResponse | undefined }, close) => { - return ; + return ; }); async function fetchPipelineRunResults() { @@ -329,184 +348,3 @@ const getRunStatusIcon = (status: PipelineRunStatus): ReactNode => { return
{capitalize(status)}
; } }; - -/** - * Modal component for displaying pipeline outputs - */ -interface OutputsModalProps { - jobId: string; - result: PipelineRunResponse | undefined; - onDismiss: () => void; -} - -const OutputsModal = ({ jobId, result, onDismiss }: OutputsModalProps): ReactNode => { - return ( - -
-

Available Output Files

- - {!result ? ( -
- -
Loading outputs...
-
- ) : ( -
- {result.pipelineRunReport.outputs && Object.entries(result.pipelineRunReport.outputs).length > 0 ? ( -
- {Object.entries(result.pipelineRunReport.outputs).map(([key, url]) => ( -
-
{key}
- { - window.open(url, '_blank'); - }} - style={{ marginLeft: '1rem' }} - > -
- - Download -
-
-
- ))} - {result.pipelineRunReport.outputExpirationDate && ( -
- All output files for this job will be automatically deleted on{' '} - - {new Date(result.pipelineRunReport.outputExpirationDate).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} - - . Please download them before this date. -
- )} -
- ) : ( -
No output files found for this job.
- )} -
- )} - -
- Close -
-
-
- ); -}; - -/** - * Modal component for displaying pipeline errors - */ -interface ErrorModalProps { - jobId: string; - result: PipelineRunResponse | undefined; - onDismiss: () => void; -} - -const ErrorModal = ({ jobId, result, onDismiss }: ErrorModalProps): ReactNode => { - return ( - -
-

Error Details

- - {!result ? ( -
- -
Loading error details...
-
- ) : ( -
- {result.errorReport ? ( -
-
-
Error Message:
-
{result.errorReport.message}
-
- - {result.errorReport.causes && result.errorReport.causes.length > 0 && ( -
-
Error Causes:
-
- {result.errorReport.causes.map((cause, index) => ( - // eslint-disable-next-line react/no-array-index-key -
- {cause} -
- ))} -
-
- )} -
- ) : ( -
- No detailed error information available for this job. -
- )} -
- )} - -
- Close -
-
-
- ); -}; diff --git a/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx b/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx new file mode 100644 index 0000000000..abed59327d --- /dev/null +++ b/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx @@ -0,0 +1,84 @@ +import { ButtonPrimary, Modal, Spinner } from '@terra-ui-packages/components'; +import React, { ReactNode } from 'react'; +import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models'; + +/** + * Modal component for displaying pipeline errors + */ +interface ErrorModalProps { + jobId: string; + result: PipelineRunResponse | undefined; + onDismiss: () => void; +} + +export const ViewErrorModal = ({ jobId, result, onDismiss }: ErrorModalProps): ReactNode => { + return ( + +
+

Error Details

+ + {!result ? ( +
+ +
Loading error details...
+
+ ) : ( +
+ {result.errorReport ? ( +
+
+
Error Message:
+
{result.errorReport.message}
+
+ + {result.errorReport.causes && result.errorReport.causes.length > 0 && ( +
+
Error Causes:
+
+ {result.errorReport.causes.map((cause, index) => ( + // eslint-disable-next-line react/no-array-index-key +
+ {cause} +
+ ))} +
+
+ )} +
+ ) : ( +
+ No detailed error information available for this job. +
+ )} +
+ )} + +
+ Close +
+
+
+ ); +}; diff --git a/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx b/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx new file mode 100644 index 0000000000..1565b58405 --- /dev/null +++ b/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx @@ -0,0 +1,103 @@ +import { ButtonPrimary, Icon, Modal, Spinner } from '@terra-ui-packages/components'; +import React, { ReactNode } from 'react'; +import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models'; + +/** + * Modal component for displaying pipeline outputs + */ +interface OutputsModalProps { + jobId: string; + result: PipelineRunResponse | undefined; + onDismiss: () => void; +} + +export const ViewOutputsModal = ({ jobId, result, onDismiss }: OutputsModalProps): ReactNode => { + return ( + +
+

Available Output Files

+ + {!result ? ( +
+ +
Loading outputs...
+
+ ) : ( +
+ {result.pipelineRunReport.outputs && Object.entries(result.pipelineRunReport.outputs).length > 0 ? ( +
+ {Object.entries(result.pipelineRunReport.outputs).map(([key, url]) => ( +
+
{key}
+ { + window.open(url, '_blank'); + }} + style={{ marginLeft: '1rem' }} + > +
+ + Download +
+
+
+ ))} + {result.pipelineRunReport.outputExpirationDate && ( +
+ All output files for this job will be automatically deleted on{' '} + + {new Date(result.pipelineRunReport.outputExpirationDate).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + . Please download them before this date. +
+ )} +
+ ) : ( +
No output files found for this job.
+ )} +
+ )} + +
+ Close +
+
+
+ ); +}; From 7a1220ae3686f0f1f5e7017c276b978a53e35137 Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Mon, 9 Jun 2025 17:25:59 -0400 Subject: [PATCH 05/10] move control of loading to modal --- .../pipelines/views/JobHistory.tsx | 73 ++++++++----------- .../pipelines/views/modals/ViewErrorModal.tsx | 29 ++++++-- .../views/modals/ViewOutputsModal.tsx | 29 ++++++-- 3 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.tsx index 92b0629a3e..d9101309f0 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.tsx @@ -7,12 +7,7 @@ import { AutoSizer } from 'react-virtualized'; import FooterWrapper from 'src/components/FooterWrapper'; import { FlexTable, HeaderCell, Paginator, TooltipCell } from 'src/components/table'; import { Teaspoons } from 'src/libs/ajax/teaspoons/Teaspoons'; -import { - GetPipelineRunsResponse, - PipelineRun, - PipelineRunResponse, - PipelineRunStatus, -} from 'src/libs/ajax/teaspoons/teaspoons-models'; +import { GetPipelineRunsResponse, PipelineRun, PipelineRunStatus } from 'src/libs/ajax/teaspoons/teaspoons-models'; import { useCancellation } from 'src/libs/react-utils'; import { pipelinesTopBar } from 'src/pages/scientificServices/pipelines/common/scientific-services-common'; import { ViewErrorModal } from 'src/pages/scientificServices/pipelines/views/modals/ViewErrorModal'; @@ -232,6 +227,7 @@ const StatusCell = ({ pipelineRun }: CellProps): ReactNode => { return
{getRunStatusIcon(pipelineRun.status)}
; }; +/** Format date like "Feb 15, 2025", and enable tooltip with precise time */ const MediumDateWithTooltip = ({ date }: { date: string | Date }): React.JSX.Element => { return {formatDate(date)}; }; @@ -249,7 +245,6 @@ const DataDeletionDateCell = ({ pipelineRun }: CellProps): ReactNode => { return
N/A
; } - // Calculate deletion date as 14 days after completion const completionDate = new Date(pipelineRun?.timeCompleted); const deletionDate = new Date(completionDate); deletionDate.setDate(deletionDate.getDate() + 14); @@ -268,59 +263,55 @@ const QuotaUsedCell = (props: CellProps): ReactNode => { }; const ActionCell = ({ pipelineRun }: CellProps): ReactNode => { - const signal = useCancellation(); - const [pipelineRunResult, setPipelineRunResult] = useState(); - - const outputsModal = useModalHandler((args: { jobId: string; result: PipelineRunResponse | undefined }, close) => { - return ; + const outputsModal = useModalHandler(() => { + return ; }); - const errorModal = useModalHandler((args: { jobId: string; result: PipelineRunResponse | undefined }, close) => { - return ; + const errorModal = useModalHandler(() => { + return ; }); - async function fetchPipelineRunResults() { - const results = await Teaspoons(signal).getPipelineRunResult(pipelineRun.jobId); - setPipelineRunResult(results); - } - return (
{pipelineRun.status === 'SUCCEEDED' && ( <> - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - { - e.preventDefault(); - if (!pipelineRunResult) { - await fetchPipelineRunResults(); - } - outputsModal.open({ jobId: pipelineRun.jobId, result: pipelineRunResult }); + {outputsModal.maybeRender()} )} {pipelineRun.status === 'FAILED' && ( <> - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - { - e.preventDefault(); - if (!pipelineRunResult) { - await fetchPipelineRunResults(); - } - errorModal.open({ jobId: pipelineRun.jobId, result: pipelineRunResult }); + {errorModal.maybeRender()} )} diff --git a/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx b/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx index abed59327d..9220e44b5a 100644 --- a/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx +++ b/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.tsx @@ -1,30 +1,49 @@ import { ButtonPrimary, Modal, Spinner } from '@terra-ui-packages/components'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, useEffect, useState } from 'react'; +import { Teaspoons } from 'src/libs/ajax/teaspoons/Teaspoons'; import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models'; +import { useCancellation } from 'src/libs/react-utils'; /** * Modal component for displaying pipeline errors */ interface ErrorModalProps { jobId: string; - result: PipelineRunResponse | undefined; onDismiss: () => void; } -export const ViewErrorModal = ({ jobId, result, onDismiss }: ErrorModalProps): ReactNode => { +export const ViewErrorModal = ({ jobId, onDismiss }: ErrorModalProps): ReactNode => { + const [result, setResult] = useState(); + const [loading, setLoading] = useState(true); + const signal = useCancellation(); + + useEffect(() => { + const fetchPipelineRunResults = async () => { + try { + setLoading(true); + const results = await Teaspoons(signal).getPipelineRunResult(jobId); + setResult(results); + } finally { + setLoading(false); + } + }; + + fetchPipelineRunResults(); + }, [jobId, signal]); + return (

Error Details

- {!result ? ( + {loading ? (
Loading error details...
) : (
- {result.errorReport ? ( + {result?.errorReport ? (
void; } -export const ViewOutputsModal = ({ jobId, result, onDismiss }: OutputsModalProps): ReactNode => { +export const ViewOutputsModal = ({ jobId, onDismiss }: OutputsModalProps): ReactNode => { + const [result, setResult] = useState(); + const [loading, setLoading] = useState(true); + const signal = useCancellation(); + + useEffect(() => { + const fetchPipelineRunResults = async () => { + try { + setLoading(true); + const results = await Teaspoons(signal).getPipelineRunResult(jobId); + setResult(results); + } finally { + setLoading(false); + } + }; + + fetchPipelineRunResults(); + }, [jobId, signal]); + return (

Available Output Files

- {!result ? ( + {loading ? (
Loading outputs...
) : (
- {result.pipelineRunReport.outputs && Object.entries(result.pipelineRunReport.outputs).length > 0 ? ( + {result?.pipelineRunReport.outputs && Object.entries(result.pipelineRunReport.outputs).length > 0 ? (
Date: Mon, 9 Jun 2025 17:28:58 -0400 Subject: [PATCH 06/10] cleanup --- .../pipelines/views/modals/ViewOutputsModal.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx b/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx index 21d27a48ca..e0bbd789d3 100644 --- a/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx +++ b/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.tsx @@ -1,4 +1,5 @@ import { ButtonPrimary, Icon, Modal, Spinner } from '@terra-ui-packages/components'; +import { formatDate } from '@terra-ui-packages/core-utils'; import React, { ReactNode, useEffect, useState } from 'react'; import { Teaspoons } from 'src/libs/ajax/teaspoons/Teaspoons'; import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models'; @@ -91,11 +92,7 @@ export const ViewOutputsModal = ({ jobId, onDismiss }: OutputsModalProps): React > All output files for this job will be automatically deleted on{' '} - {new Date(result.pipelineRunReport.outputExpirationDate).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} + {formatDate(result.pipelineRunReport.outputExpirationDate)} . Please download them before this date.
From 6a23b85277f8107649f97ae461d1d44efa40e95f Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Mon, 9 Jun 2025 19:47:26 -0400 Subject: [PATCH 07/10] tests --- .../views/modals/ViewErrorModal.test.tsx | 139 ++++++++++++++++++ .../views/modals/ViewOutputsModal.test.tsx | 135 +++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.test.tsx create mode 100644 src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.test.tsx diff --git a/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.test.tsx b/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.test.tsx new file mode 100644 index 0000000000..6fd6d00b00 --- /dev/null +++ b/src/pages/scientificServices/pipelines/views/modals/ViewErrorModal.test.tsx @@ -0,0 +1,139 @@ +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Teaspoons, TeaspoonsContract } from 'src/libs/ajax/teaspoons/Teaspoons'; +import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models'; +import { asMockedFn, partial, renderWithAppContexts } from 'src/testing/test-utils'; + +import { ViewErrorModal } from './ViewErrorModal'; + +jest.mock('src/libs/ajax/teaspoons/Teaspoons'); + +describe('ViewErrorModal', () => { + const jobId = 'test-job-id'; + const onDismissMock = jest.fn(); + + it('displays loading state initially', () => { + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockReturnValue(new Promise(() => {})), + }) + ); + + renderWithAppContexts(); + + expect(screen.getByText('Loading error details...')).toBeInTheDocument(); + expect(screen.getByText(`Pipeline Error - ${jobId}`)).toBeInTheDocument(); + }); + + it('fetches and displays error information', async () => { + const mockErrorReport = { + message: 'Test error message', + errorCode: 504, + causes: ['Error cause 1', 'Error cause 2'], + }; + + const mockPipelineRunResponse: Partial = { + errorReport: mockErrorReport, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResponse), + }) + ); + + renderWithAppContexts(); + + await waitFor(() => { + expect(screen.queryByText('Loading error details...')).not.toBeInTheDocument(); + }); + + // Verify error details are displayed + expect(screen.getByText('Error Message:')).toBeInTheDocument(); + expect(screen.getByText('Test error message')).toBeInTheDocument(); + + // Verify error causes are displayed + expect(screen.getByText('Error Causes:')).toBeInTheDocument(); + expect(screen.getByText('Error cause 1')).toBeInTheDocument(); + expect(screen.getByText('Error cause 2')).toBeInTheDocument(); + }); + + it('displays a message when no error details are available', async () => { + const mockPipelineRunResponse: Partial = {}; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResponse), + }) + ); + + renderWithAppContexts(); + + // Wait for loading to complete + await waitFor(() => { + expect(screen.queryByText('Loading error details...')).not.toBeInTheDocument(); + }); + + // Verify "no error details" message is displayed + expect(screen.getByText('No detailed error information available for this job.')).toBeInTheDocument(); + }); + + it('calls onDismiss when Close button is clicked', async () => { + const mockPipelineRunResponse: Partial = { + errorReport: { + errorCode: 500, + causes: [], + message: 'Test error', + }, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResponse), + }) + ); + + renderWithAppContexts(); + + await waitFor(() => { + expect(screen.queryByText('Loading error details...')).not.toBeInTheDocument(); + }); + + const user = userEvent.setup(); + await user.click(screen.getByText('Close')); + + // Verify onDismiss was called + expect(onDismissMock).toHaveBeenCalledTimes(1); + }); + + it('handles errors empty causes array', async () => { + const mockErrorReport = { + message: 'Simple error message with no causes', + errorCode: 500, + causes: [], + }; + + const mockPipelineRunResponse: Partial = { + errorReport: mockErrorReport, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResponse), + }) + ); + + renderWithAppContexts(); + + await waitFor(() => { + expect(screen.queryByText('Loading error details...')).not.toBeInTheDocument(); + }); + + // Verify error message is displayed + expect(screen.getByText('Simple error message with no causes')).toBeInTheDocument(); + + // Verify causes section is not displayed + expect(screen.queryByText('Error Causes:')).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.test.tsx b/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.test.tsx new file mode 100644 index 0000000000..fabc31a650 --- /dev/null +++ b/src/pages/scientificServices/pipelines/views/modals/ViewOutputsModal.test.tsx @@ -0,0 +1,135 @@ +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Teaspoons, TeaspoonsContract } from 'src/libs/ajax/teaspoons/Teaspoons'; +import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models'; +import { asMockedFn, partial, renderWithAppContexts } from 'src/testing/test-utils'; + +import { ViewOutputsModal } from './ViewOutputsModal'; + +jest.mock('src/libs/ajax/teaspoons/Teaspoons'); + +describe('ViewOutputsModal', () => { + const jobId = 'test-job-id'; + const onDismissMock = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays loading state initially', () => { + // Mock Teaspoons to return a promise that never resolves to keep the loading state + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockReturnValue(new Promise(() => {})), + }) + ); + + renderWithAppContexts(); + + expect(screen.getByText('Loading outputs...')).toBeInTheDocument(); + expect(screen.getByText(`Pipeline Outputs - ${jobId}`)).toBeInTheDocument(); + }); + + it('fetches and displays pipeline outputs', async () => { + const mockOutputs = { + output1: 'https://example.com/output1.vcf', + output2: 'https://example.com/output2.bam', + }; + + const mockPipelineRunResponse: Partial = { + pipelineRunReport: { + pipelineName: 'test-pipeline', + pipelineVersion: 1, + toolVersion: '1.0.0', + outputExpirationDate: '2025-07-09T00:00:00Z', + outputs: mockOutputs, + }, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResponse), + }) + ); + + renderWithAppContexts(); + + // Wait for loading to complete + await waitFor(() => { + expect(screen.queryByText('Loading outputs...')).not.toBeInTheDocument(); + }); + + // Verify outputs are displayed + expect(screen.getByText('output1')).toBeInTheDocument(); + expect(screen.getByText('output2')).toBeInTheDocument(); + + // Verify download buttons are present + const downloadButtons = screen.getAllByText('Download'); + expect(downloadButtons).toHaveLength(2); + + // Verify expiration notice is displayed + expect(screen.getByText(/All output files for this job will be automatically deleted on/)).toBeInTheDocument(); + expect(screen.getByText('Jul 9, 2025')).toBeInTheDocument(); + }); + + it('displays a message when no outputs are available', async () => { + const mockPipelineRunResponse: Partial = { + pipelineRunReport: { + pipelineName: 'test-pipeline', + pipelineVersion: 1, + toolVersion: '1.0.0', + outputExpirationDate: '2025-07-09T00:00:00Z', + outputs: {}, + }, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResponse), + }) + ); + + renderWithAppContexts(); + + // Wait for loading to complete + await waitFor(() => { + expect(screen.queryByText('Loading outputs...')).not.toBeInTheDocument(); + }); + + // Verify "no outputs" message is displayed + expect(screen.getByText('No output files found for this job.')).toBeInTheDocument(); + }); + + it('calls onDismiss when Close button is clicked', async () => { + const mockPipelineRunResponse: Partial = { + pipelineRunReport: { + pipelineName: 'test-pipeline', + pipelineVersion: 1, + toolVersion: '1.0.0', + outputExpirationDate: '2025-07-09T00:00:00Z', + outputs: {}, + }, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResponse), + }) + ); + + renderWithAppContexts(); + + // Wait for loading to complete + await waitFor(() => { + expect(screen.queryByText('Loading outputs...')).not.toBeInTheDocument(); + }); + + // Click the Close button + const user = userEvent.setup(); + await user.click(screen.getByText('Close')); + + // Verify onDismiss was called + expect(onDismissMock).toHaveBeenCalledTimes(1); + }); +}); From 517361dff5988e9b3aa45fcc43a08120592a031f Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Mon, 9 Jun 2025 20:11:37 -0400 Subject: [PATCH 08/10] more tests --- .../pipelines/views/JobHistory.test.tsx | 236 +++++++++++++++++- 1 file changed, 235 insertions(+), 1 deletion(-) diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx index e1219ed5c9..849e227569 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx @@ -1,4 +1,5 @@ -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { Teaspoons, TeaspoonsContract } from 'src/libs/ajax/teaspoons/Teaspoons'; import { asMockedFn, partial, renderWithAppContexts as render } from 'src/testing/test-utils'; @@ -44,6 +45,8 @@ describe('job history table', () => { updatedAt: '2023-10-01T00:00:00Z', pipelineVersion: 'v1.0.0', pipelineName: 'array_imputation', + jobId: 'job-123', + timeSubmitted: '2023-10-01T00:00:00Z', }, ]; @@ -65,4 +68,235 @@ describe('job history table', () => { expect(screen.queryAllByText('Test Job')).toHaveLength(2); expect(screen.getByText('In Progress')).toBeInTheDocument(); }); + + it('shows View Outputs button for SUCCEEDED jobs', async () => { + const pipelineRuns = [ + { + id: '123', + jobId: 'job-123', + description: 'Successful Job', + status: 'SUCCEEDED', + timeSubmitted: '2023-10-01T00:00:00Z', + timeCompleted: '2023-10-01T01:00:00Z', + pipelineVersion: 'v1.0.0', + pipelineName: 'array_imputation', + createdAt: '2023-10-01T00:00:00Z', + updatedAt: '2023-10-01T01:00:00Z', + }, + ]; + + const mockPipelineRunResponse = { + pageToken: null, + results: pipelineRuns, + totalResults: 1, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse), + }) + ); + + render(); + + // Wait for the table to render with job data + await waitFor(() => { + expect(screen.getAllByText('job-123')).toHaveLength(2); + }); + + // Verify the "View Outputs" button is present + const viewOutputsButton = screen.getByText('View Outputs'); + expect(viewOutputsButton).toBeInTheDocument(); + + // Verify the "View Error" button is not present + expect(screen.queryByText('View Error')).not.toBeInTheDocument(); + }); + + it('shows View Error button for FAILED jobs', async () => { + const pipelineRuns = [ + { + id: '456', + jobId: 'job-456', + description: 'Failed Job', + status: 'FAILED', + timeSubmitted: '2023-10-01T00:00:00Z', + timeCompleted: '2023-10-01T01:00:00Z', + pipelineVersion: 'v1.0.0', + pipelineName: 'array_imputation', + createdAt: '2023-10-01T00:00:00Z', + updatedAt: '2023-10-01T01:00:00Z', + }, + ]; + + const mockPipelineRunResponse = { + pageToken: null, + results: pipelineRuns, + totalResults: 1, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse), + }) + ); + + render(); + + // Wait for the table to render with job data + await waitFor(() => { + expect(screen.getAllByText('job-456')).toHaveLength(2); + }); + + // Verify the "View Error" button is present + const viewErrorButton = screen.getByText('View Error'); + expect(viewErrorButton).toBeInTheDocument(); + + // Verify the "View Outputs" button is not present + expect(screen.queryByText('View Outputs')).not.toBeInTheDocument(); + }); + + it('shows neither button for RUNNING jobs', async () => { + const pipelineRuns = [ + { + id: '789', + jobId: 'job-789', + description: 'Running Job', + status: 'RUNNING', + timeSubmitted: '2023-10-01T00:00:00Z', + pipelineVersion: 'v1.0.0', + pipelineName: 'array_imputation', + createdAt: '2023-10-01T00:00:00Z', + updatedAt: '2023-10-01T00:30:00Z', + }, + ]; + + const mockPipelineRunResponse = { + pageToken: null, + results: pipelineRuns, + totalResults: 1, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse), + }) + ); + + render(); + + // Wait for the table to render with job data + await waitFor(() => { + expect(screen.getAllByText('job-789')).toHaveLength(2); + }); + + // Verify neither button is present + expect(screen.queryByText('View Outputs')).not.toBeInTheDocument(); + expect(screen.queryByText('View Error')).not.toBeInTheDocument(); + expect(screen.getByText('In Progress')).toBeInTheDocument(); + }); + + it('opens the outputs modal when View Outputs button is clicked', async () => { + const pipelineRuns = [ + { + id: '123', + jobId: 'job-123', + description: 'Successful Job', + status: 'SUCCEEDED', + timeSubmitted: '2023-10-01T00:00:00Z', + timeCompleted: '2023-10-01T01:00:00Z', + pipelineVersion: 'v1.0.0', + pipelineName: 'array_imputation', + createdAt: '2023-10-01T00:00:00Z', + updatedAt: '2023-10-01T01:00:00Z', + }, + ]; + + const mockPipelineRunResponse = { + pageToken: null, + results: pipelineRuns, + totalResults: 1, + }; + + // Mock the pipeline run results that will be requested by the modal + const mockPipelineRunResult = { + pipelineRunReport: { + outputs: { + 'output1.txt': 'https://example.com/output1.txt', + }, + }, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse), + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResult), + }) + ); + + render(); + + // Wait for the table to render + await waitFor(() => { + expect(screen.getAllByText('job-123')).toHaveLength(2); + }); + + // Click the "View Outputs" button + const user = userEvent.setup(); + await user.click(screen.getByText('View Outputs')); + + // Verify the modal is opened + expect(await screen.findByText('Pipeline Outputs - job-123')).toBeInTheDocument(); + }); + + it('opens the error modal when View Error button is clicked', async () => { + const pipelineRuns = [ + { + id: '456', + jobId: 'job-456', + description: 'Failed Job', + status: 'FAILED', + timeSubmitted: '2023-10-01T00:00:00Z', + timeCompleted: '2023-10-01T01:00:00Z', + pipelineVersion: 'v1.0.0', + pipelineName: 'array_imputation', + createdAt: '2023-10-01T00:00:00Z', + updatedAt: '2023-10-01T01:00:00Z', + }, + ]; + + const mockPipelineRunResponse = { + pageToken: null, + results: pipelineRuns, + totalResults: 1, + }; + + // Mock the pipeline run results that will be requested by the modal + const mockPipelineRunResult = { + errorReport: { + message: 'Test error message', + causes: ['Test error cause'], + }, + }; + + asMockedFn(Teaspoons).mockReturnValue( + partial({ + getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse), + getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResult), + }) + ); + + render(); + + // Wait for the table to render + await waitFor(() => { + expect(screen.getAllByText('job-456')).toHaveLength(2); + }); + + // Click the "View Error" button + const user = userEvent.setup(); + await user.click(screen.getByText('View Error')); + + // Verify the modal is opened + expect(await screen.findByText('Pipeline Error - job-456')).toBeInTheDocument(); + }); }); From 2f652075174f5df3f5c4df56c02b659c37f05bb7 Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Mon, 9 Jun 2025 20:21:20 -0400 Subject: [PATCH 09/10] cleanup --- .../pipelines/utils/mock-utils.ts | 14 ++ .../pipelines/views/JobHistory.test.tsx | 127 +++--------------- 2 files changed, 35 insertions(+), 106 deletions(-) diff --git a/src/pages/scientificServices/pipelines/utils/mock-utils.ts b/src/pages/scientificServices/pipelines/utils/mock-utils.ts index c7c38f639a..3decd96491 100644 --- a/src/pages/scientificServices/pipelines/utils/mock-utils.ts +++ b/src/pages/scientificServices/pipelines/utils/mock-utils.ts @@ -1,6 +1,8 @@ import { Pipeline, PipelineInput, + PipelineRun, + PipelineRunStatus, PipelineWithDetails, UserPipelineQuotaDetails, } from 'src/libs/ajax/teaspoons/teaspoons-models'; @@ -43,3 +45,15 @@ export function mockUserPipelineQuotaDetails(name: string): UserPipelineQuotaDet quotaUnits: 'things', }; } + +export function mockPipelineRun(status: PipelineRunStatus): PipelineRun { + return { + jobId: 'run-id-123', + pipelineName: 'array_imputation', + status, + description: 'Test pipeline run', + timeSubmitted: '2023-10-01T00:00:00Z', + timeCompleted: status === 'SUCCEEDED' || status === 'FAILED' ? '2023-10-01T01:00:00Z' : undefined, + quotaConsumed: status === 'SUCCEEDED' ? 500 : undefined, + }; +} diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx index 849e227569..2c534b3cf0 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.test.tsx @@ -2,6 +2,7 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Teaspoons, TeaspoonsContract } from 'src/libs/ajax/teaspoons/Teaspoons'; +import { mockPipelineRun } from 'src/pages/scientificServices/pipelines/utils/mock-utils'; import { asMockedFn, partial, renderWithAppContexts as render } from 'src/testing/test-utils'; import { JobHistory } from './JobHistory'; @@ -36,19 +37,8 @@ jest.mock('src/libs/nav', () => ({ describe('job history table', () => { it('renders the job history table', async () => { - const pipelineRuns = [ - { - id: '123', - description: 'Test Job', - status: 'RUNNING', - createdAt: '2023-10-01T00:00:00Z', - updatedAt: '2023-10-01T00:00:00Z', - pipelineVersion: 'v1.0.0', - pipelineName: 'array_imputation', - jobId: 'job-123', - timeSubmitted: '2023-10-01T00:00:00Z', - }, - ]; + const pipelineRun = mockPipelineRun('RUNNING'); + const pipelineRuns = [pipelineRun]; const mockPipelineRunResponse = { pageToken: 'nextPageToken', @@ -65,25 +55,13 @@ describe('job history table', () => { render(); expect(await screen.findByText('Job History')).toBeInTheDocument(); - expect(screen.queryAllByText('Test Job')).toHaveLength(2); + expect(screen.queryAllByText(pipelineRun.description!)).toHaveLength(2); expect(screen.getByText('In Progress')).toBeInTheDocument(); }); it('shows View Outputs button for SUCCEEDED jobs', async () => { - const pipelineRuns = [ - { - id: '123', - jobId: 'job-123', - description: 'Successful Job', - status: 'SUCCEEDED', - timeSubmitted: '2023-10-01T00:00:00Z', - timeCompleted: '2023-10-01T01:00:00Z', - pipelineVersion: 'v1.0.0', - pipelineName: 'array_imputation', - createdAt: '2023-10-01T00:00:00Z', - updatedAt: '2023-10-01T01:00:00Z', - }, - ]; + const pipelineRun = mockPipelineRun('SUCCEEDED'); + const pipelineRuns = [pipelineRun]; const mockPipelineRunResponse = { pageToken: null, @@ -99,34 +77,19 @@ describe('job history table', () => { render(); - // Wait for the table to render with job data await waitFor(() => { - expect(screen.getAllByText('job-123')).toHaveLength(2); + expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2); }); - // Verify the "View Outputs" button is present const viewOutputsButton = screen.getByText('View Outputs'); expect(viewOutputsButton).toBeInTheDocument(); - // Verify the "View Error" button is not present expect(screen.queryByText('View Error')).not.toBeInTheDocument(); }); it('shows View Error button for FAILED jobs', async () => { - const pipelineRuns = [ - { - id: '456', - jobId: 'job-456', - description: 'Failed Job', - status: 'FAILED', - timeSubmitted: '2023-10-01T00:00:00Z', - timeCompleted: '2023-10-01T01:00:00Z', - pipelineVersion: 'v1.0.0', - pipelineName: 'array_imputation', - createdAt: '2023-10-01T00:00:00Z', - updatedAt: '2023-10-01T01:00:00Z', - }, - ]; + const pipelineRun = mockPipelineRun('FAILED'); + const pipelineRuns = [pipelineRun]; const mockPipelineRunResponse = { pageToken: null, @@ -142,33 +105,19 @@ describe('job history table', () => { render(); - // Wait for the table to render with job data await waitFor(() => { - expect(screen.getAllByText('job-456')).toHaveLength(2); + expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2); }); - // Verify the "View Error" button is present const viewErrorButton = screen.getByText('View Error'); expect(viewErrorButton).toBeInTheDocument(); - // Verify the "View Outputs" button is not present expect(screen.queryByText('View Outputs')).not.toBeInTheDocument(); }); it('shows neither button for RUNNING jobs', async () => { - const pipelineRuns = [ - { - id: '789', - jobId: 'job-789', - description: 'Running Job', - status: 'RUNNING', - timeSubmitted: '2023-10-01T00:00:00Z', - pipelineVersion: 'v1.0.0', - pipelineName: 'array_imputation', - createdAt: '2023-10-01T00:00:00Z', - updatedAt: '2023-10-01T00:30:00Z', - }, - ]; + const pipelineRun = mockPipelineRun('RUNNING'); + const pipelineRuns = [pipelineRun]; const mockPipelineRunResponse = { pageToken: null, @@ -184,32 +133,18 @@ describe('job history table', () => { render(); - // Wait for the table to render with job data await waitFor(() => { - expect(screen.getAllByText('job-789')).toHaveLength(2); + expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2); }); - // Verify neither button is present expect(screen.queryByText('View Outputs')).not.toBeInTheDocument(); expect(screen.queryByText('View Error')).not.toBeInTheDocument(); expect(screen.getByText('In Progress')).toBeInTheDocument(); }); it('opens the outputs modal when View Outputs button is clicked', async () => { - const pipelineRuns = [ - { - id: '123', - jobId: 'job-123', - description: 'Successful Job', - status: 'SUCCEEDED', - timeSubmitted: '2023-10-01T00:00:00Z', - timeCompleted: '2023-10-01T01:00:00Z', - pipelineVersion: 'v1.0.0', - pipelineName: 'array_imputation', - createdAt: '2023-10-01T00:00:00Z', - updatedAt: '2023-10-01T01:00:00Z', - }, - ]; + const pipelineRun = mockPipelineRun('SUCCEEDED'); + const pipelineRuns = [pipelineRun]; const mockPipelineRunResponse = { pageToken: null, @@ -217,7 +152,6 @@ describe('job history table', () => { totalResults: 1, }; - // Mock the pipeline run results that will be requested by the modal const mockPipelineRunResult = { pipelineRunReport: { outputs: { @@ -235,34 +169,19 @@ describe('job history table', () => { render(); - // Wait for the table to render await waitFor(() => { - expect(screen.getAllByText('job-123')).toHaveLength(2); + expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2); }); - // Click the "View Outputs" button const user = userEvent.setup(); await user.click(screen.getByText('View Outputs')); - // Verify the modal is opened - expect(await screen.findByText('Pipeline Outputs - job-123')).toBeInTheDocument(); + expect(await screen.findByText('Pipeline Outputs', { exact: false })).toBeInTheDocument(); }); it('opens the error modal when View Error button is clicked', async () => { - const pipelineRuns = [ - { - id: '456', - jobId: 'job-456', - description: 'Failed Job', - status: 'FAILED', - timeSubmitted: '2023-10-01T00:00:00Z', - timeCompleted: '2023-10-01T01:00:00Z', - pipelineVersion: 'v1.0.0', - pipelineName: 'array_imputation', - createdAt: '2023-10-01T00:00:00Z', - updatedAt: '2023-10-01T01:00:00Z', - }, - ]; + const pipelineRun = mockPipelineRun('FAILED'); + const pipelineRuns = [pipelineRun]; const mockPipelineRunResponse = { pageToken: null, @@ -270,7 +189,6 @@ describe('job history table', () => { totalResults: 1, }; - // Mock the pipeline run results that will be requested by the modal const mockPipelineRunResult = { errorReport: { message: 'Test error message', @@ -287,16 +205,13 @@ describe('job history table', () => { render(); - // Wait for the table to render await waitFor(() => { - expect(screen.getAllByText('job-456')).toHaveLength(2); + expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2); }); - // Click the "View Error" button const user = userEvent.setup(); await user.click(screen.getByText('View Error')); - // Verify the modal is opened - expect(await screen.findByText('Pipeline Error - job-456')).toBeInTheDocument(); + expect(await screen.findByText('Pipeline Error', { exact: false })).toBeInTheDocument(); }); }); From dd06ac6025a1bdbafd0a530567993d17768bfd3f Mon Sep 17 00:00:00 2001 From: Matt Bemis Date: Wed, 11 Jun 2025 13:31:39 -0400 Subject: [PATCH 10/10] fix typo --- src/pages/scientificServices/pipelines/views/JobHistory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/scientificServices/pipelines/views/JobHistory.tsx b/src/pages/scientificServices/pipelines/views/JobHistory.tsx index d9101309f0..6ee4626aee 100644 --- a/src/pages/scientificServices/pipelines/views/JobHistory.tsx +++ b/src/pages/scientificServices/pipelines/views/JobHistory.tsx @@ -51,7 +51,7 @@ export const JobHistory = () => {

Job History

- All files associated with jobs will be automatically deleted after 2 weeks from completed. + All files associated with jobs will be automatically deleted after 2 weeks from completion.
For support, email{' '}