web UI: add "show retries" toggles on buildset page

On a buildset page, add a switch to toggle the display of non-final
jobs. By default, show all jobs.
Add a button in front of each final job to show/hide retries for
that specific job.

Hide the toggle switch if there are no retried jobs in the buildset.

Change-Id: I22057dbd404e80f03a466610bc13968f9c8cbf10
This commit is contained in:
Matthieu Huin 2021-08-26 16:45:08 +02:00
parent d7e3878934
commit e455bc3a16
2 changed files with 174 additions and 62 deletions

View File

@ -22,8 +22,15 @@ import {
DataListItem,
DataListItemRow,
DataListItemCells,
Flex,
FlexItem,
Switch,
} from '@patternfly/react-core'
import { OutlinedClockIcon } from '@patternfly/react-icons'
import {
AngleDownIcon,
AngleRightIcon,
OutlinedClockIcon
} from '@patternfly/react-icons'
import 'moment-duration-format'
import * as moment from 'moment'
@ -39,75 +46,175 @@ class BuildList extends React.Component {
// page. Without this flag we might then even use this (with more
// information) on the /builds page.
constructor() {
super()
constructor(props) {
super(props)
const { builds } = this.props
let retriedJobs = builds.filter((build) => {
return !build.final
}).map((build) => (build.job_name)
).filter((build, idx, self) => {
return self.indexOf(build) === idx
})
this.state = {
selectedBuildId: null,
visibleNonFinalBuilds: retriedJobs,
retriedJobs: retriedJobs,
}
}
handleSelectDataListItem = (buildId) => {
this.setState({
selectedBuildId: buildId,
sortedBuilds = () => {
const { builds } = this.props
const { visibleNonFinalBuilds } = this.state
return builds.sort((a, b) => {
// Group builds by job name, then order by decreasing start time; this will ensure retries are together
if (a.job_name === b.job_name) {
if (a.start_time < b.start_time) {
return 1
}
if (a.start_time > b.start_time) {
return -1
}
return 0
}
if (a.job_name > b.job_name) {
return 1
} else {
return -1
}
}).filter((build) => {
if (build.final || visibleNonFinalBuilds.indexOf(build.job_name) >= 0) {
return true
}
else {
return false
}
})
}
render() {
const { builds, tenant } = this.props
const { selectedBuildId } = this.state
handleFinalSwitch = isChecked => {
const { retriedJobs } = this.state
this.setState({ visibleNonFinalBuilds: (isChecked ? retriedJobs : []) })
}
handleToggleVisibleNonFinalBuilds = (jobName) => {
const { visibleNonFinalBuilds } = this.state
const index = visibleNonFinalBuilds.indexOf(jobName)
const newVisible =
index >= 0 ? [...visibleNonFinalBuilds.slice(0, index), ...visibleNonFinalBuilds.slice(index + 1, visibleNonFinalBuilds.length)] : [...visibleNonFinalBuilds, jobName]
this.setState({
visibleNonFinalBuilds: newVisible,
})
}
renderRetriesButton = (build, hasRetries) => {
const { visibleNonFinalBuilds } = this.state
if (!build.final || !hasRetries) {
return <DataListCell key={`${build.uuid}-final`} width={1} isIcon={true}>
{/* Hide the icon to maintain alignment between final and non-final elements */}
<AngleRightIcon visibility="hidden" />
</DataListCell >
}
const isExpanded = (visibleNonFinalBuilds.indexOf(build.job_name) >= 0)
const RetryIcon =
isExpanded
? AngleDownIcon
: AngleRightIcon
const retryAltText =
isExpanded
? 'Hide retries for this job'
: 'Show retries for this job'
// TODO either replace this with an ExpandableSection (but this breaks the layout) or figure out CSS animations for the icon.
return (
<DataList
className="zuul-build-list"
isCompact
selectedDataListItemId={selectedBuildId}
onSelectDataListItem={this.handleSelectDataListItem}
style={{ fontSize: 'var(--pf-global--FontSize--md)' }}
>
{builds.map((build) => (
<DataListItem key={build.uuid || build.job_name} id={build.uuid}>
<Link
to={`${tenant.linkPrefix}/build/${build.uuid}`}
style={{
textDecoration: 'none',
color: build.voting
? 'inherit'
: 'var(--pf-global--disabled-color--100)',
}}
>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key={build.uuid} width={3}>
<BuildResultWithIcon
result={build.result}
colored={build.voting}
size="sm"
>
{build.job_name}
{!build.voting && ' (non-voting)'}
</BuildResultWithIcon>
</DataListCell>,
<DataListCell key={`${build.uuid}-time`}>
<IconProperty
icon={<OutlinedClockIcon />}
value={moment
.duration(build.duration, 'seconds')
.format('h [hr] m [min] s [sec]')}
/>
</DataListCell>,
<DataListCell key={`${build.uuid}-result`}>
<BuildResult
result={build.result}
colored={build.voting}
/>
</DataListCell>,
]}
/>
</DataListItemRow>
</Link>
</DataListItem>
))}
</DataList>
<DataListCell key={`${build.uuid}-final`} width={1} isIcon={true}>
<RetryIcon
onClick={() => { this.handleToggleVisibleNonFinalBuilds(build.job_name) }}
title={retryAltText}
style={{ cursor: 'pointer' }} />
</DataListCell >
)
}
render() {
const { tenant } = this.props
const { visibleNonFinalBuilds, retriedJobs } = this.state
let retrySwitch = retriedJobs.length > 0 ?
<FlexItem align={{ default: 'alignRight' }}>
<span>Show retries &nbsp;</span>
<Switch
isChecked={visibleNonFinalBuilds === retriedJobs}
onChange={this.handleFinalSwitch}
isReversed
/>
</FlexItem> :
<></>
const sortedBuilds = this.sortedBuilds()
return (
<Flex direction={{ default: 'column' }}>
{retrySwitch}
<FlexItem>
<DataList
className="zuul-build-list"
isCompact
style={{ fontSize: 'var(--pf-global--FontSize--md)' }}
>
{sortedBuilds.map((build) => {
function linkWrap(cell) {
return (<Link
to={`${tenant.linkPrefix}/build/${build.uuid}`}
style={{
textDecoration: 'none',
color: build.voting
? 'inherit'
: 'var(--pf-global--disabled-color--100)',
}}
>
{cell}
</Link>)
}
return (
<DataListItem key={build.uuid || build.job_name} id={build.uuid}>
<DataListItemRow
style={!build.final ? { backgroundColor: 'var(--pf-global--BackgroundColor--light-200)' } : {}}>
<DataListItemCells
dataListCells={[
this.renderRetriesButton(build, retriedJobs.indexOf(build.job_name) >= 0),
<DataListCell key={build.uuid} width={3}>
{linkWrap(<BuildResultWithIcon
result={build.result}
colored={build.voting}
size="sm"
>
{build.job_name}
{!build.voting && ' (non-voting)'}
</BuildResultWithIcon>)}
</DataListCell>,
<DataListCell key={`${build.uuid}-time`}>
{linkWrap(<IconProperty
icon={<OutlinedClockIcon />}
value={moment
.duration(build.duration, 'seconds')
.format('h [hr] m [min] s [sec]')}
/>)}
</DataListCell>,
<DataListCell key={`${build.uuid}-result`}>
{linkWrap(<BuildResult
result={build.result}
colored={build.voting}
/>)}
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
)
})}
</DataList>
</FlexItem>
</Flex>
)
}
}

View File

@ -51,6 +51,11 @@ a.refresh {
font-weight: bold;
}
/* Remove ugly outline when a Switch is selected */
.pf-c-switch {
--pf-c-switch__input--focus__toggle--OutlineWidth: 0;
}
/* Keep the normal font-size for compact tables */
.zuul-build-table td {
font-size: var(--pf-global--FontSize--md);