// 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 ( <>
    {interestingJobs.map((job, idx) => (
  • {this.renderJob(job, times.jobs[job.name])}
  • ))} {(this.state.showSkipped || skippedJobCount) ? (
  • ) : ''}
) } 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 = (
{change.project}
{this.renderChangeLink(change)}
{this.renderProgressBar(change)}
{change.live === true ? (
{this.renderTimer(change, times)}
) : ''}
{expand ? this.renderJobList(change.jobs, times) : ''}
) return ( {header} ) } } export default connect(state => ({ tenant: state.tenant, preferences: state.preferences, }))(ChangePanel)