Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ describe('JobDetails', () => {
// JobIOView elements
expect(screen.getByText('Inputs')).toBeInTheDocument();
expect(screen.getByText('Outputs')).toBeInTheDocument();

// Timeline element
expect(screen.getByText('Timeline')).toBeInTheDocument();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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 { JobIOView } from 'src/pages/scientificServices/pipelines/tabs/history/details/sections/JobIOView';
import { PipelineRunTimeline } from 'src/pages/scientificServices/pipelines/tabs/history/details/sections/timeline/PipelineRunTimeline';

export interface JobDetailsProps {
jobId: string;
Expand Down Expand Up @@ -62,8 +63,10 @@ export const JobDetails = ({ jobId }: JobDetailsProps) => {
<JobDetailsHeader pipelineRunResult={pipelineRunResult} />

<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1.5rem' }}>
<div style={{ flex: '0 0 25%', minWidth: '200px' }}>
<PipelineRunTimeline pipelineRunResult={pipelineRunResult} />
</div>
<div style={{ flex: 1 }}>
{/* Eventually, the Job Timeline / Run Information component will go here too */}
<JobIOView pipelineRunResult={pipelineRunResult} />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models';

import { calculatePipelineRunDuration } from './PipelineRunTimeline';

describe('PipelineRunTimeline', () => {
describe('calculatePipelineRunDuration', () => {
it('should return N/A when completed timestamp is missing', () => {
const mockPipelineRun = {
jobReport: {
id: 'test-id',
status: 'RUNNING',
submitted: '2024-01-01T10:00:00Z',
completed: undefined,
},
} as PipelineRunResponse;

const duration = calculatePipelineRunDuration(mockPipelineRun);

expect(duration).toBe('N/A');
});

it('should format duration with hours, minutes, and seconds when duration is over 1 hour', () => {
const mockPipelineRun = {
jobReport: {
id: 'test-id',
status: 'SUCCEEDED',
submitted: '2024-01-01T10:00:00Z',
completed: '2024-01-01T11:30:45Z',
},
} as PipelineRunResponse;

const duration = calculatePipelineRunDuration(mockPipelineRun);

expect(duration).toBe('1h 30m 45s');
});

it('should format duration with only minutes and seconds when duration is under 1 hour', () => {
const mockPipelineRun = {
jobReport: {
id: 'test-id',
status: 'SUCCEEDED',
submitted: '2024-01-01T10:00:00Z',
completed: '2024-01-01T10:15:30Z',
},
} as PipelineRunResponse;

const duration = calculatePipelineRunDuration(mockPipelineRun);

expect(duration).toBe('15m 30s');
});

it('should handle duration less than 1 minute', () => {
const mockPipelineRun = {
jobReport: {
id: 'test-id',
status: 'SUCCEEDED',
submitted: '2024-01-01T10:00:00Z',
completed: '2024-01-01T10:00:45Z',
},
} as PipelineRunResponse;

const duration = calculatePipelineRunDuration(mockPipelineRun);

expect(duration).toBe('0m 45s');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Icon } from '@terra-ui-packages/components';
import React from 'react';
import { PipelineRunResponse } from 'src/libs/ajax/teaspoons/teaspoons-models';
import colors from 'src/libs/colors';
import { calculateTimelineEvents } from 'src/pages/scientificServices/pipelines/tabs/history/details/sections/timeline/pipeline-timeline-utils';

import { PipelineRunTimelineEvent } from './PipelineRunTimelineEvent';

interface PipelineRunTimelineProps {
pipelineRunResult: PipelineRunResponse;
}

export const PipelineRunTimeline = ({ pipelineRunResult }: PipelineRunTimelineProps) => {
const timelineEvents = calculateTimelineEvents(pipelineRunResult);

return (
<div
style={{
backgroundColor: '#f4f6f9',
border: '1px solid #d6d9dc',
borderRadius: '4px',
padding: '1rem 1rem 1.5rem',
margin: '1rem 0',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '1rem',
}}
>
<h3>Timeline</h3>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
backgroundColor: 'white',
padding: '0.5rem 0.75rem',
border: '1px solid #D8D9DC',
borderRadius: '20px',
fontWeight: 500,
}}
>
<Icon icon='clock' size={16} style={{ color: colors.dark(0.7) }} />
{calculatePipelineRunDuration(pipelineRunResult)}
</div>
</div>
{timelineEvents.map((event, index) => (
<PipelineRunTimelineEvent key={event.label} event={event} isLast={index === timelineEvents.length - 1} />
))}
</div>
);
};

export const calculatePipelineRunDuration = (pipelineRunResult: PipelineRunResponse): string => {
if (!pipelineRunResult.jobReport.submitted || !pipelineRunResult.jobReport.completed) {
return 'N/A';
}

const durationMs =
new Date(pipelineRunResult.jobReport.completed).getTime() -
new Date(pipelineRunResult.jobReport.submitted).getTime();

const totalSeconds = Math.floor(durationMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;

if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`;
}
return `${minutes}m ${seconds}s`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect } from '@storybook/test';
import { screen } from '@testing-library/react';
import React from 'react';
import { PipelineTimelineEvent } from 'src/pages/scientificServices/pipelines/tabs/history/details/sections/timeline/pipeline-timeline-utils';
import { renderWithAppContexts as render } from 'src/testing/test-utils';

import { PipelineRunTimelineEvent } from './PipelineRunTimelineEvent';

describe('PipelineRunTimelineEvent', () => {
it('should render event label', () => {
const event: PipelineTimelineEvent = {
label: 'Quality Checks',
status: 'SUCCEEDED',
moreInfo: 'Input data passed QC checks',
};

render(<PipelineRunTimelineEvent event={event} isLast={false} />);

expect(screen.getByText('Quality Checks')).toBeInTheDocument();
});

it('should render event moreInfo when provided', () => {
const event: PipelineTimelineEvent = {
label: 'Quota Charged',
status: 'SUCCEEDED',
moreInfo: '250 samples',
};

render(<PipelineRunTimelineEvent event={event} isLast={false} />);

expect(screen.getByText('250 samples')).toBeInTheDocument();
});

it('should render timestamp when provided', () => {
const event: PipelineTimelineEvent = {
label: 'Submitted',
status: 'SUCCEEDED',
timestamp: '2024-01-01T10:00:00Z',
};

render(<PipelineRunTimelineEvent event={event} isLast={false} />);

expect(screen.getByText(/1\/1\/2024/)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import colors from 'src/libs/colors';
import {
getTimelineEventStatusIcon,
PipelineTimelineEvent,
} from 'src/pages/scientificServices/pipelines/tabs/history/details/sections/timeline/pipeline-timeline-utils';

interface PipelineRunTimelineEventProps {
event: PipelineTimelineEvent;
isLast: boolean;
}

export const PipelineRunTimelineEvent = ({ event, isLast }: PipelineRunTimelineEventProps) => {
return (
<div style={{ display: 'flex', alignItems: 'center', marginBottom: isLast ? 0 : '0.5rem' }}>
<div
style={{
display: 'flex',
marginRight: '1rem',
}}
>
{getTimelineEventStatusIcon(event.status)}
</div>

<div
style={{
flex: 1,
padding: '0.75rem',
border: '1px solid #d7d9dc',
backgroundColor: 'white',
borderRadius: '4px',
minHeight: '4rem',
position: 'relative',
}}
>
<div
style={{
position: 'absolute',
left: '-8px',
top: '50%',
transform: 'translateY(-50%)',
width: 0,
height: 0,
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
borderRight: '8px solid #d7d9dc',
}}
/>
<div
style={{
position: 'absolute',
left: '-6px',
top: '50%',
transform: 'translateY(-50%)',
width: 0,
height: 0,
borderTop: '7px solid transparent',
borderBottom: '7px solid transparent',
borderRight: '7px solid white',
}}
/>

{!isLast && (
<div
style={{
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
width: '3px',
height: '1rem',
backgroundColor: '#d7d9dc',
}}
/>
)}

<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
<div style={{ fontWeight: 'bold', color: colors.dark() }}>{event.label}</div>
{event.timestamp && (
<div style={{ color: colors.dark(0.9), fontStyle: 'italic' }}>
{new Date(event.timestamp).toLocaleString()}
</div>
)}
{event.moreInfo && <div style={{ color: colors.dark(0.9), fontStyle: 'italic' }}>{event.moreInfo}</div>}
</div>
</div>
</div>
);
};
Loading
Loading