Update web ui for dependency refactor

This updates the web ui to handle multiple changes per item.  It
is compatible with the current data output as well as the upcoming
API.

Change-Id: I536967e51b22b60c8ff7baa46b902a36d1ea44dd
This commit is contained in:
James E. Blair 2024-02-01 13:30:49 -08:00
parent c531adacae
commit 4a7e86f7f6
11 changed files with 278 additions and 192 deletions

View File

@ -48,22 +48,22 @@ ExternalLink.propTypes = {
children: PropTypes.node, children: PropTypes.node,
} }
function buildExternalLink(buildish) { function buildExternalLink(ref) {
/* TODO (felix): What should we show for periodic builds /* TODO (felix): What should we show for periodic builds
here? They don't provide a change, but the ref_url is here? They don't provide a change, but the ref_url is
also not usable */ also not usable */
if (buildish.ref_url && buildish.change) { if (ref.ref_url && ref.change) {
return ( return (
<ExternalLink target={buildish.ref_url}> <ExternalLink target={ref.ref_url}>
<strong>Change </strong> <strong>Change </strong>
{buildish.change},{buildish.patchset} {ref.change},{ref.patchset}
</ExternalLink> </ExternalLink>
) )
} else if (buildish.ref_url && buildish.newrev) { } else if (ref.ref_url && ref.newrev) {
return ( return (
<ExternalLink target={buildish.ref_url}> <ExternalLink target={ref.ref_url}>
<strong>Revision </strong> <strong>Revision </strong>
{buildish.newrev.slice(0, 7)} {ref.newrev.slice(0, 7)}
</ExternalLink> </ExternalLink>
) )
} }
@ -71,20 +71,20 @@ function buildExternalLink(buildish) {
return null return null
} }
function buildExternalTableLink(buildish) { function buildExternalTableLink(ref) {
/* TODO (felix): What should we show for periodic builds /* TODO (felix): What should we show for periodic builds
here? They don't provide a change, but the ref_url is here? They don't provide a change, but the ref_url is
also not usable */ also not usable */
if (buildish.ref_url && buildish.change) { if (ref.ref_url && ref.change) {
return ( return (
<ExternalLink target={buildish.ref_url}> <ExternalLink target={ref.ref_url}>
{buildish.change},{buildish.patchset} {ref.change},{ref.patchset}
</ExternalLink> </ExternalLink>
) )
} else if (buildish.ref_url && buildish.newrev) { } else if (ref.ref_url && ref.newrev) {
return ( return (
<ExternalLink target={buildish.ref_url}> <ExternalLink target={ref.ref_url}>
{buildish.newrev.slice(0, 7)} {ref.newrev.slice(0, 7)}
</ExternalLink> </ExternalLink>
) )
} }
@ -92,6 +92,32 @@ function buildExternalTableLink(buildish) {
return null return null
} }
function renderRefInfo(ref) {
const refinfo = ref.branch ? (
<>
<strong>Branch </strong> {ref.branch}
</>
) : (
<>
<strong>Ref </strong> {ref.ref}
</>
)
const oldrev = ref.oldrev ? (
<><br/><strong>Old</strong> {ref.oldrev}</>
) : ( <></> )
const newrev = ref.newrev ? (
<><br/><strong>New</strong> {ref.newrev}</>
) : ( <></> )
return (
<>
{refinfo}
{oldrev}
{newrev}
</>
)
}
function IconProperty(props) { function IconProperty(props) {
const { icon, value, WrapElement = 'span' } = props const { icon, value, WrapElement = 'span' } = props
return ( return (
@ -146,4 +172,4 @@ function setDarkMode(darkMode) {
} }
} }
export { IconProperty, removeHash, ExternalLink, buildExternalLink, buildExternalTableLink, ConditionalWrapper, resolveDarkMode, setDarkMode } export { IconProperty, removeHash, ExternalLink, buildExternalLink, buildExternalTableLink, renderRefInfo, ConditionalWrapper, resolveDarkMode, setDarkMode }

View File

@ -22,8 +22,6 @@ import {
BookIcon, BookIcon,
BuildIcon, BuildIcon,
CodeBranchIcon, CodeBranchIcon,
CodeIcon,
CubeIcon,
FileCodeIcon, FileCodeIcon,
FingerprintIcon, FingerprintIcon,
HistoryIcon, HistoryIcon,
@ -35,20 +33,36 @@ import {
} from '@patternfly/react-icons' } from '@patternfly/react-icons'
import * as moment from 'moment' import * as moment from 'moment'
import * as moment_tz from 'moment-timezone' import * as moment_tz from 'moment-timezone'
import _ from 'lodash'
import 'moment-duration-format' import 'moment-duration-format'
import { BuildResultBadge, BuildResultWithIcon } from './Misc' import { BuildResultBadge, BuildResultWithIcon } from './Misc'
import { buildExternalLink, ExternalLink, IconProperty } from '../../Misc' import { buildExternalLink, renderRefInfo, ExternalLink, IconProperty } from '../../Misc'
import AutoholdModal from '../autohold/autoholdModal' import AutoholdModal from '../autohold/autoholdModal'
function getRefs(build) {
// This method has a purpose beyond backwards compat: return the
// zuul ref for this build first, then the remaining refs.
if (!('refs' in build.buildset)) {
// Backwards compat
return [build]
}
return [build.ref, ...build.buildset.refs.filter((i) => !_.isEqual(i, build.ref))]
}
function getRef(build) {
return 'project' in build ? build : build.ref
}
function Build({ build, tenant, timezone, user }) { function Build({ build, tenant, timezone, user }) {
const [showAutoholdModal, setShowAutoholdModal] = useState(false) const [showAutoholdModal, setShowAutoholdModal] = useState(false)
const change = build.change ? build.change : '' const buildRef = getRef(build)
const ref = build.change ? '' : build.ref // the change or ref to use for api actions like autohold
const project = build.project const actionRef = buildRef.change ? '' : buildRef.ref
const actionChange = buildRef.change ? String(buildRef.change) : ''
//const project = build.project
const job_name = build.job_name const job_name = build.job_name
const build_link = buildExternalLink(build)
const index_links = build.manifest && build.manifest.index_links const index_links = build.manifest && build.manifest.index_links
const build_log_url = build.log_url ? const build_log_url = build.log_url ?
(index_links ? build.log_url + 'index.html' : build.log_url) (index_links ? build.log_url + 'index.html' : build.log_url)
@ -78,7 +92,6 @@ function Build({ build, tenant, timezone, user }) {
) )
} }
return ( return (
<> <>
<Title <Title
@ -112,38 +125,20 @@ function Build({ build, tenant, timezone, user }) {
<Flex flex={{ lg: 'flex_1' }}> <Flex flex={{ lg: 'flex_1' }}>
<FlexItem> <FlexItem>
<List style={{ listStyle: 'none' }}> <List style={{ listStyle: 'none' }}>
{build_link && ( {getRefs(build).map((ref, idx) => (
<IconProperty <IconProperty
key={idx}
WrapElement={ListItem} WrapElement={ListItem}
icon={<CodeIcon />} icon={<CodeBranchIcon />}
value={build_link} value={
<span>
{buildExternalLink(ref)}<br/>
<strong>Project </strong> {ref.project}<br/>
{renderRefInfo(ref)}
</span>
}
/> />
)} ))}
{/* TODO (felix): Link to project page in Zuul */}
<IconProperty
WrapElement={ListItem}
icon={<CubeIcon />}
value={
<>
<strong>Project </strong> {build.project}
</>
}
/>
<IconProperty
WrapElement={ListItem}
icon={<CodeBranchIcon />}
value={
build.branch ? (
<>
<strong>Branch </strong> {build.branch}
</>
) : (
<>
<strong>Ref </strong> {build.ref}
</>
)
}
/>
<IconProperty <IconProperty
WrapElement={ListItem} WrapElement={ListItem}
icon={<StreamIcon />} icon={<StreamIcon />}
@ -225,7 +220,7 @@ function Build({ build, tenant, timezone, user }) {
'/builds?job_name=' + '/builds?job_name=' +
build.job_name + build.job_name +
'&project=' + '&project=' +
build.project buildRef.project
} }
title="See previous runs of this job inside current project." title="See previous runs of this job inside current project."
> >
@ -277,9 +272,9 @@ function Build({ build, tenant, timezone, user }) {
{<AutoholdModal {<AutoholdModal
showAutoholdModal={showAutoholdModal} showAutoholdModal={showAutoholdModal}
setShowAutoholdModal={setShowAutoholdModal} setShowAutoholdModal={setShowAutoholdModal}
change={String(change)} change={actionChange}
changeRef={ref} changeRef={actionRef}
project={project} project={buildRef.project}
jobName={job_name} jobName={job_name}
/>} />}
</> </>

View File

@ -50,6 +50,10 @@ import * as moment_tz from 'moment-timezone'
import { BuildResult, BuildResultWithIcon } from './Misc' import { BuildResult, BuildResultWithIcon } from './Misc'
import { buildExternalTableLink, IconProperty } from '../../Misc' import { buildExternalTableLink, IconProperty } from '../../Misc'
function getRef(build) {
return 'project' in build ? build : build.ref
}
function BuildTable({ function BuildTable({
builds, builds,
fetching, fetching,
@ -101,7 +105,8 @@ function BuildTable({
] ]
function createBuildRow(build) { function createBuildRow(build) {
const changeOrRefLink = buildExternalTableLink(build) const ref = getRef(build)
const changeOrRefLink = buildExternalTableLink(ref)
return { return {
// Pass the build's uuid as row id, so we can use it later on in the // Pass the build's uuid as row id, so we can use it later on in the
@ -125,10 +130,10 @@ function BuildTable({
), ),
}, },
{ {
title: build.project, title: ref.project,
}, },
{ {
title: build.branch ? build.branch : build.ref, title: ref.branch ? ref.branch : ref.ref,
}, },
{ {
title: build.pipeline, title: build.pipeline,

View File

@ -27,10 +27,8 @@ import {
ModalVariant, ModalVariant,
} from '@patternfly/react-core' } from '@patternfly/react-core'
import { import {
CodeIcon,
CodeBranchIcon, CodeBranchIcon,
OutlinedCommentDotsIcon, OutlinedCommentDotsIcon,
CubeIcon,
FingerprintIcon, FingerprintIcon,
StreamIcon, StreamIcon,
OutlinedCalendarAltIcon, OutlinedCalendarAltIcon,
@ -41,15 +39,19 @@ import * as moment from 'moment'
import * as moment_tz from 'moment-timezone' import * as moment_tz from 'moment-timezone'
import 'moment-duration-format' import 'moment-duration-format'
import { buildExternalLink, IconProperty } from '../../Misc' import { buildExternalLink, renderRefInfo, IconProperty } from '../../Misc'
import { BuildResultBadge, BuildResultWithIcon } from './Misc' import { BuildResultBadge, BuildResultWithIcon } from './Misc'
import { enqueue, enqueue_ref } from '../../api' import { enqueue, enqueue_ref } from '../../api'
import { addNotification, addApiError } from '../../actions/notifications' import { addNotification, addApiError } from '../../actions/notifications'
import { ChartModal } from '../charts/ChartModal' import { ChartModal } from '../charts/ChartModal'
import BuildsetGanttChart from '../charts/GanttChart' import BuildsetGanttChart from '../charts/GanttChart'
function getRefs(buildset) {
// For backwards compat: get a list of this items changes.
return 'refs' in buildset ? buildset.refs : [buildset]
}
function Buildset({ buildset, timezone, tenant, user, preferences }) { function Buildset({ buildset, timezone, tenant, user, preferences }) {
const buildset_link = buildExternalLink(buildset)
const [isGanttChartModalOpen, setIsGanttChartModalOpen] = useState(false) const [isGanttChartModalOpen, setIsGanttChartModalOpen] = useState(false)
function renderBuildTimes() { function renderBuildTimes() {
@ -234,32 +236,6 @@ function Buildset({ buildset, timezone, tenant, user, preferences }) {
) )
} }
function renderRefInfo(buildset) {
const refinfo = buildset.branch ? (
<>
<strong>Branch </strong> {buildset.branch}
</>
) : (
<>
<strong>Ref </strong> {buildset.ref}
</>
)
const oldrev = buildset.oldrev ? (
<><br/><strong>Old</strong> {buildset.oldrev}</>
) : ( <></> )
const newrev = buildset.newrev ? (
<><br/><strong>New</strong> {buildset.newrev}</>
) : ( <></> )
return (
<>
{refinfo}
{oldrev}
{newrev}
</>
)
}
return ( return (
<> <>
<Title headingLevel="h2"> <Title headingLevel="h2">
@ -276,32 +252,20 @@ function Buildset({ buildset, timezone, tenant, user, preferences }) {
<Flex flex={{ default: 'flex_1' }}> <Flex flex={{ default: 'flex_1' }}>
<FlexItem> <FlexItem>
<List style={{ listStyle: 'none' }}> <List style={{ listStyle: 'none' }}>
{/* TODO (felix): It would be cool if we could differentiate {getRefs(buildset).map((ref, idx) => (
between the SVC system (Github, Gitlab, Gerrit), so we could
show the respective icon here (GithubIcon, GitlabIcon,
GitIcon - AFAIK the Gerrit icon is not very popular among
icon frameworks like fontawesome */}
{buildset_link && (
<IconProperty <IconProperty
WrapElement={ListItem} WrapElement={ListItem}
icon={<CodeIcon />} icon={<CodeBranchIcon />}
value={buildset_link} key={idx}
value={
<span>
{buildExternalLink(ref)}<br/>
<strong>Project </strong> {ref.project}<br/>
{renderRefInfo(ref)}
</span>
}
/> />
)} ))}
{/* TODO (felix): Link to project page in Zuul */}
<IconProperty
WrapElement={ListItem}
icon={<CubeIcon />}
value={
<>
<strong>Project </strong> {buildset.project}
</>
}
/>
<IconProperty
WrapElement={ListItem}
icon={<CodeBranchIcon />}
value={renderRefInfo(buildset)}/>
<IconProperty <IconProperty
WrapElement={ListItem} WrapElement={ListItem}
icon={<StreamIcon />} icon={<StreamIcon />}

View File

@ -55,6 +55,10 @@ import * as moment_tz from 'moment-timezone'
import { BuildResult, BuildResultWithIcon } from './Misc' import { BuildResult, BuildResultWithIcon } from './Misc'
import { buildExternalTableLink, IconProperty } from '../../Misc' import { buildExternalTableLink, IconProperty } from '../../Misc'
function getRef(buildset) {
return 'refs' in buildset ? buildset.refs[0] : buildset
}
function BuildsetTable({ function BuildsetTable({
buildsets, buildsets,
fetching, fetching,
@ -140,7 +144,8 @@ function BuildsetTable({
] ]
function createBuildsetRow(buildset) { function createBuildsetRow(buildset) {
const changeOrRefLink = buildExternalTableLink(buildset) const ref = getRef(buildset)
const changeOrRefLink = buildExternalTableLink(ref)
let duration let duration
if (currentDuration === 'Buildset Duration') { if (currentDuration === 'Buildset Duration') {
@ -163,12 +168,12 @@ function BuildsetTable({
<BuildResultWithIcon <BuildResultWithIcon
result={buildset.result} result={buildset.result}
link={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}> link={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}>
{buildset.project} {ref.project}
</BuildResultWithIcon> </BuildResultWithIcon>
), ),
}, },
{ {
title: buildset.branch ? buildset.branch : buildset.ref, title: ref.branch ? ref.branch : ref.ref,
}, },
{ {
title: buildset.pipeline, title: buildset.pipeline,

View File

@ -17,7 +17,7 @@ import PropTypes from 'prop-types'
import { Badge } from 'patternfly-react' import { Badge } from 'patternfly-react'
import { Tooltip } from '@patternfly/react-core' import { Tooltip } from '@patternfly/react-core'
import Change from './Change' import Item from './Item'
class ChangeQueue extends React.Component { class ChangeQueue extends React.Component {
@ -38,15 +38,15 @@ class ChangeQueue extends React.Component {
shortName = shortName.substr(0, 32) + '...' shortName = shortName.substr(0, 32) + '...'
} }
let changesList = [] let changesList = []
queue.heads.forEach((changes, changeIdx) => { queue.heads.forEach((items, itemIdx) => {
changes.forEach((change, idx) => { items.forEach((item, idx) => {
changesList.push( changesList.push(
<Change <Item
change={change} item={item}
queue={queue} queue={queue}
expanded={expanded} expanded={expanded}
pipeline={pipeline} pipeline={pipeline}
key={changeIdx.toString() + idx} key={itemIdx.toString() + idx}
/>) />)
}) })
}) })

View File

@ -37,12 +37,17 @@ import { fetchStatusIfNeeded } from '../../actions/status'
import LineAngleImage from '../../images/line-angle.png' import LineAngleImage from '../../images/line-angle.png'
import LineTImage from '../../images/line-t.png' import LineTImage from '../../images/line-t.png'
import ChangePanel from './ChangePanel' import ItemPanel from './ItemPanel'
function getChange(item) {
// For backwards compat: get a representative change for this item
// if there is more than one.
return 'changes' in item ? item.changes[0] : item
}
class Change extends React.Component { class Item extends React.Component {
static propTypes = { static propTypes = {
change: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
queue: PropTypes.object.isRequired, queue: PropTypes.object.isRequired,
expanded: PropTypes.bool.isRequired, expanded: PropTypes.bool.isRequired,
pipeline: PropTypes.object, pipeline: PropTypes.object,
@ -59,7 +64,10 @@ class Change extends React.Component {
} }
dequeueConfirm = () => { dequeueConfirm = () => {
const { tenant, change, pipeline } = this.props const { tenant, item, pipeline } = this.props
const change = getChange(item)
// Use the first change as a proxy for the item since queue
// commands operate on changes
let projectName = change.project let projectName = change.project
let changeId = change.id || 'N/A' let changeId = change.id || 'N/A'
let changeRef = change.ref let changeRef = change.ref
@ -90,7 +98,8 @@ class Change extends React.Component {
renderDequeueModal() { renderDequeueModal() {
const { showDequeueModal } = this.state const { showDequeueModal } = this.state
const { change } = this.props const { item } = this.props
const change = getChange(item)
let projectName = change.project let projectName = change.project
let changeId = change.id || change.ref let changeId = change.id || change.ref
const title = 'You are about to dequeue a change' const title = 'You are about to dequeue a change'
@ -111,7 +120,8 @@ class Change extends React.Component {
} }
promoteConfirm = () => { promoteConfirm = () => {
const { tenant, change, pipeline } = this.props const { tenant, item, pipeline } = this.props
const change = getChange(item)
let changeId = change.id || 'NA' let changeId = change.id || 'NA'
this.setState(() => ({ showPromoteModal: false })) this.setState(() => ({ showPromoteModal: false }))
if (changeId !== 'N/A') { if (changeId !== 'N/A') {
@ -138,7 +148,8 @@ class Change extends React.Component {
renderPromoteModal() { renderPromoteModal() {
const { showPromoteModal } = this.state const { showPromoteModal } = this.state
const { change } = this.props const { item } = this.props
const change = getChange(item)
let changeId = change.id || 'N/A' let changeId = change.id || 'N/A'
const title = 'You are about to promote a change' const title = 'You are about to promote a change'
return ( return (
@ -166,7 +177,7 @@ class Change extends React.Component {
icon={<BanIcon style={{ icon={<BanIcon style={{
color: 'var(--pf-global--danger-color--100)', color: 'var(--pf-global--danger-color--100)',
}} />} }} />}
description="Stop all jobs for this change" description="Stop all jobs for this item"
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
this.setState(() => ({ showDequeueModal: true })) this.setState(() => ({ showDequeueModal: true }))
@ -177,7 +188,7 @@ class Change extends React.Component {
icon={<AngleDoubleUpIcon style={{ icon={<AngleDoubleUpIcon style={{
color: 'var(--pf-global--default-color--200)', color: 'var(--pf-global--default-color--200)',
}} />} }} />}
description="Promote this change to the top of the queue" description="Promote this item to the top of the queue"
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
this.setState(() => ({ showPromoteModal: true })) this.setState(() => ({ showPromoteModal: true }))
@ -207,20 +218,19 @@ class Change extends React.Component {
} }
renderStatusIcon(item) {
renderStatusIcon(change) {
let iconGlyph = 'pficon pficon-ok' let iconGlyph = 'pficon pficon-ok'
let iconTitle = 'Succeeding' let iconTitle = 'Succeeding'
if (change.active !== true) { if (item.active !== true) {
iconGlyph = 'pficon pficon-pending' iconGlyph = 'pficon pficon-pending'
iconTitle = 'Waiting until closer to head of queue to' + iconTitle = 'Waiting until closer to head of queue to' +
' start jobs' ' start jobs'
} else if (change.live !== true) { } else if (item.live !== true) {
iconGlyph = 'pficon pficon-info' iconGlyph = 'pficon pficon-info'
iconTitle = 'Dependent change required for testing' iconTitle = 'Dependent item required for testing'
} else if (change.failing_reasons && } else if (item.failing_reasons &&
change.failing_reasons.length > 0) { item.failing_reasons.length > 0) {
let reason = change.failing_reasons.join(', ') let reason = item.failing_reasons.join(', ')
iconTitle = 'Failing because ' + reason iconTitle = 'Failing because ' + reason
if (reason.match(/merge conflict/)) { if (reason.match(/merge conflict/)) {
iconGlyph = 'pficon pficon-error-circle-o zuul-build-merge-conflict' iconGlyph = 'pficon pficon-error-circle-o zuul-build-merge-conflict'
@ -233,9 +243,9 @@ class Change extends React.Component {
className={'zuul-build-status ' + iconGlyph} className={'zuul-build-status ' + iconGlyph}
title={iconTitle} /> title={iconTitle} />
) )
if (change.live) { if (item.live) {
return ( return (
<Link to={this.props.tenant.linkPrefix + '/status/change/' + change.id}> <Link to={this.props.tenant.linkPrefix + '/status/change/' + getChange(item).id}>
{icon} {icon}
</Link> </Link>
) )
@ -244,9 +254,9 @@ class Change extends React.Component {
} }
} }
renderLineImg(change, i) { renderLineImg(item, i) {
let image = LineTImage let image = LineTImage
if (change._tree_branches.indexOf(i) === change._tree_branches.length - 1) { if (item._tree_branches.indexOf(i) === item._tree_branches.length - 1) {
// Angle line // Angle line
image = LineAngleImage image = LineAngleImage
} }
@ -254,13 +264,13 @@ class Change extends React.Component {
} }
render() { render() {
const { change, queue, expanded, pipeline, user, tenant } = this.props const { item, queue, expanded, pipeline, user, tenant } = this.props
let row = [] let row = []
let adminMenuWidth = 15 let adminMenuWidth = 15
let i let i
for (i = 0; i < queue._tree_columns; i++) { for (i = 0; i < queue._tree_columns; i++) {
let className = '' let className = ''
if (i < change._tree.length && change._tree[i] !== null) { if (i < item._tree.length && item._tree[i] !== null) {
if (this.props.preferences.darkMode) { if (this.props.preferences.darkMode) {
className = ' zuul-change-row-line-dark' className = ' zuul-change-row-line-dark'
} else { } else {
@ -269,19 +279,19 @@ class Change extends React.Component {
} }
row.push( row.push(
<td key={i} className={'zuul-change-row' + className}> <td key={i} className={'zuul-change-row' + className}>
{i === change._tree_index ? this.renderStatusIcon(change) : ''} {i === item._tree_index ? this.renderStatusIcon(item) : ''}
{change._tree_branches.indexOf(i) !== -1 ? ( {item._tree_branches.indexOf(i) !== -1 ? (
this.renderLineImg(change, i)) : ''} this.renderLineImg(item, i)) : ''}
</td>) </td>)
} }
let changeWidth = (user.isAdmin && user.scope.indexOf(tenant.name) !== -1) let itemWidth = (user.isAdmin && user.scope.indexOf(tenant.name) !== -1)
? 360 - adminMenuWidth - 16 * queue._tree_columns ? 360 - adminMenuWidth - 16 * queue._tree_columns
: 360 - 16 * queue._tree_columns : 360 - 16 * queue._tree_columns
row.push( row.push(
<td key={i + 1} <td key={i + 1}
className="zuul-change-cell" className="zuul-change-cell"
style={{ width: changeWidth + 'px' }}> style={{ width: itemWidth + 'px' }}>
<ChangePanel change={change} globalExpanded={expanded} pipeline={pipeline} /> <ItemPanel item={item} globalExpanded={expanded} pipeline={pipeline} />
</td> </td>
) )
if (user.isAdmin && user.scope.indexOf(tenant.name) !== -1) { if (user.isAdmin && user.scope.indexOf(tenant.name) !== -1) {
@ -311,4 +321,4 @@ export default connect(state => ({
tenant: state.tenant, tenant: state.tenant,
user: state.user, user: state.user,
preferences: state.preferences, preferences: state.preferences,
}))(Change) }))(Item)

View File

@ -1,4 +1,5 @@
// Copyright 2018 Red Hat, Inc // Copyright 2018 Red Hat, Inc
// Copyright 2024 Acme Gating, LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); you may // 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 // not use this file except in compliance with the License. You may obtain
@ -20,11 +21,15 @@ import * as moment from 'moment'
import 'moment-duration-format' import 'moment-duration-format'
import { Button } from '@patternfly/react-core' import { Button } from '@patternfly/react-core'
function getChanges(item) {
// For backwards compat: get a list of this items changes.
return 'changes' in item ? item.changes : [item]
}
class ChangePanel extends React.Component { class ItemPanel extends React.Component {
static propTypes = { static propTypes = {
globalExpanded: PropTypes.bool.isRequired, globalExpanded: PropTypes.bool.isRequired,
change: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
tenant: PropTypes.object, tenant: PropTypes.object,
preferences: PropTypes.object preferences: PropTypes.object
} }
@ -175,13 +180,13 @@ class ChangePanel extends React.Component {
} }
return ( return (
<React.Fragment> <React.Fragment>
<small title='Remaining Time' className='time' style={{display: 'inline'}}>
{remainingTime}
</small>
<br />
<small title='Elapsed Time' className='time' style={{display: 'inline'}}> <small title='Elapsed Time' className='time' style={{display: 'inline'}}>
{this.enqueueTime(change.enqueue_time)} {this.enqueueTime(change.enqueue_time)}
</small> </small>
<small> | </small>
<small title='Remaining Time' className='time' style={{display: 'inline'}}>
{remainingTime}
</small>
</React.Fragment> </React.Fragment>
) )
} }
@ -347,12 +352,12 @@ class ChangePanel extends React.Component {
) )
} }
calculateTimes (change) { calculateTimes (item) {
let maxRemaining = 0 let maxRemaining = 0
let jobs = {} let jobs = {}
const now = Date.now() const now = Date.now()
for (const job of change.jobs) { for (const job of item.jobs) {
let jobElapsed = null let jobElapsed = null
let jobRemaining = null let jobRemaining = null
if (job.start_time) { if (job.start_time) {
@ -378,7 +383,7 @@ class ChangePanel extends React.Component {
} }
// If not all the jobs have started, this will be null, so only // If not all the jobs have started, this will be null, so only
// use our value if it's oky to calculate it. // use our value if it's oky to calculate it.
if (change.remaininging_time === null) { if (item.remaininging_time === null) {
maxRemaining = null maxRemaining = null
} }
return { return {
@ -389,36 +394,40 @@ class ChangePanel extends React.Component {
render () { render () {
const { expanded } = this.state const { expanded } = this.state
const { change, globalExpanded } = this.props const { item, globalExpanded } = this.props
let expand = globalExpanded let expand = globalExpanded
if (this.clicked) { if (this.clicked) {
expand = expanded expand = expanded
} }
const times = this.calculateTimes(change) const times = this.calculateTimes(item)
const header = ( const header = (
<div className={`panel panel-default ${this.props.preferences.darkMode ? 'zuul-change-dark' : 'zuul-change'}`}> <div className={`panel panel-default ${this.props.preferences.darkMode ? 'zuul-change-dark' : 'zuul-change'}`}>
<div className={`panel-heading ${this.props.preferences.darkMode ? 'zuul-patchset-header-dark' : 'zuul-patchset-header'}`} <div className={`panel-heading ${this.props.preferences.darkMode ? 'zuul-patchset-header-dark' : 'zuul-patchset-header'}`}
onClick={this.onClick}> onClick={this.onClick}>
<div className='row'> <div>
<div className='col-xs-8'> {item.live === true ? (
<span className='change_project'>{change.project}</span>
<div className='row'> <div className='row'>
<div className='col-xs-4'> <div className='col-xs-6'>
{this.renderChangeLink(change)} {this.renderProgressBar(item)}
</div> </div>
<div className='col-xs-8'> <div className='col-xs-6 text-right'>
{this.renderProgressBar(change)} {this.renderTimer(item, times)}
</div> </div>
</div> </div>
</div>
{change.live === true ? (
<div className='col-xs-4 text-right'>
{this.renderTimer(change, times)}
</div>
) : ''} ) : ''}
{getChanges(item).map((change, idx) => (
<div key={idx} className='row'>
<div className='col-xs-8'>
<span className='change_project'>{change.project}</span>
</div>
<div className='col-xs-4 text-right'>
{this.renderChangeLink(change)}
</div>
</div>
))}
</div> </div>
</div> </div>
{expand ? this.renderJobList(change.jobs, times) : ''} {expand ? this.renderJobList(item.jobs, times) : ''}
</div > </div >
) )
return ( return (
@ -432,4 +441,4 @@ class ChangePanel extends React.Component {
export default connect(state => ({ export default connect(state => ({
tenant: state.tenant, tenant: state.tenant,
preferences: state.preferences, preferences: state.preferences,
}))(ChangePanel) }))(ItemPanel)

View File

@ -1,4 +1,5 @@
// Copyright 2018 Red Hat, Inc // Copyright 2018 Red Hat, Inc
// Copyright 2024 Acme Gating, LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); you may // 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 // not use this file except in compliance with the License. You may obtain
@ -20,11 +21,13 @@ import { Button } from '@patternfly/react-core'
import { setTenantAction } from '../../actions/tenant' import { setTenantAction } from '../../actions/tenant'
import configureStore from '../../store' import configureStore from '../../store'
import ChangePanel from './ChangePanel' import ItemPanel from './ItemPanel'
const fakeChange = { const fakeItem = {
project: 'org-project', changes: [{
project: 'org-project'
}],
jobs: [{ jobs: [{
name: 'job-name', name: 'job-name',
url: 'stream/42', url: 'stream/42',
@ -32,14 +35,13 @@ const fakeChange = {
}] }]
} }
it('item panel render multi tenant links', () => {
it('change panel render multi tenant links', () => {
const store = configureStore() const store = configureStore()
store.dispatch(setTenantAction('tenant-one', false)) store.dispatch(setTenantAction('tenant-one', false))
const application = create( const application = create(
<Provider store={store}> <Provider store={store}>
<Router> <Router>
<ChangePanel change={fakeChange} globalExpanded={true} /> <ItemPanel item={fakeItem} globalExpanded={true} />
</Router> </Router>
</Provider> </Provider>
) )
@ -50,13 +52,13 @@ it('change panel render multi tenant links', () => {
expect(skipButton === undefined) expect(skipButton === undefined)
}) })
it('change panel render white-label tenant links', () => { it('item panel render white-label tenant links', () => {
const store = configureStore() const store = configureStore()
store.dispatch(setTenantAction('tenant-one', true)) store.dispatch(setTenantAction('tenant-one', true))
const application = create( const application = create(
<Provider store={store}> <Provider store={store}>
<Router> <Router>
<ChangePanel change={fakeChange} globalExpanded={true} /> <ItemPanel item={fakeItem} globalExpanded={true} />
</Router> </Router>
</Provider> </Provider>
) )
@ -67,7 +69,77 @@ it('change panel render white-label tenant links', () => {
expect(skipButton === undefined) expect(skipButton === undefined)
}) })
it('change panel skip jobs', () => { it('item panel skip jobs', () => {
const fakeItem = {
changes: [{
project: 'org-project'
}],
jobs: [{
name: 'job-name',
url: 'stream/42',
result: 'skipped'
}]
}
const store = configureStore()
store.dispatch(setTenantAction('tenant-one', true))
const application = create(
<Provider store={store}>
<Router>
<ItemPanel item={fakeItem} globalExpanded={true} />
</Router>
</Provider>
)
const skipButton = application.root.findByType(Button)
expect(skipButton.props.children.includes('skipped job'))
})
/* Backwards compat; remove after circular dependency refactor */
const fakeChange = {
project: 'org-project',
jobs: [{
name: 'job-name',
url: 'stream/42',
result: null
}]
}
it('item panel backwards compat render multi tenant links', () => {
const store = configureStore()
store.dispatch(setTenantAction('tenant-one', false))
const application = create(
<Provider store={store}>
<Router>
<ItemPanel item={fakeChange} globalExpanded={true} />
</Router>
</Provider>
)
const jobLink = application.root.findByType(Link)
expect(jobLink.props.to).toEqual(
'/t/tenant-one/stream/42')
const skipButton = application.root.findAllByType(Button)
expect(skipButton === undefined)
})
it('item panel backwards compat render white-label tenant links', () => {
const store = configureStore()
store.dispatch(setTenantAction('tenant-one', true))
const application = create(
<Provider store={store}>
<Router>
<ItemPanel item={fakeChange} globalExpanded={true} />
</Router>
</Provider>
)
const jobLink = application.root.findByType(Link)
expect(jobLink.props.to).toEqual(
'/stream/42')
const skipButton = application.root.findAllByType(Button)
expect(skipButton === undefined)
})
it('item panel backwards compat skip jobs', () => {
const fakeChange = { const fakeChange = {
project: 'org-project', project: 'org-project',
jobs: [{ jobs: [{
@ -82,7 +154,7 @@ it('change panel skip jobs', () => {
const application = create( const application = create(
<Provider store={store}> <Provider store={store}>
<Router> <Router>
<ChangePanel change={fakeChange} globalExpanded={true} /> <ItemPanel item={fakeChange} globalExpanded={true} />
</Router> </Router>
</Provider> </Provider>
) )

View File

@ -209,7 +209,7 @@ a.refresh {
.zuul-change-total-result { .zuul-change-total-result {
height: 10px; height: 10px;
width: 100px; width: 180px;
margin: 0; margin: 0;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;

View File

@ -18,7 +18,7 @@ import { connect } from 'react-redux'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchChangeIfNeeded } from '../actions/change' import { fetchChangeIfNeeded } from '../actions/change'
import ChangePanel from '../containers/status/ChangePanel' import ItemPanel from '../containers/status/ItemPanel'
import { Fetchable } from '../containers/Fetching' import { Fetchable } from '../containers/Fetching'
@ -63,7 +63,7 @@ class ChangeStatusPage extends React.Component {
render () { render () {
const { remoteData } = this.props const { remoteData } = this.props
const change = remoteData.change const itemlist = remoteData.change
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<PageSection style={{paddingRight: '5px'}}> <PageSection style={{paddingRight: '5px'}}>
@ -72,11 +72,11 @@ class ChangeStatusPage extends React.Component {
fetchCallback={this.updateData} fetchCallback={this.updateData}
/> />
</PageSection> </PageSection>
{change && change.map((item, idx) => ( {itemlist && itemlist.map((item, idx) => (
<div className='row zuul-change-content' key={idx}> <div className='row zuul-change-content' key={idx}>
<ChangePanel <ItemPanel
globalExpanded={true} globalExpanded={true}
change={item} item={item}
/> />
</div> </div>
))} ))}