// Copyright 2018 Red Hat, Inc
//
// 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
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
import * as React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import * as moment from 'moment'
import 'moment-duration-format'
import { Button } from '@patternfly/react-core'
class ChangePanel extends React.Component {
static propTypes = {
globalExpanded: PropTypes.bool.isRequired,
change: PropTypes.object.isRequired,
tenant: PropTypes.object,
preferences: PropTypes.object
}
constructor () {
super()
this.state = {
expanded: false,
showSkipped: false,
}
this.onClick = this.onClick.bind(this)
this.toggleSkippedJobs = this.toggleSkippedJobs.bind(this)
this.clicked = false
}
onClick (e) {
// Skip middle mouse button
if (e.button === 1) {
return
}
let expanded = this.state.expanded
if (!this.clicked) {
expanded = this.props.globalExpanded
}
this.clicked = true
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)
if (delta > (4 * hours)) {
status = 'text-danger'
} else if (delta > (2 * hours)) {
status = 'text-warning'
}
return {text}
}
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 (
{changeText !== '' ? (
{changeText}) : changeTitle}
)
}
renderProgressBar (change) {
const interesting_jobs = change.jobs.filter(j => this.jobStrResult(j) !== 'skipped')
let jobPercent = (100 / interesting_jobs.length).toFixed(2)
return (
{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:
break
}
return
})}
)
}
renderTimer (change, times) {
let remainingTime
if (times.remaining === null) {
remainingTime = 'unknown'
} else {
remainingTime = this.time(times.remaining)
}
return (
{remainingTime}
{this.enqueueTime(change.enqueue_time)}
)
}
renderJobProgressBar (elapsedTime, remainingTime) {
let progressPercent = 100 * (elapsedTime / (elapsedTime +
remainingTime))
// Show animation in preparation phase
let className
let progressWidth = progressPercent
let title = ''
let remaining = remainingTime
if (Number.isNaN(progressPercent)) {
progressWidth = 100
progressPercent = 0
className = 'progress-bar-striped progress-bar-animated'
}
if (remaining !== null) {
title = 'Estimated time remaining: ' + moment.duration(remaining).format({
template: 'd [days] h [hours] m [minutes] s [seconds]',
largest: 2,
minValue: 30,
})
}
return (
)
}
renderJobStatusLabel (job, result) {
let className, title
switch (result) {
case 'success':
className = 'label-success'
break
case 'failure':
className = 'label-danger'
break
case 'unstable':
case 'retry_limit':
case 'post_failure':
case 'node_failure':
className = 'label-warning'
break
case 'paused':
case 'skipped':
className = 'label-info'
break
case 'waiting':
className = 'label-default'
if (job.waiting_status !== null) {
title = 'Waiting on ' + job.waiting_status
}
break
case 'queued':
className = 'label-default'
if (job.waiting_status !== null) {
title = 'Waiting on ' + job.waiting_status
}
break
// 'in progress' 'lost' 'aborted' ...
default:
className = 'label-default'
}
return (
{result}
)
}
renderJob (job, job_times) {
const { tenant } = this.props
let job_name = job.name
let ordinal_rules = new Intl.PluralRules('en', {type: 'ordinal'})
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
}
if (job.tries > 1) {
job_name = job_name + ' (' + job.tries + suffixes[ordinal_rules.select(job.tries)] + ' attempt)'
}
let name = ''
if (job.result !== null) {
name = {job_name}
} else if (job.url !== null) {
let url = job.url
if (job.url.match('stream/')) {
const to = (
tenant.linkPrefix + '/' + job.url
)
name = {job_name}
} else {
name = {job_name}
}
} else {
name = {job_name}
}
let resultBar
let result = this.jobStrResult(job)
if (result === 'in progress') {
resultBar = this.renderJobProgressBar(job_times.elapsed, job_times.remaining)
} else {
resultBar = this.renderJobStatusLabel(job, result)
}
return (
{name}
{resultBar}
{job.voting === false ? (
(non-voting)) : ''}
)
}
toggleSkippedJobs (e) {
// Skip middle mouse button
if (e.button === 1) {
return
}
this.setState({ showSkipped: !this.state.showSkipped })
}
renderJobList (jobs, times) {
const [buttonText, interestingJobs] = this.state.showSkipped ?
['Hide', jobs] :
['Show', jobs.filter(j => this.jobStrResult(j) !== 'skipped')]
const skippedJobCount = jobs.length - interestingJobs.length
return (
<>
>
)
}
calculateTimes (change) {
let maxRemaining = 0
let jobs = {}
const now = Date.now()
for (const job of change.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 (change.remaininging_time === null) {
maxRemaining = null
}
return {
remaining: maxRemaining,
jobs: jobs,
}
}
render () {
const { expanded } = this.state
const { change, globalExpanded } = this.props
let expand = globalExpanded
if (this.clicked) {
expand = expanded
}
const times = this.calculateTimes(change)
const header = (