Merge "web: add errors from the job-output to the build page"

This commit is contained in:
Zuul 2018-12-31 17:42:36 +00:00 committed by Gerrit Code Review
commit 0372d3a872
4 changed files with 189 additions and 3 deletions

View File

@ -12,11 +12,14 @@
// License for the specific language governing permissions and limitations
// under the License.
import Axios from 'axios'
import * as API from '../api'
export const BUILD_FETCH_REQUEST = 'BUILD_FETCH_REQUEST'
export const BUILD_FETCH_SUCCESS = 'BUILD_FETCH_SUCCESS'
export const BUILD_FETCH_FAIL = 'BUILD_FETCH_FAIL'
export const BUILD_OUTPUT_FETCH_SUCCESS = 'BUILD_OUTPUT_FETCH_SUCCESS'
export const requestBuild = () => ({
type: BUILD_FETCH_REQUEST
@ -29,6 +32,51 @@ export const receiveBuild = (buildId, build) => ({
receivedAt: Date.now()
})
const receiveBuildOutput = (buildId, output) => {
const hosts = {}
// Compute stats
output.forEach(phase => {
Object.entries(phase.stats).forEach(([host, stats]) => {
if (!hosts[host]) {
hosts[host] = stats
hosts[host].failed = []
} else {
hosts[host].changed += stats.changed
hosts[host].failures += stats.failures
hosts[host].ok += stats.ok
}
if (stats.failures > 0) {
// Look for failed tasks
phase.plays.forEach(play => {
play.tasks.forEach(task => {
if (task.hosts[host]) {
if (task.hosts[host].results &&
task.hosts[host].results.length > 0) {
task.hosts[host].results.forEach(result => {
if (result.failed) {
result.name = task.task.name
hosts[host].failed.push(result)
}
})
} else if (task.hosts[host].rc || task.hosts[host].failed) {
let result = task.hosts[host]
result.name = task.task.name
hosts[host].failed.push(result)
}
}
})
})
}
})
})
return {
type: BUILD_OUTPUT_FETCH_SUCCESS,
buildId: buildId,
output: hosts,
receivedAt: Date.now()
}
}
const failedBuild = error => ({
type: BUILD_FETCH_FAIL,
error
@ -37,7 +85,26 @@ const failedBuild = error => ({
const fetchBuild = (tenant, build) => dispatch => {
dispatch(requestBuild())
return API.fetchBuild(tenant.apiPrefix, build)
.then(response => dispatch(receiveBuild(build, response.data)))
.then(response => {
dispatch(receiveBuild(build, response.data))
if (response.data.log_url) {
const url = response.data.log_url.substr(
0, response.data.log_url.lastIndexOf('/') + 1)
Axios.get(url + 'job-output.json.gz')
.then(response => dispatch(receiveBuildOutput(build, response.data)))
.catch(error => {
if (!error.request) {
throw error
}
// Try without compression
Axios.get(url + 'job-output.json')
.then(response => dispatch(receiveBuildOutput(
build, response.data)))
})
.catch(error => console.error(
'Couldn\'t decode job-output...', error))
}
})
.catch(error => dispatch(failedBuild(error)))
}

View File

@ -18,6 +18,8 @@ import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { Panel } from 'react-bootstrap'
import BuildOutput from './BuildOutput'
class Build extends React.Component {
static propTypes = {
@ -79,6 +81,7 @@ class Build extends React.Component {
))}
</tbody>
</table>
{build.output && <BuildOutput output={build.output}/>}
</Panel.Body>
</Panel>
)

View File

@ -0,0 +1,111 @@
// 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 { Panel } from 'react-bootstrap'
import {
Icon,
ListView,
} from 'patternfly-react'
class BuildOutput extends React.Component {
static propTypes = {
output: PropTypes.object,
}
renderHosts (hosts) {
return (
<ListView>
{Object.entries(hosts).map(([host, values]) => (
<ListView.Item
key={host}
heading={host}
additionalInfo={[
<ListView.InfoItem key="ok" title="Task OK">
<Icon type='pf' name='info' />
<strong>{values.ok}</strong>
</ListView.InfoItem>,
<ListView.InfoItem key="changed" title="Task changed">
<Icon type='pf' name='ok' />
<strong>{values.changed}</strong>
</ListView.InfoItem>,
<ListView.InfoItem key="fail" title="Task failure">
<Icon type='pf' name='error-circle-o' />
<strong>{values.failures}</strong>
</ListView.InfoItem>
]}
/>
))}
</ListView>
)
}
renderFailedTask (host, task) {
return (
<Panel key={host + task.zuul_log_id}>
<Panel.Heading>{host}: {task.name}</Panel.Heading>
<Panel.Body>
{task.invocation && task.invocation.module_args &&
task.invocation.module_args._raw_params && (
<strong key="cmd">
{task.invocation.module_args._raw_params} <br />
</strong>
)}
{task.msg && (
<pre key="msg">{task.msg}</pre>
)}
{task.exception && (
<pre key="exc">{task.exception}</pre>
)}
{task.stdout_lines && task.stdout_lines.length > 0 && (
<span key="stdout" style={{whiteSpace: 'pre'}} title="stdout">
{task.stdout_lines.slice(-42).map((line, idx) => (
<span key={idx}>{line}<br/></span>))}
<br />
</span>
)}
{task.stderr_lines && task.stderr_lines.length > 0 && (
<span key="stderr" style={{whiteSpace: 'pre'}} title="stderr">
{task.stderr_lines.slice(-42).map((line, idx) => (
<span key={idx}>{line}<br/></span>))}
<br />
</span>
)}
</Panel.Body>
</Panel>
)
}
render () {
const { output } = this.props
return (
<React.Fragment>
<div key="tasks">
{Object.entries(output)
.filter(([, values]) => values.failed.length > 0)
.map(([host, values]) => (values.failed.map(failed => (
this.renderFailedTask(host, failed)))))}
</div>
<div key="hosts">
{this.renderHosts(output)}
</div>
</React.Fragment>
)
}
}
export default BuildOutput

View File

@ -12,13 +12,15 @@
// License for the specific language governing permissions and limitations
// under the License.
import update from 'immutability-helper'
import {
BUILD_FETCH_FAIL,
BUILD_FETCH_REQUEST,
BUILD_FETCH_SUCCESS
BUILD_FETCH_SUCCESS,
BUILD_OUTPUT_FETCH_SUCCESS
} from '../actions/build'
import update from 'immutability-helper'
export default (state = {
isFetching: false,
@ -33,6 +35,9 @@ export default (state = {
return update(state, {$merge: {isFetching: false}})
case BUILD_FETCH_FAIL:
return update(state, {$merge: {isFetching: false}})
case BUILD_OUTPUT_FETCH_SUCCESS:
return update(
state, {builds: {[action.buildId]: {$merge: {output: action.output}}}})
default:
return state
}