diff options
Diffstat (limited to 'web/src/containers/build/Console.jsx')
-rw-r--r-- | web/src/containers/build/Console.jsx | 418 |
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 /> {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 /> {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> ) } |