From 4a7e86f7f66c429f67085cd9e9e16097cf6a2842 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Thu, 1 Feb 2024 13:30:49 -0800 Subject: [PATCH] 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 --- web/src/Misc.jsx | 56 ++++++++--- web/src/containers/build/Build.jsx | 77 ++++++++-------- web/src/containers/build/BuildTable.jsx | 11 ++- web/src/containers/build/Buildset.jsx | 70 ++++---------- web/src/containers/build/BuildsetTable.jsx | 11 ++- web/src/containers/status/ChangeQueue.jsx | 12 +-- .../status/{Change.jsx => Item.jsx} | 70 ++++++++------ .../status/{ChangePanel.jsx => ItemPanel.jsx} | 59 +++++++----- ...hangePanel.test.jsx => ItemPanel.test.jsx} | 92 +++++++++++++++++-- web/src/index.css | 2 +- web/src/pages/ChangeStatus.jsx | 10 +- 11 files changed, 278 insertions(+), 192 deletions(-) rename web/src/containers/status/{Change.jsx => Item.jsx} (82%) rename web/src/containers/status/{ChangePanel.jsx => ItemPanel.jsx} (91%) rename web/src/containers/status/{ChangePanel.test.jsx => ItemPanel.test.jsx} (50%) diff --git a/web/src/Misc.jsx b/web/src/Misc.jsx index 1186f42d38..fafd318e3d 100644 --- a/web/src/Misc.jsx +++ b/web/src/Misc.jsx @@ -48,22 +48,22 @@ ExternalLink.propTypes = { children: PropTypes.node, } -function buildExternalLink(buildish) { +function buildExternalLink(ref) { /* TODO (felix): What should we show for periodic builds here? They don't provide a change, but the ref_url is also not usable */ - if (buildish.ref_url && buildish.change) { + if (ref.ref_url && ref.change) { return ( - + Change - {buildish.change},{buildish.patchset} + {ref.change},{ref.patchset} ) - } else if (buildish.ref_url && buildish.newrev) { + } else if (ref.ref_url && ref.newrev) { return ( - + Revision - {buildish.newrev.slice(0, 7)} + {ref.newrev.slice(0, 7)} ) } @@ -71,20 +71,20 @@ function buildExternalLink(buildish) { return null } -function buildExternalTableLink(buildish) { +function buildExternalTableLink(ref) { /* TODO (felix): What should we show for periodic builds here? They don't provide a change, but the ref_url is also not usable */ - if (buildish.ref_url && buildish.change) { + if (ref.ref_url && ref.change) { return ( - - {buildish.change},{buildish.patchset} + + {ref.change},{ref.patchset} ) - } else if (buildish.ref_url && buildish.newrev) { + } else if (ref.ref_url && ref.newrev) { return ( - - {buildish.newrev.slice(0, 7)} + + {ref.newrev.slice(0, 7)} ) } @@ -92,6 +92,32 @@ function buildExternalTableLink(buildish) { return null } +function renderRefInfo(ref) { + const refinfo = ref.branch ? ( + <> + Branch {ref.branch} + + ) : ( + <> + Ref {ref.ref} + + ) + const oldrev = ref.oldrev ? ( + <>
Old {ref.oldrev} + ) : ( <> ) + const newrev = ref.newrev ? ( + <>
New {ref.newrev} + ) : ( <> ) + + return ( + <> + {refinfo} + {oldrev} + {newrev} + + ) +} + function IconProperty(props) { const { icon, value, WrapElement = 'span' } = props 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 } diff --git a/web/src/containers/build/Build.jsx b/web/src/containers/build/Build.jsx index 5e80add0eb..29123b5377 100644 --- a/web/src/containers/build/Build.jsx +++ b/web/src/containers/build/Build.jsx @@ -22,8 +22,6 @@ import { BookIcon, BuildIcon, CodeBranchIcon, - CodeIcon, - CubeIcon, FileCodeIcon, FingerprintIcon, HistoryIcon, @@ -35,20 +33,36 @@ import { } from '@patternfly/react-icons' import * as moment from 'moment' import * as moment_tz from 'moment-timezone' +import _ from 'lodash' import 'moment-duration-format' import { BuildResultBadge, BuildResultWithIcon } from './Misc' -import { buildExternalLink, ExternalLink, IconProperty } from '../../Misc' +import { buildExternalLink, renderRefInfo, ExternalLink, IconProperty } from '../../Misc' 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 }) { const [showAutoholdModal, setShowAutoholdModal] = useState(false) - const change = build.change ? build.change : '' - const ref = build.change ? '' : build.ref - const project = build.project + const buildRef = getRef(build) + // the change or ref to use for api actions like autohold + const actionRef = buildRef.change ? '' : buildRef.ref + const actionChange = buildRef.change ? String(buildRef.change) : '' + //const project = build.project const job_name = build.job_name - const build_link = buildExternalLink(build) const index_links = build.manifest && build.manifest.index_links const build_log_url = build.log_url ? (index_links ? build.log_url + 'index.html' : build.log_url) @@ -78,7 +92,6 @@ function Build({ build, tenant, timezone, user }) { ) } - return ( <> <FlexItem> <List style={{ listStyle: 'none' }}> - {build_link && ( + {getRefs(build).map((ref, idx) => ( <IconProperty + key={idx} WrapElement={ListItem} - icon={<CodeIcon />} - value={build_link} + icon={<CodeBranchIcon />} + 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 WrapElement={ListItem} icon={<StreamIcon />} @@ -225,7 +220,7 @@ function Build({ build, tenant, timezone, user }) { '/builds?job_name=' + build.job_name + '&project=' + - build.project + buildRef.project } title="See previous runs of this job inside current project." > @@ -277,9 +272,9 @@ function Build({ build, tenant, timezone, user }) { {<AutoholdModal showAutoholdModal={showAutoholdModal} setShowAutoholdModal={setShowAutoholdModal} - change={String(change)} - changeRef={ref} - project={project} + change={actionChange} + changeRef={actionRef} + project={buildRef.project} jobName={job_name} />} </> diff --git a/web/src/containers/build/BuildTable.jsx b/web/src/containers/build/BuildTable.jsx index ae46463242..020058568a 100644 --- a/web/src/containers/build/BuildTable.jsx +++ b/web/src/containers/build/BuildTable.jsx @@ -50,6 +50,10 @@ import * as moment_tz from 'moment-timezone' import { BuildResult, BuildResultWithIcon } from './Misc' import { buildExternalTableLink, IconProperty } from '../../Misc' +function getRef(build) { + return 'project' in build ? build : build.ref +} + function BuildTable({ builds, fetching, @@ -101,7 +105,8 @@ function BuildTable({ ] function createBuildRow(build) { - const changeOrRefLink = buildExternalTableLink(build) + const ref = getRef(build) + const changeOrRefLink = buildExternalTableLink(ref) return { // 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, diff --git a/web/src/containers/build/Buildset.jsx b/web/src/containers/build/Buildset.jsx index f2c0be416b..3b3765e571 100644 --- a/web/src/containers/build/Buildset.jsx +++ b/web/src/containers/build/Buildset.jsx @@ -27,10 +27,8 @@ import { ModalVariant, } from '@patternfly/react-core' import { - CodeIcon, CodeBranchIcon, OutlinedCommentDotsIcon, - CubeIcon, FingerprintIcon, StreamIcon, OutlinedCalendarAltIcon, @@ -41,15 +39,19 @@ import * as moment from 'moment' import * as moment_tz from 'moment-timezone' import 'moment-duration-format' -import { buildExternalLink, IconProperty } from '../../Misc' +import { buildExternalLink, renderRefInfo, IconProperty } from '../../Misc' import { BuildResultBadge, BuildResultWithIcon } from './Misc' import { enqueue, enqueue_ref } from '../../api' import { addNotification, addApiError } from '../../actions/notifications' import { ChartModal } from '../charts/ChartModal' 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 }) { - const buildset_link = buildExternalLink(buildset) const [isGanttChartModalOpen, setIsGanttChartModalOpen] = useState(false) 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 ( <> <Title headingLevel="h2"> @@ -276,32 +252,20 @@ function Buildset({ buildset, timezone, tenant, user, preferences }) { <Flex flex={{ default: 'flex_1' }}> <FlexItem> <List style={{ listStyle: 'none' }}> - {/* TODO (felix): It would be cool if we could differentiate - 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 && ( + {getRefs(buildset).map((ref, idx) => ( <IconProperty WrapElement={ListItem} - icon={<CodeIcon />} - value={buildset_link} + icon={<CodeBranchIcon />} + 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 WrapElement={ListItem} icon={<StreamIcon />} diff --git a/web/src/containers/build/BuildsetTable.jsx b/web/src/containers/build/BuildsetTable.jsx index 8ba06cfbf6..c2a1dc6287 100644 --- a/web/src/containers/build/BuildsetTable.jsx +++ b/web/src/containers/build/BuildsetTable.jsx @@ -55,6 +55,10 @@ import * as moment_tz from 'moment-timezone' import { BuildResult, BuildResultWithIcon } from './Misc' import { buildExternalTableLink, IconProperty } from '../../Misc' +function getRef(buildset) { + return 'refs' in buildset ? buildset.refs[0] : buildset +} + function BuildsetTable({ buildsets, fetching, @@ -140,7 +144,8 @@ function BuildsetTable({ ] function createBuildsetRow(buildset) { - const changeOrRefLink = buildExternalTableLink(buildset) + const ref = getRef(buildset) + const changeOrRefLink = buildExternalTableLink(ref) let duration if (currentDuration === 'Buildset Duration') { @@ -163,12 +168,12 @@ function BuildsetTable({ <BuildResultWithIcon result={buildset.result} link={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}> - {buildset.project} + {ref.project} </BuildResultWithIcon> ), }, { - title: buildset.branch ? buildset.branch : buildset.ref, + title: ref.branch ? ref.branch : ref.ref, }, { title: buildset.pipeline, diff --git a/web/src/containers/status/ChangeQueue.jsx b/web/src/containers/status/ChangeQueue.jsx index 4b66979c58..63f3615332 100644 --- a/web/src/containers/status/ChangeQueue.jsx +++ b/web/src/containers/status/ChangeQueue.jsx @@ -17,7 +17,7 @@ import PropTypes from 'prop-types' import { Badge } from 'patternfly-react' import { Tooltip } from '@patternfly/react-core' -import Change from './Change' +import Item from './Item' class ChangeQueue extends React.Component { @@ -38,15 +38,15 @@ class ChangeQueue extends React.Component { shortName = shortName.substr(0, 32) + '...' } let changesList = [] - queue.heads.forEach((changes, changeIdx) => { - changes.forEach((change, idx) => { + queue.heads.forEach((items, itemIdx) => { + items.forEach((item, idx) => { changesList.push( - <Change - change={change} + <Item + item={item} queue={queue} expanded={expanded} pipeline={pipeline} - key={changeIdx.toString() + idx} + key={itemIdx.toString() + idx} />) }) }) diff --git a/web/src/containers/status/Change.jsx b/web/src/containers/status/Item.jsx similarity index 82% rename from web/src/containers/status/Change.jsx rename to web/src/containers/status/Item.jsx index a825e348be..9bdfef5320 100644 --- a/web/src/containers/status/Change.jsx +++ b/web/src/containers/status/Item.jsx @@ -37,12 +37,17 @@ import { fetchStatusIfNeeded } from '../../actions/status' import LineAngleImage from '../../images/line-angle.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 = { - change: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, queue: PropTypes.object.isRequired, expanded: PropTypes.bool.isRequired, pipeline: PropTypes.object, @@ -59,7 +64,10 @@ class Change extends React.Component { } 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 changeId = change.id || 'N/A' let changeRef = change.ref @@ -90,7 +98,8 @@ class Change extends React.Component { renderDequeueModal() { const { showDequeueModal } = this.state - const { change } = this.props + const { item } = this.props + const change = getChange(item) let projectName = change.project let changeId = change.id || change.ref const title = 'You are about to dequeue a change' @@ -111,7 +120,8 @@ class Change extends React.Component { } promoteConfirm = () => { - const { tenant, change, pipeline } = this.props + const { tenant, item, pipeline } = this.props + const change = getChange(item) let changeId = change.id || 'NA' this.setState(() => ({ showPromoteModal: false })) if (changeId !== 'N/A') { @@ -138,7 +148,8 @@ class Change extends React.Component { renderPromoteModal() { const { showPromoteModal } = this.state - const { change } = this.props + const { item } = this.props + const change = getChange(item) let changeId = change.id || 'N/A' const title = 'You are about to promote a change' return ( @@ -166,7 +177,7 @@ class Change extends React.Component { icon={<BanIcon style={{ color: 'var(--pf-global--danger-color--100)', }} />} - description="Stop all jobs for this change" + description="Stop all jobs for this item" onClick={(event) => { event.preventDefault() this.setState(() => ({ showDequeueModal: true })) @@ -177,7 +188,7 @@ class Change extends React.Component { icon={<AngleDoubleUpIcon style={{ 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) => { event.preventDefault() this.setState(() => ({ showPromoteModal: true })) @@ -207,20 +218,19 @@ class Change extends React.Component { } - - renderStatusIcon(change) { + renderStatusIcon(item) { let iconGlyph = 'pficon pficon-ok' let iconTitle = 'Succeeding' - if (change.active !== true) { + if (item.active !== true) { iconGlyph = 'pficon pficon-pending' iconTitle = 'Waiting until closer to head of queue to' + ' start jobs' - } else if (change.live !== true) { + } else if (item.live !== true) { iconGlyph = 'pficon pficon-info' - iconTitle = 'Dependent change required for testing' - } else if (change.failing_reasons && - change.failing_reasons.length > 0) { - let reason = change.failing_reasons.join(', ') + iconTitle = 'Dependent item required for testing' + } else if (item.failing_reasons && + item.failing_reasons.length > 0) { + let reason = item.failing_reasons.join(', ') iconTitle = 'Failing because ' + reason if (reason.match(/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} title={iconTitle} /> ) - if (change.live) { + if (item.live) { return ( - <Link to={this.props.tenant.linkPrefix + '/status/change/' + change.id}> + <Link to={this.props.tenant.linkPrefix + '/status/change/' + getChange(item).id}> {icon} </Link> ) @@ -244,9 +254,9 @@ class Change extends React.Component { } } - renderLineImg(change, i) { + renderLineImg(item, i) { 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 image = LineAngleImage } @@ -254,13 +264,13 @@ class Change extends React.Component { } render() { - const { change, queue, expanded, pipeline, user, tenant } = this.props + const { item, queue, expanded, pipeline, user, tenant } = this.props let row = [] let adminMenuWidth = 15 let i for (i = 0; i < queue._tree_columns; i++) { 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) { className = ' zuul-change-row-line-dark' } else { @@ -269,19 +279,19 @@ class Change extends React.Component { } row.push( <td key={i} className={'zuul-change-row' + className}> - {i === change._tree_index ? this.renderStatusIcon(change) : ''} - {change._tree_branches.indexOf(i) !== -1 ? ( - this.renderLineImg(change, i)) : ''} + {i === item._tree_index ? this.renderStatusIcon(item) : ''} + {item._tree_branches.indexOf(i) !== -1 ? ( + this.renderLineImg(item, i)) : ''} </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 - 16 * queue._tree_columns row.push( <td key={i + 1} className="zuul-change-cell" - style={{ width: changeWidth + 'px' }}> - <ChangePanel change={change} globalExpanded={expanded} pipeline={pipeline} /> + style={{ width: itemWidth + 'px' }}> + <ItemPanel item={item} globalExpanded={expanded} pipeline={pipeline} /> </td> ) if (user.isAdmin && user.scope.indexOf(tenant.name) !== -1) { @@ -311,4 +321,4 @@ export default connect(state => ({ tenant: state.tenant, user: state.user, preferences: state.preferences, -}))(Change) +}))(Item) diff --git a/web/src/containers/status/ChangePanel.jsx b/web/src/containers/status/ItemPanel.jsx similarity index 91% rename from web/src/containers/status/ChangePanel.jsx rename to web/src/containers/status/ItemPanel.jsx index a992f89de8..17c7ecd6b0 100644 --- a/web/src/containers/status/ChangePanel.jsx +++ b/web/src/containers/status/ItemPanel.jsx @@ -1,4 +1,5 @@ // Copyright 2018 Red Hat, Inc +// Copyright 2024 Acme Gating, LLC // // 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 @@ -20,11 +21,15 @@ import * as moment from 'moment' import 'moment-duration-format' 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 = { globalExpanded: PropTypes.bool.isRequired, - change: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, tenant: PropTypes.object, preferences: PropTypes.object } @@ -175,13 +180,13 @@ class ChangePanel extends React.Component { } return ( <React.Fragment> - <small title='Remaining Time' className='time' style={{display: 'inline'}}> - {remainingTime} - </small> - <br /> <small title='Elapsed Time' className='time' style={{display: 'inline'}}> {this.enqueueTime(change.enqueue_time)} </small> + <small> | </small> + <small title='Remaining Time' className='time' style={{display: 'inline'}}> + {remainingTime} + </small> </React.Fragment> ) } @@ -347,12 +352,12 @@ class ChangePanel extends React.Component { ) } - calculateTimes (change) { + calculateTimes (item) { let maxRemaining = 0 let jobs = {} const now = Date.now() - for (const job of change.jobs) { + for (const job of item.jobs) { let jobElapsed = null let jobRemaining = null 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 // use our value if it's oky to calculate it. - if (change.remaininging_time === null) { + if (item.remaininging_time === null) { maxRemaining = null } return { @@ -389,36 +394,40 @@ class ChangePanel extends React.Component { render () { const { expanded } = this.state - const { change, globalExpanded } = this.props + const { item, globalExpanded } = this.props let expand = globalExpanded if (this.clicked) { expand = expanded } - const times = this.calculateTimes(change) + const times = this.calculateTimes(item) const header = ( <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'}`} onClick={this.onClick}> - <div className='row'> - <div className='col-xs-8'> - <span className='change_project'>{change.project}</span> + <div> + {item.live === true ? ( <div className='row'> - <div className='col-xs-4'> - {this.renderChangeLink(change)} + <div className='col-xs-6'> + {this.renderProgressBar(item)} </div> - <div className='col-xs-8'> - {this.renderProgressBar(change)} + <div className='col-xs-6 text-right'> + {this.renderTimer(item, times)} </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> - {expand ? this.renderJobList(change.jobs, times) : ''} + {expand ? this.renderJobList(item.jobs, times) : ''} </div > ) return ( @@ -432,4 +441,4 @@ class ChangePanel extends React.Component { export default connect(state => ({ tenant: state.tenant, preferences: state.preferences, -}))(ChangePanel) +}))(ItemPanel) diff --git a/web/src/containers/status/ChangePanel.test.jsx b/web/src/containers/status/ItemPanel.test.jsx similarity index 50% rename from web/src/containers/status/ChangePanel.test.jsx rename to web/src/containers/status/ItemPanel.test.jsx index cd27edc737..06a658d5d7 100644 --- a/web/src/containers/status/ChangePanel.test.jsx +++ b/web/src/containers/status/ItemPanel.test.jsx @@ -1,4 +1,5 @@ // Copyright 2018 Red Hat, Inc +// Copyright 2024 Acme Gating, LLC // // 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 @@ -20,11 +21,13 @@ import { Button } from '@patternfly/react-core' import { setTenantAction } from '../../actions/tenant' import configureStore from '../../store' -import ChangePanel from './ChangePanel' +import ItemPanel from './ItemPanel' -const fakeChange = { - project: 'org-project', +const fakeItem = { + changes: [{ + project: 'org-project' + }], jobs: [{ name: 'job-name', url: 'stream/42', @@ -32,14 +35,13 @@ const fakeChange = { }] } - -it('change panel render multi tenant links', () => { +it('item panel render multi tenant links', () => { const store = configureStore() store.dispatch(setTenantAction('tenant-one', false)) const application = create( <Provider store={store}> <Router> - <ChangePanel change={fakeChange} globalExpanded={true} /> + <ItemPanel item={fakeItem} globalExpanded={true} /> </Router> </Provider> ) @@ -50,13 +52,13 @@ it('change panel render multi tenant links', () => { expect(skipButton === undefined) }) -it('change panel render white-label tenant links', () => { +it('item panel render white-label tenant links', () => { const store = configureStore() store.dispatch(setTenantAction('tenant-one', true)) const application = create( <Provider store={store}> <Router> - <ChangePanel change={fakeChange} globalExpanded={true} /> + <ItemPanel item={fakeItem} globalExpanded={true} /> </Router> </Provider> ) @@ -67,7 +69,77 @@ it('change panel render white-label tenant links', () => { 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 = { project: 'org-project', jobs: [{ @@ -82,7 +154,7 @@ it('change panel skip jobs', () => { const application = create( <Provider store={store}> <Router> - <ChangePanel change={fakeChange} globalExpanded={true} /> + <ItemPanel item={fakeChange} globalExpanded={true} /> </Router> </Provider> ) diff --git a/web/src/index.css b/web/src/index.css index 4cedc144f0..c86f280b0b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -209,7 +209,7 @@ a.refresh { .zuul-change-total-result { height: 10px; - width: 100px; + width: 180px; margin: 0; display: inline-block; vertical-align: middle; diff --git a/web/src/pages/ChangeStatus.jsx b/web/src/pages/ChangeStatus.jsx index 02846ef983..e0ffa022db 100644 --- a/web/src/pages/ChangeStatus.jsx +++ b/web/src/pages/ChangeStatus.jsx @@ -18,7 +18,7 @@ import { connect } from 'react-redux' import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { fetchChangeIfNeeded } from '../actions/change' -import ChangePanel from '../containers/status/ChangePanel' +import ItemPanel from '../containers/status/ItemPanel' import { Fetchable } from '../containers/Fetching' @@ -63,7 +63,7 @@ class ChangeStatusPage extends React.Component { render () { const { remoteData } = this.props - const change = remoteData.change + const itemlist = remoteData.change return ( <PageSection variant={PageSectionVariants.light}> <PageSection style={{paddingRight: '5px'}}> @@ -72,11 +72,11 @@ class ChangeStatusPage extends React.Component { fetchCallback={this.updateData} /> </PageSection> - {change && change.map((item, idx) => ( + {itemlist && itemlist.map((item, idx) => ( <div className='row zuul-change-content' key={idx}> - <ChangePanel + <ItemPanel globalExpanded={true} - change={item} + item={item} /> </div> ))}