// 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' class ChangePanel extends React.Component { static propTypes = { globalExpanded: PropTypes.bool.isRequired, change: PropTypes.object.isRequired, tenant: PropTypes.object } constructor () { super() this.state = { expanded: false } this.onClick = this.onClick.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) { let jobPercent = (100 / change.jobs.length).toFixed(2) return (
{change.jobs.map((job, idx) => { let result = this.jobStrResult(job) if (result !== 'queued') { 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': case 'skipped': className = ' progress-bar-info' break default: break } return
} else { return '' } })}
) } renderTimer (change) { let remainingTime if (change.remaining_time === null) { remainingTime = 'unknown' } else { remainingTime = this.time(change.remaining_time) } 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 (result) { let className 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 // 'in progress' 'queued' 'lost' 'aborted' 'waiting' ... default: className = 'label-default' } return ( {result} ) } renderJob (job) { const { tenant } = this.props let job_name = job.name if (job.tries > 1) { job_name = job_name + ' (' + 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.elapsed_time, job.remaining_time) } else { resultBar = this.renderJobStatusLabel(result) } return ( {name} {resultBar} {job.voting === false ? ( (non-voting)) : ''}
) } renderJobList (jobs) { return (
    {jobs.map((job, idx) => (
  • {this.renderJob(job)}
  • ))}
) } render () { const { expanded } = this.state const { change, globalExpanded } = this.props let expand = globalExpanded if (this.clicked) { expand = expanded } const header = (
{change.project}
{this.renderChangeLink(change)}
{this.renderProgressBar(change)}
{change.live === true ? (
{this.renderTimer(change)}
) : ''}
{expand ? this.renderJobList(change.jobs) : ''}
) return ( {header} ) } } export default connect(state => ({tenant: state.tenant}))(ChangePanel)