web: add errors from the job-output to the build page
This change updates the build page to fetch the job-output.json file and display the failed tasks. Logserver needs to enables CORS header for the zuul-web service. Change-Id: Ied9d1bb6489f608bc5402a98c8ae3a24b37b91e2
This commit is contained in:
parent
ed1d588785
commit
2f8a1be7c9
|
@ -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)))
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue