Make helper functions available to other components

This moves some of the helper functions used to format and display
various information of a QueueItem to a more generic file so they can be
used by other components as well.

In addition, the render methods for the ChangeLink and
(QueueItem)Progressbar where changed to React Components and now use
newer PF4 components for styling.

Finally, this changes fixes a typo in the calculateQueueItemTimes
function to make the fallback to null work.

Change-Id: Ia7f3f214dfa1dbc041c1dcb821ff478980d81011
This commit is contained in:
Felix Edel
2024-02-22 14:01:44 +01:00
parent 4790535efe
commit 037ba1f651
3 changed files with 190 additions and 151 deletions

View File

@@ -14,6 +14,7 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import * as moment from 'moment'
import { ExternalLinkAltIcon } from '@patternfly/react-icons'
function removeHash() {
@@ -111,11 +112,11 @@ function renderRefInfo(ref) {
</>
)
const oldrev = ref.oldrev ? (
<><br/><strong>Old</strong> {ref.oldrev}</>
) : ( <></> )
<><br /><strong>Old</strong> {ref.oldrev}</>
) : (<></>)
const newrev = ref.newrev ? (
<><br/><strong>New</strong> {ref.newrev}</>
) : ( <></> )
<><br /><strong>New</strong> {ref.newrev}</>
) : (<></>)
return (
<>
@@ -180,4 +181,25 @@ function setDarkMode(darkMode) {
}
}
export { IconProperty, removeHash, ExternalLink, buildExternalLink, buildExternalTableLink, describeRef, renderRefInfo, ConditionalWrapper, resolveDarkMode, setDarkMode }
function formatTime(ms) {
return moment.duration(ms).format({
template: 'h [hr] m [min]',
largest: 2,
minValue: 1,
usePlural: false,
})
}
export {
buildExternalLink,
buildExternalTableLink,
ConditionalWrapper,
describeRef,
ExternalLink,
formatTime,
IconProperty,
removeHash,
renderRefInfo,
resolveDarkMode,
setDarkMode,
}

View File

@@ -21,6 +21,14 @@ import * as moment from 'moment'
import 'moment-duration-format'
import { Button } from '@patternfly/react-core'
import {
calculateQueueItemTimes,
ChangeLink,
getJobStrResult,
QueueItemProgressbar,
} from './Misc'
import { formatTime } from '../../Misc'
function getRefs(item) {
// For backwards compat: get a list of this items refs.
return 'refs' in item ? item.refs : [item]
@@ -58,22 +66,13 @@ class ItemPanel extends React.Component {
this.setState({ expanded: !expanded })
}
time (ms) {
return moment.duration(ms).format({
template: 'h [hr] m [min]',
largest: 2,
minValue: 1,
usePlural: false,
})
}
enqueueTime (ms) {
// Special format case for enqueue time to add style
let hours = 60 * 60 * 1000
let now = Date.now()
let delta = now - ms
let status = 'text-success'
let text = this.time(delta)
let text = formatTime(delta)
if (delta > (4 * hours)) {
status = 'text-danger'
} else if (delta > (2 * hours)) {
@@ -82,101 +81,12 @@ class ItemPanel extends React.Component {
return <span className={status}>{text}</span>
}
jobStrResult (job) {
let result = job.result ? job.result.toLowerCase() : null
if (result === null) {
if (job.url === null) {
if (job.queued === false) {
result = 'waiting'
} else {
result = 'queued'
}
} else if (job.paused !== null && job.paused) {
result = 'paused'
} else {
result = 'in progress'
}
}
return result
}
renderChangeLink (change) {
let changeId = change.id || 'NA'
let changeTitle = changeId
// Fall back to display the ref if there is no change id
if (changeId === 'NA' && change.ref) {
changeTitle = change.ref
}
let changeText = ''
if (change.url !== null) {
let githubId = changeId.match(/^([0-9]+),([0-9a-f]{40})$/)
if (githubId) {
changeTitle = githubId
changeText = '#' + githubId[1]
} else if (/^[0-9a-f]{40}$/.test(changeId)) {
changeText = changeId.slice(0, 7)
}
} else if (changeId.length === 40) {
changeText = changeId.slice(0, 7)
}
return (
<small>
<a href={change.url} onClick={(e) => e.stopPropagation()}>
{changeText !== '' ? (
<abbr title={changeTitle}>{changeText}</abbr>) : changeTitle}
</a>
</small>)
}
renderProgressBar (change) {
const interesting_jobs = change.jobs.filter(j => this.jobStrResult(j) !== 'skipped')
let jobPercent = (100 / interesting_jobs.length).toFixed(2)
return (
<div className={`progress zuul-change-total-result${this.props.preferences.darkMode ? ' progress-dark' : ''}`}>
{change.jobs.map((job, idx) => {
let result = this.jobStrResult(job)
if (['queued', 'waiting', 'skipped'].includes(result)) {
return ''
}
let className = ''
switch (result) {
case 'success':
className = ' progress-bar-success'
break
case 'lost':
case 'failure':
className = ' progress-bar-danger'
break
case 'unstable':
case 'retry_limit':
case 'post_failure':
case 'node_failure':
className = ' progress-bar-warning'
break
case 'paused':
className = ' progress-bar-info'
break
default:
if (job.pre_fail) {
className = ' progress-bar-danger'
}
break
}
return <div className={'progress-bar' + className}
key={idx}
title={job.name}
style={{width: jobPercent + '%'}}/>
})}
</div>
)
}
renderTimer (change, times) {
let remainingTime
if (times.remaining === null) {
remainingTime = 'unknown'
} else {
remainingTime = this.time(times.remaining)
remainingTime = formatTime(times.remaining)
}
return (
<React.Fragment>
@@ -300,7 +210,7 @@ class ItemPanel extends React.Component {
name = <span className='zuul-job-name'>{job_name}</span>
}
let resultBar
let result = this.jobStrResult(job)
let result = getJobStrResult(job)
if (result === 'in progress') {
resultBar = this.renderJobProgressBar(job, job_times.elapsed, job_times.remaining)
} else {
@@ -328,7 +238,7 @@ class ItemPanel extends React.Component {
renderJobList (jobs, times) {
const [buttonText, interestingJobs] = this.state.showSkipped ?
['Hide', jobs] :
['Show', jobs.filter(j => this.jobStrResult(j) !== 'skipped')]
['Show', jobs.filter(j => getJobStrResult(j) !== 'skipped')]
const skippedJobCount = jobs.length - interestingJobs.length
return (
@@ -352,46 +262,6 @@ class ItemPanel extends React.Component {
)
}
calculateTimes (item) {
let maxRemaining = 0
let jobs = {}
const now = Date.now()
for (const job of item.jobs) {
let jobElapsed = null
let jobRemaining = null
if (job.start_time) {
let jobStart = parseInt(job.start_time * 1000)
if (job.end_time) {
let jobEnd = parseInt(job.end_time * 1000)
jobElapsed = jobEnd - jobStart
} else {
jobElapsed = Math.max(now - jobStart, 0)
if (job.estimated_time) {
jobRemaining = Math.max(parseInt(job.estimated_time * 1000) - jobElapsed, 0)
}
}
}
if (jobRemaining && jobRemaining > maxRemaining) {
maxRemaining = jobRemaining
}
jobs[job.name] = {
elapsed: jobElapsed,
remaining: jobRemaining,
}
}
// If not all the jobs have started, this will be null, so only
// use our value if it's oky to calculate it.
if (item.remaininging_time === null) {
maxRemaining = null
}
return {
remaining: maxRemaining,
jobs: jobs,
}
}
render () {
const { expanded } = this.state
const { item, globalExpanded } = this.props
@@ -399,7 +269,7 @@ class ItemPanel extends React.Component {
if (this.clicked) {
expand = expanded
}
const times = this.calculateTimes(item)
const times = calculateQueueItemTimes(item)
const header = (
<div className={`panel panel-default ${this.props.preferences.darkMode ? 'zuul-change-dark' : 'zuul-change'}`}>
<div className={`panel-heading ${this.props.preferences.darkMode ? 'zuul-patchset-header-dark' : 'zuul-patchset-header'}`}
@@ -408,7 +278,7 @@ class ItemPanel extends React.Component {
{item.live === true ? (
<div className='row'>
<div className='col-xs-6'>
{this.renderProgressBar(item)}
<QueueItemProgressbar item={item} darkMode={this.props.preferences.darkMode} />
</div>
<div className='col-xs-6 text-right'>
{this.renderTimer(item, times)}
@@ -421,7 +291,7 @@ class ItemPanel extends React.Component {
<span className='change_project'>{change.project}</span>
</div>
<div className='col-xs-4 text-right'>
{this.renderChangeLink(change)}
<ChangeLink change={change} />
</div>
</div>
))}

View File

@@ -1,5 +1,6 @@
// Copyright 2018 Red Hat, Inc
// Copyright 2020 BMW Group
// Copyright 2024 Acme Gating, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
@@ -29,6 +30,8 @@ import {
TimesIcon,
} from '@patternfly/react-icons'
import { ExternalLink } from '../../Misc'
const QUEUE_ITEM_ICON_CONFIGS = {
SUCCESS: {
icon: CheckIcon,
@@ -148,4 +151,148 @@ PipelineIcon.propTypes = {
size: PropTypes.string,
}
export { getQueueItemIconConfig, PipelineIcon }
function ChangeLink({ change }) {
let changeId = change.id || 'NA'
let changeTitle = changeId
// Fall back to display the ref if there is no change id
if (changeId === 'NA' && change.ref) {
changeTitle = change.ref
}
let changeText = ''
if (change.url !== null) {
let githubId = changeId.match(/^([0-9]+),([0-9a-f]{40})$/)
if (githubId) {
changeTitle = githubId
changeText = '#' + githubId[1]
} else if (/^[0-9a-f]{40}$/.test(changeId)) {
changeText = changeId.slice(0, 7)
}
} else if (changeId.length === 40) {
changeText = changeId.slice(0, 7)
}
return (
<ExternalLink target={change.url}>
{changeText !== '' ? changeText : changeTitle}
</ExternalLink>
)
}
ChangeLink.propTypes = {
change: PropTypes.object,
}
const getJobStrResult = (job) => {
let result = job.result ? job.result.toLowerCase() : null
if (result === null) {
if (job.url === null) {
if (job.queued === false) {
result = 'waiting'
} else {
result = 'queued'
}
} else if (job.paused !== null && job.paused) {
result = 'paused'
} else {
result = 'in progress'
}
}
return result
}
const calculateQueueItemTimes = (item) => {
let maxRemaining = 0
let jobs = {}
const now = Date.now()
for (const job of item.jobs) {
let jobElapsed = null
let jobRemaining = null
if (job.start_time) {
let jobStart = parseInt(job.start_time * 1000)
if (job.end_time) {
let jobEnd = parseInt(job.end_time * 1000)
jobElapsed = jobEnd - jobStart
} else {
jobElapsed = Math.max(now - jobStart, 0)
if (job.estimated_time) {
jobRemaining = Math.max(parseInt(job.estimated_time * 1000) - jobElapsed, 0)
}
}
}
if (jobRemaining && jobRemaining > maxRemaining) {
maxRemaining = jobRemaining
}
jobs[job.name] = {
elapsed: jobElapsed,
remaining: jobRemaining,
}
}
// If not all the jobs have started, this will be null, so only
// use our value if it's oky to calculate it.
if (item.remaining_time === null) {
maxRemaining = null
}
return {
remaining: maxRemaining,
jobs: jobs,
}
}
function QueueItemProgressbar({ item, darkMode }) {
// TODO (felix): Use a PF4 progress bar instead
const interesting_jobs = item.jobs.filter(j => getJobStrResult(j) !== 'skipped')
let jobPercent = (100 / interesting_jobs.length).toFixed(2)
return (
<div className={`progress zuul-change-total-result${darkMode ? ' progress-dark' : ''}`}>
{item.jobs.map((job, idx) => {
let result = getJobStrResult(job)
if (['queued', 'waiting', 'skipped'].includes(result)) {
return ''
}
let className = ''
switch (result) {
case 'success':
className = ' progress-bar-success'
break
case 'lost':
case 'failure':
className = ' progress-bar-danger'
break
case 'unstable':
case 'retry_limit':
case 'post_failure':
case 'node_failure':
className = ' progress-bar-warning'
break
case 'paused':
className = ' progress-bar-info'
break
default:
if (job.pre_fail) {
className = ' progress-bar-danger'
}
break
}
return <div className={'progress-bar' + className}
key={idx}
title={job.name}
style={{ width: jobPercent + '%' }} />
})}
</div>
)
}
QueueItemProgressbar.propTypes = {
item: PropTypes.object,
darkMode: PropTypes.bool,
}
export {
calculateQueueItemTimes,
ChangeLink,
getJobStrResult,
getQueueItemIconConfig,
QueueItemProgressbar,
PipelineIcon,
}