diff options
author | Zuul <zuul@review.opendev.org> | 2019-08-08 16:28:46 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2019-08-08 16:28:46 +0000 |
commit | da334c3bc8a7281be302c5b32fa38af719600c7b (patch) | |
tree | fdc002e8569104b8fe88e847c1071103c163c455 /web | |
parent | fb595693152a2704352b084ce43fef8985e3d2a3 (diff) | |
parent | c035bc69babb8150d50fe6f6f9ac0fff350715f2 (diff) | |
download | zuul-da334c3bc8a7281be302c5b32fa38af719600c7b.tar.gz |
Merge "Render console in js"
Diffstat (limited to 'web')
-rw-r--r-- | web/src/actions/build.js | 3 | ||||
-rw-r--r-- | web/src/containers/build/Build.jsx | 12 | ||||
-rw-r--r-- | web/src/containers/build/Console.jsx | 352 | ||||
-rw-r--r-- | web/src/index.css | 57 | ||||
-rw-r--r-- | web/src/reducers/build.js | 3 |
5 files changed, 424 insertions, 3 deletions
diff --git a/web/src/actions/build.js b/web/src/actions/build.js index 198e259ae..e29ecd624 100644 --- a/web/src/actions/build.js +++ b/web/src/actions/build.js @@ -91,7 +91,8 @@ const receiveBuildOutput = (buildId, output) => { return { type: BUILD_OUTPUT_SUCCESS, buildId: buildId, - output: hosts, + hosts: hosts, + output: output, receivedAt: Date.now() } } diff --git a/web/src/containers/build/Build.jsx b/web/src/containers/build/Build.jsx index a0cdece4c..46fdaaa47 100644 --- a/web/src/containers/build/Build.jsx +++ b/web/src/containers/build/Build.jsx @@ -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> ) diff --git a/web/src/containers/build/Console.jsx b/web/src/containers/build/Console.jsx new file mode 100644 index 000000000..efd0d0a7e --- /dev/null +++ b/web/src/containers/build/Console.jsx @@ -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 diff --git a/web/src/index.css b/web/src/index.css index cfdb46880..98d4c72af 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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; +} diff --git a/web/src/reducers/build.js b/web/src/reducers/build.js index 3ef10e67a..084e9a36f 100644 --- a/web/src/reducers/build.js +++ b/web/src/reducers/build.js @@ -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}}) |