summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Wienand <iwienand@redhat.com>2022-08-25 18:01:59 +1000
committerIan Wienand <iwienand@redhat.com>2022-09-13 11:24:58 +1000
commit54e79eafa022a840e600dd74e0f4b0662a22eb43 (patch)
tree0047196d1b6a17f5ad9432428647a2e270d399af
parent85786777db07b925727d37220029006c7f703a22 (diff)
downloadzuul-54e79eafa022a840e600dd74e0f4b0662a22eb43.tar.gz
web: console: convert to PF4 DataList
The console page is currently based on the ListView from PF3. The most direct conversion to PF4 is a DataList, which is similar but different. This is intended to be a like-for-like replacement of the console page using the DataList model. The things I have explicitly thought about: * The trusted flag on the playbook remains with tooltip * The task line output is the same order, with task <+>- (STATUS) [hosts] <time> The magnifying glass and status share a cell to keep them close * You can click on either the magnifying glass or the status to bring up the modal with the full task status. Both have a tooltip. * The status and hosts are modified to use PF4 label and chip models for a standard look. * Plays start rolled up, but tasks start in the expanded state as before. * If there is a failed task, the playbook and play are unrolled to show it automatically as before. * "run" playbooks are expanded by default * Tasks highlight on hover, but now using a light boxshadow rather than background color which is more consistent with other parts of the ui (like the build table, for example). * No colours in the background of the playbook rows; the DataList now has a blue line that runs down the side showing you the groupings when a playbook row is opened. I think this is more consistent with "less is more" type approach. * deep-link permalinks on the modal display of the task results open up the same task display when pasted into a new window. While I think there is plenty of room for improvement in the way this information is displayed, I've deliberately tried to keep everything the same in this changeset to a) ease review and b) so we have a PF4-based grounding to work from. There wasn't really a way to do this more incrementally; although almost everything moves around there is no tricky code to call out -- some fiddling of things that needed to state properties and some toggle javascript-code bits are the main additions. Change-Id: Ie480deb046502879542e41844e919a362203e25d
-rw-r--r--web/src/containers/build/Console.jsx376
-rw-r--r--web/src/index.css81
2 files changed, 233 insertions, 224 deletions
diff --git a/web/src/containers/build/Console.jsx b/web/src/containers/build/Console.jsx
index 194e314ee..68faea1cf 100644
--- a/web/src/containers/build/Console.jsx
+++ b/web/src/containers/build/Console.jsx
@@ -18,17 +18,28 @@ 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,
+ Chip,
+ DataList,
+ DataListItem,
+ DataListItemRow,
+ DataListCell,
+ DataListItemCells,
+ DataListToggle,
+ DataListContent,
+ Flex,
+ FlexItem,
+ Label,
Modal,
-} from 'patternfly-react'
+ Tooltip
+} from '@patternfly/react-core'
+
import {
ContainerNodeIcon,
InfoCircleIcon,
SearchPlusIcon,
+ LinkIcon,
} from '@patternfly/react-icons'
import {
@@ -167,11 +178,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) {
@@ -191,52 +202,88 @@ 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(this.props.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 is based on me finding the > icon has a
+ // min-width of 40px, and then trying to find the right variable
+ // to extend the cell padding/margins the same as the toggle
+ // control.
+ // 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}
+ style={{marginRight: 'var(--pf-c-data-list__item-control--MarginRight)'}}>
+ <span style={{display: 'inline-block', minWidth: '40px'}}></span>
+ </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'}} />
+ </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(
@@ -247,69 +294,53 @@ 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)
+ const content = <TaskOutput data={this.props.host} include={INTERESTING_KEYS}/>
+
+ const expandableItem = <DataListItem isExpanded={this.state.expanded}>
+ <DataListItemRow className='zuul-console-datalistrow'>
+ <DataListToggle
+ onClick={() => {this.setState({expanded: !this.state.expanded})}}
+ isExpanded={this.state.expanded}
+ />
+ <DataListItemCells dataListCells={ dataListCells } />
+ </DataListItemRow>
+ <DataListContent
+ isHidden={!this.state.expanded}>
+ { content }
+ </DataListContent>
+ </DataListItem>
+
+ const regularItem = <DataListItem>
+ <DataListItemRow className='zuul-console-datalistrow'>
+ <DataListItemCells dataListCells={ dataListCells } />
+ </DataListItemRow>
+ </DataListItem>
+
+ const item = interestingKeys ? expandableItem : regularItem
+
+ // TODO(ianw) : This goes in the modal; this could be made to look
+ // much better with headings and footers and whatnot.
+ const description = <a href={'#'+makeTaskPath(taskPath)}>
+ <LinkIcon name='link' title='Permalink' />
+ </a>
- 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 = []
- }
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={hostname}
+ isOpen={this.state.showModal}
+ onClose={this.close}
+ description={description}>
+ <TaskOutput data={host}/>
</Modal>
- </React.Fragment>
+ </>
)
}
}
@@ -322,60 +353,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,
@@ -388,11 +472,11 @@ class Console extends React.Component {
return (
<React.Fragment>
- <ListView key="playbooks" 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}/>))}
- </ListView>
+ </DataList>
</React.Fragment>
)
}
diff --git a/web/src/index.css b/web/src/index.css
index da74a4a24..cc533b6f9 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -300,86 +300,11 @@ pre.version {
}
/* 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
+.zuul-console-datalistrow:hover
{
- 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-view-pf-left
-{
- padding-right: 17px;
-}
-.zuul-console .list-group-item-heading
-{
- margin-bottom: 0;
-}
-.zuul-console .list-group-item-text
-{
- margin-bottom: 0;
-}
-.zuul-console .task-skipped
-{
- color: white;
- background-color: #00729b;
- width: 6em;
- cursor: pointer;
-}
-.zuul-console .task-changed
-{
- color: white;
- background-color: #a28301;
- width: 6em;
- cursor: pointer;
-}
-.zuul-console .task-ok
-{
- color: white;
- background-color: #018200;
- width: 6em;
- cursor: pointer;
-}
-.zuul-console .task-failed
-{
- color: white;
- background-color: #9b0000;
- width: 6em;
- cursor: pointer;
-}
-.zuul-console .additionalinfo-icon
-{
- cursor: default;
- margin-right: 8px;
-}
-.zuul-console .task-details-icon
-{
- cursor: pointer;
- margin-right: 8px;
-}
-.zuul-console-modal-header-link
-{
- margin-left: 2em;
- font-size: 18px;
-}
-.zuul-console-task-detail
-{
- width: 80%;
-}
-.zuul-console-task-result
-{
- padding-left: 4em;
+ box-shadow: var(--pf-c-data-list__item--m-selected--BoxShadow)
}
+
pre.zuul-log-output
{
overflow-x: auto;