Merge "Render console in js"

This commit is contained in:
Zuul
2019-08-08 16:28:46 +00:00
committed by Gerrit Code Review
5 changed files with 424 additions and 3 deletions

View File

@@ -91,7 +91,8 @@ const receiveBuildOutput = (buildId, output) => {
return {
type: BUILD_OUTPUT_SUCCESS,
buildId: buildId,
output: hosts,
hosts: hosts,
output: output,
receivedAt: Date.now()
}
}

View File

@@ -28,6 +28,7 @@ import {
import ArtifactList from './Artifact'
import BuildOutput from './BuildOutput'
import Manifest from './Manifest'
import Console from './Console'
class Build extends React.Component {
@@ -92,6 +93,10 @@ class Build extends React.Component {
<NavItem eventKey={'logs'} href="#logs">
Logs
</NavItem>}
{build.output &&
<NavItem eventKey={'console'} href="#console">
Console
</NavItem>}
</Nav>
<TabContent>
<TabPane eventKey={'summary'}>
@@ -107,15 +112,20 @@ class Build extends React.Component {
</table>
<h3>Artifacts</h3>
<ArtifactList build={build}/>
<h3>Results</h3>
{build.hosts && <BuildOutput output={build.hosts}/>}
</TabPane>
{build.manifest &&
<TabPane eventKey={'logs'}>
<Manifest tenant={this.props.tenant} build={build}/>
</TabPane>}
{build.output &&
<TabPane eventKey={'console'}>
<Console output={build.output}/>
</TabPane>}
</TabContent>
</div>
</TabContainer>
{build.output && <BuildOutput output={build.output}/>}
</Panel.Body>
</Panel>
)

View File

@@ -0,0 +1,352 @@
// 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 ReactJson from 'react-json-view'
import {
Icon,
ListView,
Row,
Col,
Modal,
} from 'patternfly-react'
const INTERESTING_KEYS = ['msg', 'stdout', 'stderr']
function didTaskFail(task) {
if (task.failed) {
return true
}
if ('failed_when_result' in task && !task.failed_when_result) {
return false
}
if ('rc' in task && task.rc) {
return true
}
return false
}
function hostTaskStats (state, host) {
if (didTaskFail(host)) { state.failed += 1}
else if (host.changed) { state.changed += 1}
else if (host.skip_reason) { state.skipped += 1}
else { state.ok += 1}
}
class TaskOutput extends React.Component {
static propTypes = {
data: PropTypes.object,
include: PropTypes.array,
}
findLoopLabel(item) {
const label = item._ansible_item_label
if (typeof(label) === 'string') {
return label
}
return ''
}
renderResults(value) {
return (
<div key='results'>
<h3 key='results-header'>results</h3>
{value.map((result, idx) => (
<div className='zuul-console-task-result' key={idx}>
<h2 key={idx}>{idx}: {this.findLoopLabel(result)}</h2>
{Object.entries(result).map(([key, value]) => (
this.renderData(key, value, true)
))}
</div>
))}
</div>
)
}
renderData(key, value, ignore_underscore) {
let ret
if (ignore_underscore && key[0] === '_') {
return (<React.Fragment key={key}/>)
}
if (this.props.include) {
if (!this.props.include.includes(key)) {
return (<React.Fragment key={key}/>)
}
if (value === '') {
return (<React.Fragment key={key}/>)
}
}
if (value === null) {
ret = (
<pre>
null
</pre>
)
} else if (typeof(value) === 'string') {
ret = (
<pre>
{value}
</pre>
)
} else if (typeof(value) === 'object') {
ret = (
<pre>
<ReactJson
src={value}
name={null}
sortKeys={true}
enableClipboard={false}
displayDataTypes={false}/>
</pre>
)
} else {
ret = (
<pre>
{value.toString()}
</pre>
)
}
return (
<div key={key}>
{ret && <h3>{key}</h3>}
{ret && ret}
</div>
)
}
render () {
const { data } = this.props
return (
<React.Fragment>
{Object.entries(data).map(([key, value]) => (
key==='results'?this.renderResults(value):this.renderData(key, value)
))}
</React.Fragment>
)
}
}
class HostTask extends React.Component {
static propTypes = {
hostname: PropTypes.string,
task: PropTypes.object,
host: PropTypes.object,
errorIds: PropTypes.object,
}
state = {
showModal: false,
failed: 0,
changed: 0,
skipped: 0,
ok: 0
}
open = () => {
this.setState({ showModal: true})
}
close = () => {
this.setState({ showModal: false})
}
constructor (props) {
super(props)
const { host } = this.props
hostTaskStats(this.state, host)
}
render () {
const { hostname, task, host, errorIds } = this.props
const ai = []
if (this.state.skipped) {
ai.push(
<ListView.InfoItem key="skipped" title="Click for details">
<span className="task-skipped" onClick={this.open}>SKIPPED</span>
</ListView.InfoItem>)
}
if (this.state.changed) {
ai.push(
<ListView.InfoItem key="changed" title="Click for details">
<span className="task-changed" onClick={this.open}>CHANGED</span>
</ListView.InfoItem>)
}
if (this.state.failed) {
ai.push(
<ListView.InfoItem key="failed" title="Click for details">
<span className="task-failed" onClick={this.open}>FAILED</span>
</ListView.InfoItem>)
}
if (this.state.ok) {
ai.push(
<ListView.InfoItem key="ok" title="Click for details">
<span className="task-ok" onClick={this.open}>OK</span>
</ListView.InfoItem>)
}
ai.push(
<ListView.InfoItem key="hostname">
{hostname}
</ListView.InfoItem>
)
const expand = errorIds.has(task.task.id)
return (
<React.Fragment>
<ListView.Item
key='header'
heading={task.task.name}
initExpanded={expand}
additionalInfo={ai}
>
<Row>
<Col sm={11}>
<pre>
<TaskOutput data={this.props.host} include={INTERESTING_KEYS}/>
</pre>
</Col>
</Row>
</ListView.Item>
<Modal key='modal' show={this.state.showModal} onHide={this.close}
dialogClassName="zuul-console-task-detail">
<Modal.Header>
<button
className="close"
onClick={this.close}
aria-hidden="true"
aria-label="Close"
>
<Icon type="pf" name="close" />
</button>
<Modal.Title>{hostname}</Modal.Title>
</Modal.Header>
<Modal.Body>
<TaskOutput data={host}/>
</Modal.Body>
</Modal>
</React.Fragment>
)
}
}
class PlayBook extends React.Component {
static propTypes = {
playbook: PropTypes.object,
errorIds: PropTypes.object,
}
render () {
const { playbook, errorIds } = this.props
const expandAll = (playbook.phase === 'run')
const expand = (expandAll || errorIds.has(playbook.phase + playbook.index))
const ai = []
if (playbook.trusted) {
ai.push(
<ListView.InfoItem key="trusted" title="Trusted">
<Icon type='pf' name='info' /> Trusted
</ListView.InfoItem>
)
}
return (
<ListView.Item
stacked={true}
additionalInfo={ai}
initExpanded={expand}
heading={playbook.phase[0].toUpperCase() + playbook.phase.slice(1) + ' playbook'}
description={playbook.playbook}
>
<Row>
<Col sm={12}>
{playbook.plays.map((play, idx) => (
play.tasks.map((task, idx2) => (
Object.entries(task.hosts).map(([hostname, host]) => (
<HostTask key={idx+'-'+idx2+hostname} hostname={hostname}
task={task} host={host} errorIds={errorIds}/>
))))))}
</Col>
</Row>
</ListView.Item>
)
}
}
class Console extends React.Component {
static propTypes = {
output: PropTypes.array,
}
constructor (props) {
super(props)
const { output } = this.props
const errorIds = new Set()
this.errorIds = errorIds
// Identify all of the hosttasks (and therefore tasks, plays, and
// playbooks) which have failed. The errorIds are either task or
// play uuids, or the phase+index for the playbook. Since they are
// different formats, we can store them in the same set without
// collisions.
output.forEach(playbook => {
playbook.plays.forEach(play => {
play.tasks.forEach(task => {
Object.entries(task.hosts).forEach(([, host]) => {
if (host.results) {
host.results.forEach(result => {
if (didTaskFail(result)) {
errorIds.add(task.task.id)
errorIds.add(play.play.id)
errorIds.add(playbook.phase + playbook.index)
}
})
}
if (didTaskFail(host)) {
errorIds.add(task.task.id)
errorIds.add(play.play.id)
errorIds.add(playbook.phase + playbook.index)
}
})
})
})
})
}
render () {
const { output } = this.props
return (
<React.Fragment>
<ListView key="playbooks" className="zuul-console">
{output.map((playbook, idx) => (
<PlayBook key={idx} playbook={playbook} errorIds={this.errorIds}/>))}
</ListView>
</React.Fragment>
)
}
}
export default Console

View File

@@ -165,3 +165,60 @@ pre.version {
.swagger-ui .servers {
padding-top: 5px
}
/* Console */
.zuul-console .list-group-item-header,
.zuul-console .list-view-pf-actions,
.zuul-console .list-view-pf-expand,
.zuul-console .list-view-pf-main-info
{
margin-top: 1px;
margin-bottom: 1px;
}
.zuul-console .list-view-pf-main-info
{
padding-top: 1px;
padding-bottom: 1px;
}
.zuul-console .list-view-pf-expand
{
padding: 0;
}
.zuul-console .list-group-item-heading
{
line-height: 20px;
font-size: 12px;
margin-bottom: 0;
}
.zuul-console .task-skipped
{
color: white;
background-color: #00729b;
width: 6em;
}
.zuul-console .task-changed
{
color: white;
background-color: #a28301;
width: 6em;
}
.zuul-console .task-ok
{
color: white;
background-color: #018200;
width: 6em;
}
.zuul-console .task-failed
{
color: white;
background-color: #9b0000;
width: 6em;
}
.zuul-console-task-detail
{
width: 80%;
}
.zuul-console-task-result
{
padding-left: 4em;
}

View File

@@ -49,7 +49,8 @@ export default (state = {
return update(state, {$merge: {isFetchingOutput: true}})
case BUILD_OUTPUT_SUCCESS:
state.builds = update(
state.builds, {[action.buildId]: {$merge: {output: action.output}}})
state.builds, {[action.buildId]: {$merge: {hosts: action.hosts,
output: action.output}}})
return update(state, {$merge: {isFetchingOutput: false}})
case BUILD_OUTPUT_FAIL:
return update(state, {$merge: {isFetchingOutput: false}})