summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-12-31 17:42:36 +0000
committerGerrit Code Review <review@openstack.org>2018-12-31 17:42:36 +0000
commit0372d3a87282fd2dca572368a1c904c09b38ebdc (patch)
tree2330e697fcf36172879b34b1f00484eb2696499f
parent972f91962c45d1e91706723f207fa6fb0ec5db5f (diff)
parent2f8a1be7c9a5ad6b6afe4512817be2948d9c41c5 (diff)
Merge "web: add errors from the job-output to the build page"
-rw-r--r--web/src/actions/build.js69
-rw-r--r--web/src/containers/build/Build.jsx3
-rw-r--r--web/src/containers/build/BuildOutput.jsx111
-rw-r--r--web/src/reducers/build.js9
4 files changed, 189 insertions, 3 deletions
diff --git a/web/src/actions/build.js b/web/src/actions/build.js
index 78f69f9..b291c88 100644
--- a/web/src/actions/build.js
+++ b/web/src/actions/build.js
@@ -12,11 +12,14 @@
12// License for the specific language governing permissions and limitations 12// License for the specific language governing permissions and limitations
13// under the License. 13// under the License.
14 14
15import Axios from 'axios'
16
15import * as API from '../api' 17import * as API from '../api'
16 18
17export const BUILD_FETCH_REQUEST = 'BUILD_FETCH_REQUEST' 19export const BUILD_FETCH_REQUEST = 'BUILD_FETCH_REQUEST'
18export const BUILD_FETCH_SUCCESS = 'BUILD_FETCH_SUCCESS' 20export const BUILD_FETCH_SUCCESS = 'BUILD_FETCH_SUCCESS'
19export const BUILD_FETCH_FAIL = 'BUILD_FETCH_FAIL' 21export const BUILD_FETCH_FAIL = 'BUILD_FETCH_FAIL'
22export const BUILD_OUTPUT_FETCH_SUCCESS = 'BUILD_OUTPUT_FETCH_SUCCESS'
20 23
21export const requestBuild = () => ({ 24export const requestBuild = () => ({
22 type: BUILD_FETCH_REQUEST 25 type: BUILD_FETCH_REQUEST
@@ -29,6 +32,51 @@ export const receiveBuild = (buildId, build) => ({
29 receivedAt: Date.now() 32 receivedAt: Date.now()
30}) 33})
31 34
35const receiveBuildOutput = (buildId, output) => {
36 const hosts = {}
37 // Compute stats
38 output.forEach(phase => {
39 Object.entries(phase.stats).forEach(([host, stats]) => {
40 if (!hosts[host]) {
41 hosts[host] = stats
42 hosts[host].failed = []
43 } else {
44 hosts[host].changed += stats.changed
45 hosts[host].failures += stats.failures
46 hosts[host].ok += stats.ok
47 }
48 if (stats.failures > 0) {
49 // Look for failed tasks
50 phase.plays.forEach(play => {
51 play.tasks.forEach(task => {
52 if (task.hosts[host]) {
53 if (task.hosts[host].results &&
54 task.hosts[host].results.length > 0) {
55 task.hosts[host].results.forEach(result => {
56 if (result.failed) {
57 result.name = task.task.name
58 hosts[host].failed.push(result)
59 }
60 })
61 } else if (task.hosts[host].rc || task.hosts[host].failed) {
62 let result = task.hosts[host]
63 result.name = task.task.name
64 hosts[host].failed.push(result)
65 }
66 }
67 })
68 })
69 }
70 })
71 })
72 return {
73 type: BUILD_OUTPUT_FETCH_SUCCESS,
74 buildId: buildId,
75 output: hosts,
76 receivedAt: Date.now()
77 }
78}
79
32const failedBuild = error => ({ 80const failedBuild = error => ({
33 type: BUILD_FETCH_FAIL, 81 type: BUILD_FETCH_FAIL,
34 error 82 error
@@ -37,7 +85,26 @@ const failedBuild = error => ({
37const fetchBuild = (tenant, build) => dispatch => { 85const fetchBuild = (tenant, build) => dispatch => {
38 dispatch(requestBuild()) 86 dispatch(requestBuild())
39 return API.fetchBuild(tenant.apiPrefix, build) 87 return API.fetchBuild(tenant.apiPrefix, build)
40 .then(response => dispatch(receiveBuild(build, response.data))) 88 .then(response => {
89 dispatch(receiveBuild(build, response.data))
90 if (response.data.log_url) {
91 const url = response.data.log_url.substr(
92 0, response.data.log_url.lastIndexOf('/') + 1)
93 Axios.get(url + 'job-output.json.gz')
94 .then(response => dispatch(receiveBuildOutput(build, response.data)))
95 .catch(error => {
96 if (!error.request) {
97 throw error
98 }
99 // Try without compression
100 Axios.get(url + 'job-output.json')
101 .then(response => dispatch(receiveBuildOutput(
102 build, response.data)))
103 })
104 .catch(error => console.error(
105 'Couldn\'t decode job-output...', error))
106 }
107 })
41 .catch(error => dispatch(failedBuild(error))) 108 .catch(error => dispatch(failedBuild(error)))
42} 109}
43 110
diff --git a/web/src/containers/build/Build.jsx b/web/src/containers/build/Build.jsx
index 6df86a5..8ce620c 100644
--- a/web/src/containers/build/Build.jsx
+++ b/web/src/containers/build/Build.jsx
@@ -18,6 +18,8 @@ import { connect } from 'react-redux'
18import { Link } from 'react-router-dom' 18import { Link } from 'react-router-dom'
19import { Panel } from 'react-bootstrap' 19import { Panel } from 'react-bootstrap'
20 20
21import BuildOutput from './BuildOutput'
22
21 23
22class Build extends React.Component { 24class Build extends React.Component {
23 static propTypes = { 25 static propTypes = {
@@ -79,6 +81,7 @@ class Build extends React.Component {
79 ))} 81 ))}
80 </tbody> 82 </tbody>
81 </table> 83 </table>
84 {build.output && <BuildOutput output={build.output}/>}
82 </Panel.Body> 85 </Panel.Body>
83 </Panel> 86 </Panel>
84 ) 87 )
diff --git a/web/src/containers/build/BuildOutput.jsx b/web/src/containers/build/BuildOutput.jsx
new file mode 100644
index 0000000..a06c3ab
--- /dev/null
+++ b/web/src/containers/build/BuildOutput.jsx
@@ -0,0 +1,111 @@
1// Copyright 2018 Red Hat, Inc
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may
4// not use this file except in compliance with the License. You may obtain
5// a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations
13// under the License.
14
15import * as React from 'react'
16import PropTypes from 'prop-types'
17import { Panel } from 'react-bootstrap'
18import {
19 Icon,
20 ListView,
21} from 'patternfly-react'
22
23
24class BuildOutput extends React.Component {
25 static propTypes = {
26 output: PropTypes.object,
27 }
28
29 renderHosts (hosts) {
30 return (
31 <ListView>
32 {Object.entries(hosts).map(([host, values]) => (
33 <ListView.Item
34 key={host}
35 heading={host}
36 additionalInfo={[
37 <ListView.InfoItem key="ok" title="Task OK">
38 <Icon type='pf' name='info' />
39 <strong>{values.ok}</strong>
40 </ListView.InfoItem>,
41 <ListView.InfoItem key="changed" title="Task changed">
42 <Icon type='pf' name='ok' />
43 <strong>{values.changed}</strong>
44 </ListView.InfoItem>,
45 <ListView.InfoItem key="fail" title="Task failure">
46 <Icon type='pf' name='error-circle-o' />
47 <strong>{values.failures}</strong>
48 </ListView.InfoItem>
49 ]}
50 />
51 ))}
52 </ListView>
53 )
54 }
55
56 renderFailedTask (host, task) {
57 return (
58 <Panel key={host + task.zuul_log_id}>
59 <Panel.Heading>{host}: {task.name}</Panel.Heading>
60 <Panel.Body>
61 {task.invocation && task.invocation.module_args &&
62 task.invocation.module_args._raw_params && (
63 <strong key="cmd">
64 {task.invocation.module_args._raw_params} <br />
65 </strong>
66 )}
67 {task.msg && (
68 <pre key="msg">{task.msg}</pre>
69 )}
70 {task.exception && (
71 <pre key="exc">{task.exception}</pre>
72 )}
73 {task.stdout_lines && task.stdout_lines.length > 0 && (
74 <span key="stdout" style={{whiteSpace: 'pre'}} title="stdout">
75 {task.stdout_lines.slice(-42).map((line, idx) => (
76 <span key={idx}>{line}<br/></span>))}
77 <br />
78 </span>
79 )}
80 {task.stderr_lines && task.stderr_lines.length > 0 && (
81 <span key="stderr" style={{whiteSpace: 'pre'}} title="stderr">
82 {task.stderr_lines.slice(-42).map((line, idx) => (
83 <span key={idx}>{line}<br/></span>))}
84 <br />
85 </span>
86 )}
87 </Panel.Body>
88 </Panel>
89 )
90 }
91
92 render () {
93 const { output } = this.props
94 return (
95 <React.Fragment>
96 <div key="tasks">
97 {Object.entries(output)
98 .filter(([, values]) => values.failed.length > 0)
99 .map(([host, values]) => (values.failed.map(failed => (
100 this.renderFailedTask(host, failed)))))}
101 </div>
102 <div key="hosts">
103 {this.renderHosts(output)}
104 </div>
105 </React.Fragment>
106 )
107 }
108}
109
110
111export default BuildOutput
diff --git a/web/src/reducers/build.js b/web/src/reducers/build.js
index 86aaad8..244f5a3 100644
--- a/web/src/reducers/build.js
+++ b/web/src/reducers/build.js
@@ -12,13 +12,15 @@
12// License for the specific language governing permissions and limitations 12// License for the specific language governing permissions and limitations
13// under the License. 13// under the License.
14 14
15import update from 'immutability-helper'
16
15import { 17import {
16 BUILD_FETCH_FAIL, 18 BUILD_FETCH_FAIL,
17 BUILD_FETCH_REQUEST, 19 BUILD_FETCH_REQUEST,
18 BUILD_FETCH_SUCCESS 20 BUILD_FETCH_SUCCESS,
21 BUILD_OUTPUT_FETCH_SUCCESS
19} from '../actions/build' 22} from '../actions/build'
20 23
21import update from 'immutability-helper'
22 24
23export default (state = { 25export default (state = {
24 isFetching: false, 26 isFetching: false,
@@ -33,6 +35,9 @@ export default (state = {
33 return update(state, {$merge: {isFetching: false}}) 35 return update(state, {$merge: {isFetching: false}})
34 case BUILD_FETCH_FAIL: 36 case BUILD_FETCH_FAIL:
35 return update(state, {$merge: {isFetching: false}}) 37 return update(state, {$merge: {isFetching: false}})
38 case BUILD_OUTPUT_FETCH_SUCCESS:
39 return update(
40 state, {builds: {[action.buildId]: {$merge: {output: action.output}}}})
36 default: 41 default:
37 return state 42 return state
38 } 43 }