diff --git a/web/src/actions/build.js b/web/src/actions/build.js index 78f69f93bf..b291c88b8e 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 6df86a550b..8ce620c855 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 { ))} + {build.output && } ) diff --git a/web/src/containers/build/BuildOutput.jsx b/web/src/containers/build/BuildOutput.jsx new file mode 100644 index 0000000000..a06c3abd86 --- /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 ( + + {Object.entries(hosts).map(([host, values]) => ( + + + {values.ok} + , + + + {values.changed} + , + + + {values.failures} + + ]} + /> + ))} + + ) + } + + renderFailedTask (host, task) { + return ( + + {host}: {task.name} + + {task.invocation && task.invocation.module_args && + task.invocation.module_args._raw_params && ( + + {task.invocation.module_args._raw_params}
+
+ )} + {task.msg && ( +
{task.msg}
+ )} + {task.exception && ( +
{task.exception}
+ )} + {task.stdout_lines && task.stdout_lines.length > 0 && ( + + {task.stdout_lines.slice(-42).map((line, idx) => ( + {line}
))} +
+
+ )} + {task.stderr_lines && task.stderr_lines.length > 0 && ( + + {task.stderr_lines.slice(-42).map((line, idx) => ( + {line}
))} +
+
+ )} +
+
+ ) + } + + render () { + const { output } = this.props + return ( + +
+ {Object.entries(output) + .filter(([, values]) => values.failed.length > 0) + .map(([host, values]) => (values.failed.map(failed => ( + this.renderFailedTask(host, failed)))))} +
+
+ {this.renderHosts(output)} +
+
+ ) + } +} + + +export default BuildOutput diff --git a/web/src/reducers/build.js b/web/src/reducers/build.js index 86aaad86d9..244f5a3561 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 }