Merge "web: add errors from the job-output to the build page"
This commit is contained in:
commit
0372d3a872
|
@ -12,11 +12,14 @@
|
||||||
// License for the specific language governing permissions and limitations
|
// License for the specific language governing permissions and limitations
|
||||||
// under the License.
|
// under the License.
|
||||||
|
|
||||||
|
import Axios from 'axios'
|
||||||
|
|
||||||
import * as API from '../api'
|
import * as API from '../api'
|
||||||
|
|
||||||
export const BUILD_FETCH_REQUEST = 'BUILD_FETCH_REQUEST'
|
export const BUILD_FETCH_REQUEST = 'BUILD_FETCH_REQUEST'
|
||||||
export const BUILD_FETCH_SUCCESS = 'BUILD_FETCH_SUCCESS'
|
export const BUILD_FETCH_SUCCESS = 'BUILD_FETCH_SUCCESS'
|
||||||
export const BUILD_FETCH_FAIL = 'BUILD_FETCH_FAIL'
|
export const BUILD_FETCH_FAIL = 'BUILD_FETCH_FAIL'
|
||||||
|
export const BUILD_OUTPUT_FETCH_SUCCESS = 'BUILD_OUTPUT_FETCH_SUCCESS'
|
||||||
|
|
||||||
export const requestBuild = () => ({
|
export const requestBuild = () => ({
|
||||||
type: BUILD_FETCH_REQUEST
|
type: BUILD_FETCH_REQUEST
|
||||||
|
@ -29,6 +32,51 @@ export const receiveBuild = (buildId, build) => ({
|
||||||
receivedAt: Date.now()
|
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 => ({
|
const failedBuild = error => ({
|
||||||
type: BUILD_FETCH_FAIL,
|
type: BUILD_FETCH_FAIL,
|
||||||
error
|
error
|
||||||
|
@ -37,7 +85,26 @@ const failedBuild = error => ({
|
||||||
const fetchBuild = (tenant, build) => dispatch => {
|
const fetchBuild = (tenant, build) => dispatch => {
|
||||||
dispatch(requestBuild())
|
dispatch(requestBuild())
|
||||||
return API.fetchBuild(tenant.apiPrefix, build)
|
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)))
|
.catch(error => dispatch(failedBuild(error)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { connect } from 'react-redux'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Panel } from 'react-bootstrap'
|
import { Panel } from 'react-bootstrap'
|
||||||
|
|
||||||
|
import BuildOutput from './BuildOutput'
|
||||||
|
|
||||||
|
|
||||||
class Build extends React.Component {
|
class Build extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -79,6 +81,7 @@ class Build extends React.Component {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{build.output && <BuildOutput output={build.output}/>}
|
||||||
</Panel.Body>
|
</Panel.Body>
|
||||||
</Panel>
|
</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
|
// License for the specific language governing permissions and limitations
|
||||||
// under the License.
|
// under the License.
|
||||||
|
|
||||||
|
import update from 'immutability-helper'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BUILD_FETCH_FAIL,
|
BUILD_FETCH_FAIL,
|
||||||
BUILD_FETCH_REQUEST,
|
BUILD_FETCH_REQUEST,
|
||||||
BUILD_FETCH_SUCCESS
|
BUILD_FETCH_SUCCESS,
|
||||||
|
BUILD_OUTPUT_FETCH_SUCCESS
|
||||||
} from '../actions/build'
|
} from '../actions/build'
|
||||||
|
|
||||||
import update from 'immutability-helper'
|
|
||||||
|
|
||||||
export default (state = {
|
export default (state = {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
@ -33,6 +35,9 @@ export default (state = {
|
||||||
return update(state, {$merge: {isFetching: false}})
|
return update(state, {$merge: {isFetching: false}})
|
||||||
case BUILD_FETCH_FAIL:
|
case BUILD_FETCH_FAIL:
|
||||||
return update(state, {$merge: {isFetching: false}})
|
return update(state, {$merge: {isFetching: false}})
|
||||||
|
case BUILD_OUTPUT_FETCH_SUCCESS:
|
||||||
|
return update(
|
||||||
|
state, {builds: {[action.buildId]: {$merge: {output: action.output}}}})
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue