-
Notifications
You must be signed in to change notification settings - Fork 22
[TSPS-635] Add Job Details view for Teaspoons jobs #5468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
980f8ac
860edc0
645b077
26199ff
23146c0
f44ac12
50c0fb9
d7b3cc3
a29d4ef
69ece79
7447f03
d138dad
8fe3fef
0146435
55fa3e0
cd87122
cd15956
5a85e70
991b588
a40e934
4cbf04e
f791cf2
ad31233
a91485c
707abe4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| }; | ||
| }; |
| 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'; | ||
|
|
@@ -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, | ||
|
|
@@ -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; | ||
|
|
@@ -204,23 +209,23 @@ const getColumns = (paginatedRuns: PipelineRun[], sort: SortProperties, onSort: | |
| cellRenderer: ({ rowIndex }) => { | ||
| return <JobIdCell pipelineRun={paginatedRuns[rowIndex]} />; | ||
| }, | ||
| size: { basis: 140 }, | ||
| size: { basis: 250 }, | ||
| }, | ||
| { | ||
| 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', | ||
|
|
@@ -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', | ||
|
|
@@ -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', | ||
|
|
@@ -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 ? ( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a screenshot showing these changes as well
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 added to description
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
|
|
@@ -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> | ||
| ); | ||
|
|
@@ -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> | ||
| ); | ||
|
|
@@ -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> | ||
| ); | ||
|
|
@@ -547,15 +607,6 @@ const getRunStatusIcon = (pipelineRun: PipelineRun): ReactNode => { | |
| } | ||
| }; | ||
|
|
||
| const pipelineNameToColor = (pipelineRun: PipelineRun): string => { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
|
||
| 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> | ||
| ); | ||
| }; |

There was a problem hiding this comment.
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