summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2019-08-08 16:28:46 +0000
committerGerrit Code Review <review@openstack.org>2019-08-08 16:28:46 +0000
commitda334c3bc8a7281be302c5b32fa38af719600c7b (patch)
treefdc002e8569104b8fe88e847c1071103c163c455 /web
parentfb595693152a2704352b084ce43fef8985e3d2a3 (diff)
parentc035bc69babb8150d50fe6f6f9ac0fff350715f2 (diff)
downloadzuul-da334c3bc8a7281be302c5b32fa38af719600c7b.tar.gz
Merge "Render console in js"
Diffstat (limited to 'web')
-rw-r--r--web/src/actions/build.js3
-rw-r--r--web/src/containers/build/Build.jsx12
-rw-r--r--web/src/containers/build/Console.jsx352
-rw-r--r--web/src/index.css57
-rw-r--r--web/src/reducers/build.js3
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}})