Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 10 additions & 22 deletions awx/ui/src/screens/Job/JobOutput/shared/OutputToolbar.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';
import { DateTime, Duration } from 'luxon';
import { calculateElapsed, secondsToHHMMSS } from 'util/dates';
import { bool, shape, func } from 'prop-types';
import {
DownloadIcon,
Expand Down Expand Up @@ -35,27 +35,17 @@ const Badge = styled(PFBadge)`
: null}
`;

const ElapsedBadge = styled(Badge)`
min-width: 70px;
font-variant-numeric: tabular-nums;
`;

const Wrapper = styled.div`
align-items: center;
display: flex;
flex-flow: row wrap;
font-size: 14px;
`;
const calculateElapsed = (started) => {
if (!started) return '00:00:00';
const now = DateTime.now();
const duration = now
.diff(DateTime.fromISO(`${started}`), [
'milliseconds',
'seconds',
'minutes',
'hours',
])
.toObject();

return Duration.fromObject({ ...duration }).toFormat('hh:mm:ss');
};

const OUTPUT_NO_COUNT_JOB_TYPES = [
'ad_hoc_command',
'system_job',
Expand Down Expand Up @@ -151,13 +141,11 @@ const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
<BadgeGroup aria-label={t`Elapsed Time`}>
<div>{t`Elapsed`}</div>
<Tooltip content={t`Elapsed time that the job ran`}>
<Badge isRead>
{job.finished
? Duration.fromObject({ seconds: job.elapsed }).toFormat(
'hh:mm:ss'
)
<ElapsedBadge isRead>
{job.finished && job.elapsed != null
? secondsToHHMMSS(job.elapsed)
: activeJobElapsedTime}
</Badge>
</ElapsedBadge>
</Tooltip>
</BadgeGroup>
{['pending', 'waiting', 'running'].includes(jobStatus) &&
Expand Down
29 changes: 29 additions & 0 deletions awx/ui/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@patternfly/react-icons';
import styled from 'styled-components';
import StatusLabel from 'components/StatusLabel';
import { calculateElapsed, secondsToHHMMSS } from 'util/dates';
import JobCancelButton from 'components/JobCancelButton';
import {
WorkflowDispatchContext,
Expand Down Expand Up @@ -48,6 +49,12 @@ const Badge = styled(PFBadge)`
margin-left: 10px;
`;

const ElapsedBadge = styled(Badge)`
margin-right: 20px;
min-width: 70px;
font-variant-numeric: tabular-nums;
`;

const ActionButton = styled(Button)`
border: none;
margin: 0px 6px;
Expand All @@ -71,6 +78,20 @@ function WorkflowOutputToolbar({ job }) {
job.summary_fields?.workflow_job_template?.id ??
job.summary_fields?.workflow_job_template?.[0]?.id;

const [activeJobElapsedTime, setActiveJobElapsedTime] = React.useState(
calculateElapsed(job.started)
);

React.useEffect(() => {
let secTimer;
if (job.started && !job.finished) {
secTimer = setInterval(() => {
setActiveJobElapsedTime(calculateElapsed(job.started));
}, 1000);
}
return () => clearInterval(secTimer);
}, [job.started, job.finished]);

const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
const navToWorkflow = () => {
if (workflowTemplateId) {
Expand Down Expand Up @@ -109,6 +130,14 @@ function WorkflowOutputToolbar({ job }) {
<ProjectDiagramIcon />
</ActionButton>
)}
<div>{t`Elapsed`}</div>
<Tooltip content={t`Elapsed time that the job ran`}>
<ElapsedBadge isRead id="workflow-elapsed-badge">
{job.finished && job.elapsed != null
? secondsToHHMMSS(job.elapsed)
: activeJobElapsedTime}
</ElapsedBadge>
</Tooltip>
<div>{t`Total Nodes`}</div>
<Badge isRead>{totalNodes}</Badge>
<Tooltip content={t`Toggle Legend`} position="bottom">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import {
WorkflowDispatchContext,
WorkflowStateContext,
Expand Down Expand Up @@ -57,14 +58,19 @@ describe('WorkflowOutputToolbar', () => {
test('should render correct toolbar item', () => {
shouldFind(`Button[ouiaId="edit-workflow"]`);
shouldFind('Button#workflow-output-toggle-legend');
shouldFind('Badge');
shouldFind('Badge#workflow-elapsed-badge');
shouldFind('Button#workflow-output-toggle-tools');
shouldFind('JobCancelButton');
});

test('Shows correct number of nodes', () => {
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
expect(wrapper.find('Badge').text()).toBe('1');
expect(
wrapper
.find('Badge')
.filterWhere((b) => b.prop('id') !== 'workflow-elapsed-badge')
.text()
).toBe('1');
});

test('Toggle Legend button dispatches as expected', () => {
Expand Down Expand Up @@ -109,4 +115,73 @@ describe('WorkflowOutputToolbar', () => {
0
);
});

describe('elapsed timer', () => {
beforeEach(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date('2021-09-01T12:30:45.000Z'));
});

afterEach(() => {
jest.useRealTimers();
});

function mountToolbar(jobOverrides) {
return mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider value={workflowContext}>
<WorkflowOutputToolbar job={{ ...job, ...jobOverrides }} />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
}

test('should show live elapsed time while running', () => {
const runningWrapper = mountToolbar({
started: '2021-09-01T12:30:40.000Z',
finished: null,
});
expect(
runningWrapper.find('Badge#workflow-elapsed-badge').text()
).toBe('00:00:05');
act(() => {
jest.advanceTimersByTime(2000);
});
runningWrapper.update();
expect(
runningWrapper.find('Badge#workflow-elapsed-badge').text()
).toBe('00:00:07');
});

test('should keep the live value while finished is set but elapsed has not arrived yet', () => {
const wsWindowWrapper = mountToolbar({
status: 'successful',
started: '2021-09-01T12:30:40.000Z',
finished: '2021-09-01T12:30:45.000Z',
elapsed: undefined,
});
expect(
wsWindowWrapper.find('Badge#workflow-elapsed-badge').text()
).toBe('00:00:05');
});

test('should show final elapsed time once finished', () => {
const finishedWrapper = mountToolbar({
status: 'successful',
started: '2021-09-01T11:00:00.000Z',
finished: '2021-09-01T12:01:01.000Z',
elapsed: 3661,
});
expect(
finishedWrapper.find('Badge#workflow-elapsed-badge').text()
).toBe('01:01:01');
act(() => {
jest.advanceTimersByTime(2000);
});
finishedWrapper.update();
expect(
finishedWrapper.find('Badge#workflow-elapsed-badge').text()
).toBe('01:01:01');
});
});
});
14 changes: 14 additions & 0 deletions awx/ui/src/util/dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ export function secondsToHHMMSS(seconds) {
return Duration.fromObject({ seconds }).toFormat('hh:mm:ss');
}

export function calculateElapsed(started) {
if (!started) return '00:00:00';
const duration = DateTime.now()
.diff(DateTime.fromISO(`${started}`), [
'milliseconds',
'seconds',
'minutes',
'hours',
])
.toObject();

return Duration.fromObject({ ...duration }).toFormat('hh:mm:ss');
}

export function secondsToDays(seconds) {
return Duration.fromObject({ seconds }).toFormat('d');
}
Expand Down
24 changes: 24 additions & 0 deletions awx/ui/src/util/dates.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RRule } from 'rrule';
import {
calculateElapsed,
dateToInputDateTime,
formatDateString,
getRRuleDayConstants,
Expand Down Expand Up @@ -44,6 +45,29 @@ describe('secondsToDays', () => {
});
});

describe('calculateElapsed', () => {
beforeEach(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date('2021-09-01T12:30:45.000Z'));
});

afterEach(() => {
jest.useRealTimers();
});

test('should return zero when not started', () => {
expect(calculateElapsed(null)).toEqual('00:00:00');
expect(calculateElapsed(undefined)).toEqual('00:00:00');
expect(calculateElapsed('')).toEqual('00:00:00');
});

test('should compute elapsed time from start timestamp', () => {
expect(calculateElapsed('2021-09-01T12:30:44.000Z')).toEqual('00:00:01');
expect(calculateElapsed('2021-09-01T12:29:45.000Z')).toEqual('00:01:00');
expect(calculateElapsed('2021-09-01T10:00:00.000Z')).toEqual('02:30:45');
});
});

describe('secondsToHHMMSS', () => {
test('it returns the expected value', () => {
expect(secondsToHHMMSS(50000)).toEqual('13:53:20');
Expand Down