summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 78f69f93b..b291c88b8 100644
--- a/web/src/actions/build.js
+++ b/web/src/actions/build.js
@@ -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)))
}
diff --git a/web/src/containers/build/Build.jsx b/web/src/containers/build/Build.jsx
index 6df86a550..8ce620c85 100644
--- a/web/src/containers/build/Build.jsx
+++ b/web/src/containers/build/Build.jsx
@@ -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>
)
diff --git a/web/src/containers/build/BuildOutput.jsx b/web/src/containers/build/BuildOutput.jsx
new file mode 100644
index 000000000..a06c3abd8
--- /dev/null
+++ b/web/src/containers/build/BuildOutput.jsx
@@ -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
diff --git a/web/src/reducers/build.js b/web/src/reducers/build.js
index 86aaad86d..244f5a356 100644
--- a/web/src/reducers/build.js
+++ b/web/src/reducers/build.js
@@ -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
}