diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index a8ac64b2cc..e8aaad7be5 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1305,6 +1305,8 @@ class UnifiedJob( status_data['instance_group_name'] = None elif status in ['successful', 'failed', 'canceled'] and self.finished: status_data['finished'] = datetime.datetime.strftime(self.finished, "%Y-%m-%dT%H:%M:%S.%fZ") + elif status == 'running': + status_data['started'] = datetime.datetime.strftime(self.finished, "%Y-%m-%dT%H:%M:%S.%fZ") status_data.update(self.websocket_emit_data()) status_data['group_name'] = 'jobs' if getattr(self, 'unified_job_template_id', None): diff --git a/awx/ui/src/screens/Job/JobOutput/shared/OutputToolbar.js b/awx/ui/src/screens/Job/JobOutput/shared/OutputToolbar.js index ae7d06c9fc..ee16e6c671 100644 --- a/awx/ui/src/screens/Job/JobOutput/shared/OutputToolbar.js +++ b/awx/ui/src/screens/Job/JobOutput/shared/OutputToolbar.js @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; - +import { DateTime, Duration } from 'luxon'; import { t } from '@lingui/macro'; import { bool, shape, func } from 'prop-types'; import { @@ -41,18 +41,18 @@ const Wrapper = styled.div` flex-flow: row wrap; font-size: 14px; `; +const calculateElapsed = (started) => { + const now = DateTime.now(); + const duration = now + .diff(DateTime.fromISO(`${started}`), [ + 'milliseconds', + 'seconds', + 'minutes', + 'hours', + ]) + .toObject(); -const toHHMMSS = (elapsed) => { - const sec_num = parseInt(elapsed, 10); - const hours = Math.floor(sec_num / 3600); - const minutes = Math.floor(sec_num / 60) % 60; - const seconds = sec_num % 60; - - const stampHours = hours < 10 ? `0${hours}` : hours; - const stampMinutes = minutes < 10 ? `0${minutes}` : minutes; - const stampSeconds = seconds < 10 ? `0${seconds}` : seconds; - - return `${stampHours}:${stampMinutes}:${stampSeconds}`; + return Duration.fromObject({ ...duration }).toFormat('hh:mm:ss'); }; const OUTPUT_NO_COUNT_JOB_TYPES = [ @@ -62,6 +62,7 @@ const OUTPUT_NO_COUNT_JOB_TYPES = [ ]; const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => { + const [activeJobElapsedTime, setActiveJobElapsedTime] = useState('00:00:00'); const hideCounts = OUTPUT_NO_COUNT_JOB_TYPES.includes(job.type); const playCount = job?.playbook_counts?.play_count; @@ -76,6 +77,20 @@ const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => { : 0; const { me } = useConfig(); + useEffect(() => { + let secTimer; + if (job.finished) { + return () => clearInterval(secTimer); + } + + secTimer = setInterval(() => { + const elapsedTime = calculateElapsed(job.started); + setActiveJobElapsedTime(elapsedTime); + }, 1000); + + return () => clearInterval(secTimer); + }, [job.started, job.finished]); + return ( {!hideCounts && ( @@ -124,7 +139,13 @@ const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
{t`Elapsed`}
- {toHHMMSS(job.elapsed)} + + {job.finished + ? Duration.fromObject({ seconds: job.elapsed }).toFormat( + 'hh:mm:ss' + ) + : activeJobElapsedTime} +
{['pending', 'waiting', 'running'].includes(jobStatus) &&