summaryrefslogtreecommitdiff
path: root/web/src/containers/build/Console.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/containers/build/Console.jsx')
-rw-r--r--web/src/containers/build/Console.jsx418
1 files changed, 269 insertions, 149 deletions
diff --git a/web/src/containers/build/Console.jsx b/web/src/containers/build/Console.jsx
index 0b365df62..f677dfbd2 100644
--- a/web/src/containers/build/Console.jsx
+++ b/web/src/containers/build/Console.jsx
@@ -18,17 +18,30 @@ import * as React from 'react'
import ReAnsi from '@softwarefactory-project/re-ansi'
import PropTypes from 'prop-types'
import ReactJson from 'react-json-view'
+
import {
- Icon,
- ListView,
- Row,
- Col,
+ Button,
+ Chip,
+ DataList,
+ DataListItem,
+ DataListItemRow,
+ DataListCell,
+ DataListItemCells,
+ DataListToggle,
+ DataListContent,
+ Flex,
+ FlexItem,
+ Label,
Modal,
-} from 'patternfly-react'
+ Tooltip
+} from '@patternfly/react-core'
+
import {
+ AngleRightIcon,
ContainerNodeIcon,
InfoCircleIcon,
SearchPlusIcon,
+ LinkIcon,
} from '@patternfly/react-icons'
import {
@@ -50,6 +63,19 @@ class TaskOutput extends React.Component {
renderResults(value) {
const interesting_results = []
+
+ // This was written to assume "value" is an array of
+ // key/value mappings to output. This seems to be a
+ // good assumption for the most part, but "package:" for
+ // whatever reason outputs a result that is just an array of
+ // strings with what packages were installed. So, if we
+ // see an array of strings as the value, we just swizzle
+ // that into a key/value so it displays usefully.
+ const isAllStrings = value.every(i => typeof i === 'string')
+ if (isAllStrings) {
+ value = [ {output: [...value]} ]
+ }
+
value.forEach((result, idx) => {
const keys = Object.entries(result).filter(
([key, value]) => shouldIncludeKey(
@@ -154,11 +180,11 @@ class HostTask extends React.Component {
}
open = () => {
- this.setState({ showModal: true})
+ this.setState({showModal: true})
}
close = () => {
- this.setState({ showModal: false})
+ this.setState({showModal: false})
}
constructor (props) {
@@ -178,52 +204,91 @@ class HostTask extends React.Component {
if (taskPathMatches(taskPath, displayPath))
this.state.showModal = true
+
+ // If it has errors, expand by default
+ this.state.expanded = this.props.errorIds.has(this.props.task.task.id)
}
render () {
- const { hostname, task, host, taskPath, errorIds } = this.props
+ const { hostname, task, host, taskPath } = this.props
+ const dataListCells = []
- const ai = []
+ // "interesting" result tasks are those that have some values in
+ // their results that show command output, etc. These plays get
+ // an expansion that shows these values without having to click
+ // and bring up the full insepction modal.
+ const interestingKeys = hasInterestingKeys(host, INTERESTING_KEYS)
+
+ let name = task.task.name
+ if (!name) {
+ name = host.action
+ }
+ if (task.role) {
+ name = task.role.name + ': ' + name
+ }
+
+ // NOTE(ianw) 2022-08-26 since we have some rows that expand and
+ // others that don't, the expansion button pushes things out of
+ // alignment. This tries to emulate the button and then
+ // hide it. See also:
+ // https://github.com/patternfly/patternfly/issues/5055
+ // We might want to think about other ways to present this?
+ if (!interestingKeys) {
+ dataListCells.push(
+ <DataListCell key='padding-icon' isIcon={true}
+ class='pf-c-data-list__item-control'>
+ <div className='pf-c-data-list__toggle'
+ style={{visibility: 'hidden'}}>
+ <Button disabled>
+ <AngleRightIcon />
+ </Button>
+ </div>
+ </DataListCell>
+ )
+ }
+
+ dataListCells.push(
+ <DataListCell key='name' width={4}>{name}</DataListCell>
+ )
+
+ let label = null
if (this.state.failed) {
- ai.push(
- <ListView.InfoItem key="failed" title="Click for details">
- <span className="task-details-icon" onClick={this.open}>
- <SearchPlusIcon />
- </span>
- <span className="task-failed" onClick={this.open}>FAILED</span>
- </ListView.InfoItem>)
+ label = <Label color='red' onClick={this.open}
+ style={{cursor: 'pointer'}}>FAILED</Label>
} else if (this.state.changed) {
- ai.push(
- <ListView.InfoItem key="changed" title="Click for details">
- <span className="task-details-icon" onClick={this.open}>
- <SearchPlusIcon />
- </span>
- <span className="task-changed" onClick={this.open}>CHANGED</span>
- </ListView.InfoItem>)
+ label = <Label color='orange' onClick={this.open}
+ style={{cursor: 'pointer'}}>CHANGED</Label>
} else if (this.state.skipped) {
- ai.push(
- <ListView.InfoItem key="skipped" title="Click for details">
- <span className="task-details-icon" onClick={this.open}>
- <SearchPlusIcon />
- </span>
- <span className="task-skipped" onClick={this.open}>SKIPPED</span>
- </ListView.InfoItem>)
+ label = <Label color='grey' onClick={this.open}
+ style={{cursor: 'pointer'}}>SKIPPED</Label>
} else if (this.state.ok) {
- ai.push(
- <ListView.InfoItem key="ok" title="Click for details">
- <span className="task-details-icon" onClick={this.open}>
- <SearchPlusIcon />
- </span>
- <span className="task-ok" onClick={this.open}>OK</span>
- </ListView.InfoItem>)
+ label = <Label color='green' onClick={this.open}
+ style={{cursor: 'pointer'}}>OK</Label>
}
- ai.push(
- <ListView.InfoItem key="hostname">
- <span className="additionalinfo-icon">
- <ContainerNodeIcon />
- </span>
- {hostname}
- </ListView.InfoItem>
+
+ dataListCells.push(
+ <DataListCell key='state'>
+ <Flex>
+ <FlexItem>
+ <Tooltip content={<div>Click for details</div>}>
+ <SearchPlusIcon style={{cursor: 'pointer'}} onClick={this.open} />
+ </Tooltip>
+ </FlexItem>
+ <FlexItem>
+ <Tooltip content={<div>Click for details</div>}>
+ {label}
+ </Tooltip>
+ </FlexItem>
+ </Flex>
+ </DataListCell>)
+
+ dataListCells.push(
+ <DataListCell key='node'>
+ <Chip isReadOnly={true} textMaxWidth='50ch'>
+ <span style={{ fontSize: 'var(--pf-global--FontSize--md)' }}>
+ <ContainerNodeIcon />&nbsp;{hostname}</span>
+ </Chip>
+ </DataListCell>
)
let duration = moment.duration(
@@ -234,69 +299,63 @@ class HostTask extends React.Component {
minValue: 1,
})
- ai.push(
- <ListView.InfoItem key="task-duration">
- <span className="task-duration">{duration}</span>
- </ListView.InfoItem>
+ dataListCells.push(
+ <DataListCell key='task-duration'>
+ <span className='task-duration'>{duration}</span>
+ </DataListCell>
)
- const expand = errorIds.has(task.task.id)
-
- let name = task.task.name
- if (!name) {
- name = host.action
- }
- if (task.role) {
- name = task.role.name + ': ' + name
- }
- const has_interesting_keys = hasInterestingKeys(this.props.host, INTERESTING_KEYS)
- let lc = undefined
- if (!has_interesting_keys) {
- lc = []
+ const content = <TaskOutput data={this.props.host} include={INTERESTING_KEYS}/>
+
+ let item = null
+ if (interestingKeys) {
+ item = <DataListItem isExpanded={this.state.expanded}>
+ <DataListItemRow>
+ <DataListToggle
+ onClick={() => {this.setState({expanded: !this.state.expanded})}}
+ isExpanded={this.state.expanded}
+ />
+ <DataListItemCells dataListCells={ dataListCells } />
+ </DataListItemRow>
+ <DataListContent
+ isHidden={!this.state.expanded}>
+ { content }
+ </DataListContent>
+ </DataListItem>
+ } else {
+ item = <DataListItem>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={ dataListCells } />
+ </DataListItemRow>
+ </DataListItem>
}
+
+ const modalDescription = <Flex>
+ <FlexItem>{label}</FlexItem>
+ <FlexItem>
+ <Chip isReadOnly={true} textMaxWidth='50ch'>
+ <span style={{ fontSize: 'var(--pf-global--FontSize--md)' }}>
+ <ContainerNodeIcon />&nbsp;{hostname}</span>
+ </Chip>
+ </FlexItem>
+ <FlexItem>
+ <a href={'#'+makeTaskPath(taskPath)}>
+ <LinkIcon name='link' title='Permalink' />
+ </a>
+ </FlexItem>
+ </Flex>
+
return (
- <React.Fragment>
- <ListView.Item
- key='header'
- heading={name}
- initExpanded={expand}
- additionalInfo={ai}
- leftContent={lc}
- >
- {has_interesting_keys &&
- <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}
- <span className="zuul-console-modal-header-link">
- <a href={'#'+makeTaskPath(taskPath)}>
- <Icon type="fa" name="link" title="Permalink" />
- </a>
- </span>
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <TaskOutput data={host}/>
- </Modal.Body>
+ <>
+ {item}
+ <Modal
+ title={name}
+ isOpen={this.state.showModal}
+ onClose={this.close}
+ description={modalDescription}>
+ <TaskOutput data={host}/>
</Modal>
- </React.Fragment>
+ </>
)
}
}
@@ -309,60 +368,113 @@ class PlayBook extends React.Component {
displayPath: PropTypes.array,
}
+ constructor(props) {
+ super(props)
+ this.state = {
+ // Start the playbook expanded if
+ // * has errror in it
+ // * direct link
+ // * it is a run playbook
+ expanded: (this.props.errorIds.has(this.props.playbook.phase + this.props.playbook.index) ||
+ taskPathMatches(this.props.taskPath, this.props.displayPath) ||
+ this.props.playbook.phase === 'run'),
+ // NOTE(ianw) 2022-08-26 : Plays start expanded because that is
+ // what it has always done; most playbooks probably only have
+ // one play. Maybe if there's multiple plays things could start
+ // rolled up?
+ playsExpanded: this.props.playbook.plays.map((play, idx) => this.makePlayId(play, idx))
+ }
+ }
+
+ makePlayId = (play, idx) => play.play.name + '-' + idx
+
render () {
const { playbook, errorIds, taskPath, displayPath } = this.props
- const expandAll = (playbook.phase === 'run')
- const expand = (expandAll ||
- errorIds.has(playbook.phase + playbook.index) ||
- taskPathMatches(taskPath, displayPath))
+ const togglePlays = id => {
+ const index = this.state.playsExpanded.indexOf(id)
+ const newExpanded =
+ index >= 0 ? [...this.state.playsExpanded.slice(0, index), ...this.state.playsExpanded.slice(index + 1, this.state.playsExpanded.length)] : [...this.state.playsExpanded, id]
+ this.setState({playsExpanded: newExpanded})
+ }
- const ai = []
+ // This is the header for each playbook
+ let dataListCells = []
+ dataListCells.push(
+ <DataListCell key='name' width={1}>
+ <strong>
+ {playbook.phase[0].toUpperCase() + playbook.phase.slice(1)} playbook<
+ /strong>
+ </DataListCell>)
+ dataListCells.push(
+ <DataListCell key='path' width={5}>
+ {playbook.playbook}
+ </DataListCell>)
if (playbook.trusted) {
- ai.push(
- <ListView.InfoItem key="trusted" title="This playbook runs in a trusted execution context, which permits executing code on the Zuul executor and allows access to all Ansible features.">
- <span className="additionalinfo-icon">
- <InfoCircleIcon />
- </span>
- Trusted
- </ListView.InfoItem>
- )
+ dataListCells.push(
+ <DataListCell key='trust'>
+ <Tooltip content={<div>This playbook runs in a trusted execution context, which permits executing code on the Zuul executor and allows access to all Ansible features.</div>}>
+ <Label color='blue' icon={<InfoCircleIcon />} style={{cursor: 'pointer'}}>Trusted</Label></Tooltip></DataListCell>)
+ } else {
+ // NOTE(ianw) : This empty cell keeps things lined up
+ // correctly. We tried a "untrusted" label but preferred
+ // without.
+ dataListCells.push(<DataListCell key='trust' width={1}/>)
}
return (
- <ListView.Item
- stacked={true}
- additionalInfo={ai}
- initExpanded={expand}
- heading={playbook.phase[0].toUpperCase() + playbook.phase.slice(1) + ' playbook'}
- description={playbook.playbook}
- >
- {playbook.plays.map((play, idx) => (
- <React.Fragment key={idx}>
- <Row key='play'>
- <Col sm={12}>
- <strong>Play: {play.play.name}</strong>
- </Col>
- </Row>
- {play.tasks.map((task, idx2) => (
- Object.entries(task.hosts).map(([hostname, host]) => (
- <Row key={idx2+hostname}>
- <Col sm={12}>
- <HostTask hostname={hostname}
- taskPath={taskPath.concat([
- idx.toString(), idx2.toString(), hostname])}
- displayPath={displayPath} task={task} host={host}
- errorIds={errorIds}/>
- </Col>
- </Row>
- ))))}
- </React.Fragment>
- ))}
- </ListView.Item>
+ <DataListItem isExpanded={this.state.expanded}>
+
+ <DataListItemRow>
+ <DataListToggle
+ onClick={() => this.setState({expanded: !this.state.expanded})}
+ isExpanded={this.state.expanded}/>
+ <DataListItemCells
+ dataListCells={dataListCells} />
+ </DataListItemRow>
+
+ <DataListContent isHidden={!this.state.expanded}>
+
+ {playbook.plays.map((play, idx) => (
+ <DataList isCompact={true} key={this.makePlayId(play, idx)} style={{ fontSize: 'var(--pf-global--FontSize--md)' }}>
+ <DataListItem isExpanded={this.state.playsExpanded.includes(this.makePlayId(play, idx))}>
+ <DataListItemRow>
+ <DataListToggle
+ onClick={() => togglePlays(this.makePlayId(play, idx))}
+ isExpanded={this.state.playsExpanded.includes(this.makePlayId(play, idx))}
+ id={this.makePlayId(play, idx)}/>
+ <DataListItemCells dataListCells={[
+ <DataListCell key='play'>Play: {play.play.name}</DataListCell>
+ ]}
+ />
+ </DataListItemRow>
+ <DataListContent
+ isHidden={!this.state.playsExpanded.includes(this.makePlayId(play, idx))}>
+
+ <DataList isCompact={true} style={{ fontSize: 'var(--pf-global--FontSize--md)' }} >
+ {play.tasks.map((task, idx2) => (
+ Object.entries(task.hosts).map(([hostname, host]) => (
+ <HostTask key={idx+idx2+hostname}
+ hostname={hostname}
+ taskPath={taskPath.concat([
+ idx.toString(), idx2.toString(), hostname])}
+ displayPath={displayPath} task={task} host={host}
+ errorIds={errorIds}/>
+ ))))}
+ </DataList>
+
+ </DataListContent>
+ </DataListItem>
+ </DataList>
+ ))}
+
+ </DataListContent>
+ </DataListItem>
)
}
}
+
class Console extends React.Component {
static propTypes = {
errorIds: PropTypes.object,
@@ -375,11 +487,19 @@ class Console extends React.Component {
return (
<React.Fragment>
- <ListView key="playbooks" className="zuul-console">
- {output.map((playbook, idx) => (
- <PlayBook key={idx} playbook={playbook} taskPath={[idx.toString()]}
- displayPath={displayPath} errorIds={errorIds}/>))}
- </ListView>
+ <br />
+ <span className="zuul-console">
+ <DataList isCompact={true}
+ style={{ fontSize: 'var(--pf-global--FontSize--md)' }}>
+ {
+ output.map((playbook, idx) => (
+ <PlayBook
+ key={idx} playbook={playbook} taskPath={[idx.toString()]}
+ displayPath={displayPath} errorIds={errorIds}
+ />))
+ }
+ </DataList>
+ </span>
</React.Fragment>
)
}