Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/libs/ajax/teaspoons/teaspoons-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export interface Pipeline {
description: string;
}

export type PipelineIOType = 'FILE' | 'STRING' | 'FLOAT' | 'BOOLEAN';

export interface PipelineInput {
name: string;
type: 'FILE' | 'STRING' | 'FLOAT' | 'BOOLEAN';
type: PipelineIOType | string; // Prefer a defined type, but allow for future types without breaking
isRequired: boolean;
displayName?: string;
description?: string;
Expand All @@ -19,7 +21,7 @@ export interface PipelineInput {

export interface PipelineOutput {
name: string;
type: string;
type: PipelineIOType | string; // Prefer a defined type, but allow for future types without breaking
displayName?: string;
description?: string;
}
Expand Down Expand Up @@ -124,7 +126,11 @@ export interface PipelineRunReport {
pipelineVersion: number;
toolVersion: string;
outputs?: Record<string, string>;
userInputs?: Record<string, string>;
outputExpirationDate?: string;
inputSize?: number;
inputSizeUnits?: string;
quotaConsumed?: number;
}

export type PipelineRunStatus = 'PREPARING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED';
7 changes: 7 additions & 0 deletions src/pages/scientificServices/NavPaths.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CliAuth } from 'src/pages/scientificServices/cli-auth/CliAuth';
import { About } from 'src/pages/scientificServices/pipelines/tabs/about/About';
import { JobDetails } from 'src/pages/scientificServices/pipelines/tabs/history/details/JobDetails';
import { JobHistory } from 'src/pages/scientificServices/pipelines/tabs/history/JobHistory';
import { RunJob } from 'src/pages/scientificServices/pipelines/tabs/run/RunJob';

Expand Down Expand Up @@ -29,6 +30,12 @@ export const navPaths = [
component: JobHistory,
title: 'Job History',
},
{
name: 'pipelines-history-detail',
path: '/pipelines/imputation/history/:jobId',
component: JobDetails,
title: 'Job Details',
},
{
name: 'cli-auth',
path: '/pipelines/cli-auth',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useState } from 'react';
import { Teaspoons } from 'src/libs/ajax/teaspoons/Teaspoons';
import { PipelineWithDetails } from 'src/libs/ajax/teaspoons/teaspoons-models';
import { notify } from 'src/libs/notifications';
import { useCancellation } from 'src/libs/react-utils';

export interface UsePipelineDetailsResult {
pipelineDetails: PipelineWithDetails | null;
isLoading: boolean;
error: Error | undefined;
}

export const usePipelineDetails = (pipelineName: string, pipelineVersion: number): UsePipelineDetailsResult => {
const signal = useCancellation();
const [pipelineDetails, setPipelineDetails] = useState<PipelineWithDetails | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | undefined>(undefined);

const fetchPipelineDetails = async () => {
setIsLoading(true);
setError(undefined);

try {
const response = await Teaspoons(signal).getPipelineDetails(pipelineName, pipelineVersion);
setPipelineDetails(response);
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to fetch pipeline details');
setError(error);
notify('error', error.message);
setPipelineDetails(null);
} finally {
setIsLoading(false);
}
};

useEffect(() => {
fetchPipelineDetails();
}, [pipelineName, pipelineVersion, signal]); // eslint-disable-line react-hooks/exhaustive-deps

return {
pipelineDetails,
isLoading,
error,
};
};
113 changes: 82 additions & 31 deletions src/pages/scientificServices/pipelines/tabs/history/JobHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ButtonPrimary, Icon, Spinner, TooltipTrigger, useModalHandler } from '@terra-ui-packages/components';
import { ButtonPrimary, Icon, Link, Spinner, TooltipTrigger, useModalHandler } from '@terra-ui-packages/components';
import { formatDate, formatDatetime } from '@terra-ui-packages/core-utils';
import _, { capitalize } from 'lodash';
import pluralize from 'pluralize';
Expand All @@ -11,6 +11,7 @@ import { Teaspoons } from 'src/libs/ajax/teaspoons/Teaspoons';
import { GetPipelineRunsResponse, PipelineRun } from 'src/libs/ajax/teaspoons/teaspoons-models';
import colors from 'src/libs/colors';
import Events from 'src/libs/events';
import * as Nav from 'src/libs/nav';
import { useCancellation } from 'src/libs/react-utils';
import {
pipelinesTopBar,
Expand All @@ -21,6 +22,10 @@ import { usePipelinesList } from 'src/pages/scientificServices/pipelines/hooks/u
import { FilterValues, TableFilters } from 'src/pages/scientificServices/pipelines/tabs/history/controls/TableFilters';
import { ViewErrorModal } from 'src/pages/scientificServices/pipelines/tabs/history/modals/ViewErrorModal';
import { ViewOutputsModal } from 'src/pages/scientificServices/pipelines/tabs/history/modals/ViewOutputsModal';
import {
getPipelineColor,
getPipelineStatusColor,
} from 'src/pages/scientificServices/pipelines/utils/pipeline-style-utils';

// If a job is still in "Preparing" state after this many hours, we consider it a failure.
export const PREPARING_JOB_CUTOFF_HOURS = 12;
Expand Down Expand Up @@ -204,23 +209,23 @@ const getColumns = (paginatedRuns: PipelineRun[], sort: SortProperties, onSort:
cellRenderer: ({ rowIndex }) => {
return <JobIdCell pipelineRun={paginatedRuns[rowIndex]} />;
},
size: { basis: 140 },
size: { basis: 250 },
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tweaked the width of some of the columns to keep things more appropriately distributed

},
{
field: 'description',
headerRenderer: () => <HeaderCell>Description</HeaderCell>,
cellRenderer: ({ rowIndex }) => {
return <DescriptionCell pipelineRun={paginatedRuns[rowIndex]} />;
},
size: { basis: 150 },
size: { basis: 160 },
},
{
field: 'status',
headerRenderer: () => <HeaderCell>Status</HeaderCell>,
cellRenderer: ({ rowIndex }) => {
return <StatusCell pipelineRun={paginatedRuns[rowIndex]} />;
},
size: { basis: 40 },
size: { basis: 50 },
},
{
field: 'submitted',
Expand All @@ -232,7 +237,7 @@ const getColumns = (paginatedRuns: PipelineRun[], sort: SortProperties, onSort:
cellRenderer: ({ rowIndex }) => {
return <SubmittedCell pipelineRun={paginatedRuns[rowIndex]} />;
},
size: { basis: 80 },
size: { basis: 60 },
},
{
field: 'completed',
Expand All @@ -245,15 +250,15 @@ const getColumns = (paginatedRuns: PipelineRun[], sort: SortProperties, onSort:
cellRenderer: ({ rowIndex }) => {
return <CompletedCell pipelineRun={paginatedRuns[rowIndex]} />;
},
size: { basis: 80 },
size: { basis: 60 },
},
{
field: 'dataDeletionDate',
headerRenderer: () => <HeaderCell>Deletion Date</HeaderCell>,
cellRenderer: ({ rowIndex }) => {
return <DataDeletionDateCell pipelineRun={paginatedRuns[rowIndex]} />;
},
size: { basis: 80 },
size: { basis: 60 },
},
{
field: 'quotaUsed',
Expand Down Expand Up @@ -285,26 +290,60 @@ interface CellProps {
}

const JobIdCell = ({ pipelineRun }: CellProps): ReactNode => {
// note that you cannot view details for jobs that are in PREPARING status, so we only link to details for other statuses
const canViewDetails = ['FAILED', 'RUNNING', 'SUCCEEDED'].includes(pipelineRun.status);

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', width: '100%' }}>
<div style={{ width: '100%', overflow: 'hidden' }}>
<TooltipCell
tooltip={pipelineRun.jobId}
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
}}
>
{pipelineRun.jobId}
</TooltipCell>
{canViewDetails ? (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a screenshot showing these changes as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 added to description

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be going crazy but I can't see this change in the Job History screenshot. should I be able to see something? maybe it needs a video if it's just the link?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah my b i thought jose meant just show the jobhistory.tsx changes generally since the columns have new widths.

for these specific changes, it's basically just "is the job ID a link to details or not". here's what that looks like:

Screenshot 2026-01-07 at 12 19 39 PM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

<Link
href={Nav.getLink('pipelines-history-detail', { jobId: pipelineRun.jobId })}
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
display: 'block',
}}
aria-label={`View details for job ${pipelineRun.jobId}`}
>
<TooltipTrigger content={pipelineRun.jobId} side='top'>
<span
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
display: 'block',
}}
>
{pipelineRun.jobId}
</span>
</TooltipTrigger>
</Link>
) : (
<TooltipTrigger content={pipelineRun.jobId} side='top'>
<span
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
display: 'block',
color: colors.dark(),
}}
>
{pipelineRun.jobId}
</span>
</TooltipTrigger>
)}
</div>
<div
style={{
width: 'fit-content',
fontWeight: 600,
backgroundColor: pipelineNameToColor(pipelineRun),
backgroundColor: getPipelineColor(pipelineRun),
padding: '0.33rem',
borderRadius: '4px',
fontSize: '10px',
Expand Down Expand Up @@ -502,7 +541,14 @@ const getRunStatusIcon = (pipelineRun: PipelineRun): ReactNode => {
switch (pipelineRun.status) {
case 'SUCCEEDED':
return (
<div style={{ display: 'flex', alignItems: 'center', color: colors.success(), gap: '0.5rem' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
color: getPipelineStatusColor(pipelineRun.status),
gap: '0.5rem',
}}
>
<Icon icon='success-standard' /> Done
</div>
);
Expand All @@ -520,7 +566,14 @@ const getRunStatusIcon = (pipelineRun: PipelineRun): ReactNode => {

if (hoursElapsed > PREPARING_JOB_CUTOFF_HOURS) {
return (
<div style={{ display: 'flex', alignItems: 'center', color: colors.danger(), gap: '0.5rem' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
color: getPipelineStatusColor('FAILED'),
gap: '0.5rem',
}}
>
<Icon icon='warning-standard' /> Failed
</div>
);
Expand All @@ -538,7 +591,14 @@ const getRunStatusIcon = (pipelineRun: PipelineRun): ReactNode => {
}
case 'FAILED':
return (
<div style={{ display: 'flex', alignItems: 'center', color: colors.danger(), gap: '0.5rem' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
color: getPipelineStatusColor(pipelineRun.status),
gap: '0.5rem',
}}
>
<Icon icon='warning-standard' /> Failed
</div>
);
Expand All @@ -547,15 +607,6 @@ const getRunStatusIcon = (pipelineRun: PipelineRun): ReactNode => {
}
};

const pipelineNameToColor = (pipelineRun: PipelineRun): string => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to pipeline-style-utils

switch (pipelineRun.pipelineName) {
case 'array_imputation':
return '#4D72AA4D';
default:
return '#AA4D8B4D';
}
};

const hoursElapsedSinceSubmission = (pipelineRun: PipelineRun): number => {
const submittedTime = new Date(pipelineRun.timeSubmitted);
const currentTime = new Date();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ButtonSecondary, Icon, Spinner } from '@terra-ui-packages/components';
import React, { useEffect, useState } from 'react';
import FooterWrapper from 'src/components/FooterWrapper';
import { Teaspoons } from 'src/libs/ajax/teaspoons/Teaspoons';
import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models';
import * as Nav from 'src/libs/nav';
import { notify } from 'src/libs/notifications';
import { pipelinesTopBar } from 'src/pages/scientificServices/pipelines/common/scientific-services-common';
import { JobDetailsHeader } from 'src/pages/scientificServices/pipelines/tabs/history/details/sections/JobDetailsHeader';
import { JobInputsOutputsView } from 'src/pages/scientificServices/pipelines/tabs/history/details/sections/JobInputsOutputsView';

export interface JobDetailsProps {
jobId: string;
}

export const JobDetails = ({ jobId }: JobDetailsProps) => {
const [pipelineRunResult, setPipelineRunResult] = useState<PipelineRunResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function fetchJobDetails() {
setIsLoading(true);
try {
const response = await Teaspoons().getPipelineRunResult(jobId);

setPipelineRunResult(response);
} catch (err) {
notify('error', 'Failed to load job details');
} finally {
setIsLoading(false);
}
}
fetchJobDetails();
}, [jobId]);

return (
<FooterWrapper alwaysShow>
{pipelinesTopBar('job history')}
<main
style={{
padding: '1rem 2rem 2rem',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '0.75rem' }}>
<ButtonSecondary
onClick={() => Nav.goToPath('pipelines-history')}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}
>
<Icon icon='arrowLeft' size={16} />
View All
</ButtonSecondary>
</div>

{isLoading && (
<div style={{ display: 'flex', justifyContent: 'center', padding: '3rem' }}>
<Spinner />
</div>
)}

{!isLoading && pipelineRunResult && (
<div>
<JobDetailsHeader pipelineRunResult={pipelineRunResult} />

<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1.5rem' }}>
<div style={{ flex: 1 }}>
{/* Eventually, the Job Timeline / Run Information component will go here too */}
<JobInputsOutputsView pipelineRunResult={pipelineRunResult} />
</div>
</div>
</div>
)}
</main>
</FooterWrapper>
);
};
Loading
Loading