Web: fix tabs on project page

This corrects the tab titles on the project page which currently
typically just say "master", "master", "master", ... because they
all display the default branch of the project stanza.

Instead, use the branch of the source context for the project stanza,
or, if the project stanza is not from the current project, then
use the name of its project.

This causes them to appear like:

"openstack/project-config", "master", "stable/diablo", ...

Also, update the entire Project page component hierarchy to use
hooks instead of classes.

Update the styling on the H2 element so that we can have the
refresh icon share the same vertical space (so that we don't have
large amounts of wasted vertical space at the top of each page.

Change-Id: I863e0eb4a7f20ee6363e596e61cc49b2cbc22953
This commit is contained in:
James E. Blair
2022-08-04 10:56:10 -07:00
parent af80f8dfdc
commit 8494ebf397
4 changed files with 141 additions and 146 deletions
+28 -51
View File
@@ -1,4 +1,5 @@
// Copyright 2018 Red Hat, Inc
// Copyright 2022 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
@@ -12,68 +13,44 @@
// License for the specific language governing permissions and limitations
// under the License.
import * as React from 'react'
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import {
Nav,
NavItem,
TabContainer,
TabPane,
TabContent,
} from 'patternfly-react'
Tabs,
Tab,
} from '@patternfly/react-core'
import ProjectVariant from './ProjectVariant'
class Project extends React.Component {
static propTypes = {
project: PropTypes.object.isRequired,
}
function Project(props) {
const [variantIdx, setVariantIdx] = useState(0)
const { project } = props
state = {
variantIdx: 0,
}
renderVariantTitle (variant, selected) {
let title = variant.default_branch
if (selected) {
title = <strong>{title}</strong>
}
function renderVariantTitle (variant) {
let title = variant.source_context.project === project.name ?
variant.source_context.branch : variant.source_context.project
return title
}
render () {
const { project } = this.props
const { variantIdx } = this.state
return (
<React.Fragment>
<Tabs activeKey={variantIdx}
onSelect={(event, tabIndex) => setVariantIdx(tabIndex)}
isBox>
{project.configs.map((variant, idx) => (
<Tab key={idx} eventKey={idx}
title={renderVariantTitle(variant)}>
<ProjectVariant variant={variant} />
</Tab>
))}
</Tabs>
</React.Fragment>
)
}
return (
<React.Fragment>
<h2>{project.canonical_name}</h2>
<TabContainer id="zuul-project">
<div>
<Nav bsClass="nav nav-tabs nav-tabs-pf">
{project.configs.map((variant, idx) => (
<NavItem
key={idx}
onClick={() => this.setState({variantIdx: idx})}>
<div>
{this.renderVariantTitle(variant, variantIdx === idx)}
</div>
</NavItem>
))}
</Nav>
<TabContent>
<TabPane>
{project.configs[variantIdx] && (
<ProjectVariant variant={project.configs[variantIdx]} />
)}
</TabPane>
</TabContent>
</div>
</TabContainer>
</React.Fragment>
)
}
Project.propTypes = {
project: PropTypes.object.isRequired,
}
export default Project
+57 -53
View File
@@ -18,64 +18,68 @@ import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
class ProjectVariant extends React.Component {
static propTypes = {
tenant: PropTypes.object,
variant: PropTypes.object.isRequired
function ProjectVariant(props) {
const { tenant, variant } = props
const rows = []
rows.push({label: 'Merge mode', value: variant.merge_mode})
if (variant.templates.length > 0) {
const templateList = (
<ul className='list-group'>
{variant.templates.map((item, idx) => (
<li className='list-group-item' key={idx}>{item}</li>))}
</ul>
)
rows.push({label: 'Templates', value: templateList})
}
render () {
const { tenant, variant } = this.props
const rows = []
rows.push({label: 'Merge mode', value: variant.merge_mode})
if (variant.templates.length > 0) {
const templateList = (
variant.pipelines.forEach(pipeline => {
// TODO: either adds job link anchor to load the right variant
// and/or show the job variant config in a modal?
const jobList = (
<React.Fragment>
{pipeline.queue_name && (
<p><strong>Queue: </strong> {pipeline.queue_name} </p>)}
<ul className='list-group'>
{variant.templates.map((item, idx) => (
<li className='list-group-item' key={idx}>{item}</li>))}
{pipeline.jobs.map((item, idx) => (
<li className='list-group-item' key={idx}>
<Link to={tenant.linkPrefix + '/job/' + item[0].name}>
{item[0].name}
</Link>
</li>
))}
</ul>
)
rows.push({label: 'Templates', value: templateList})
}
variant.pipelines.forEach(pipeline => {
// TODO: either adds job link anchor to load the right variant
// and/or show the job variant config in a modal?
const jobList = (
<React.Fragment>
{pipeline.queue_name && (
<p><strong>Queue: </strong> {pipeline.queue_name} </p>)}
<ul className='list-group'>
{pipeline.jobs.map((item, idx) => (
<li className='list-group-item' key={idx}>
<Link to={tenant.linkPrefix + '/job/' + item[0].name}>
{item[0].name}
</Link>
</li>
))}
</ul>
</React.Fragment>
)
rows.push({label: pipeline.name + ' jobs', value: jobList})
})
return (
<div>
<table className='table table-striped table-bordered'>
<tbody>
{rows.map(item => (
<tr key={item.label}>
<td style={{width: '10%'}}>{item.label}</td>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
</div>
</React.Fragment>
)
rows.push({label: pipeline.name + ' jobs', value: jobList})
})
return (
<div>
<table className='table table-striped table-bordered'>
<tbody>
{rows.map(item => (
<tr key={item.label}>
<td style={{width: '10%'}}>{item.label}</td>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
ProjectVariant.propTypes = {
tenant: PropTypes.object,
variant: PropTypes.object.isRequired
}
function mapStateToProps(state) {
return {
tenant: state.tenant,
}
}
export default connect(state => ({tenant: state.tenant}))(ProjectVariant)
export default connect(mapStateToProps)(ProjectVariant)
+6
View File
@@ -3,6 +3,12 @@ body {
padding: 0;
}
/* Make the H2 header inline-block so that the refresh icon/button can
share space with it floating on the right. */
h2 {
display: inline-block;
}
.pf-c-title {
padding-bottom: 10px;
}
+50 -42
View File
@@ -1,4 +1,5 @@
// Copyright 2018 Red Hat, Inc
// Copyright 2022 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
@@ -12,67 +13,74 @@
// License for the specific language governing permissions and limitations
// under the License.
import * as React from 'react'
import React, { useEffect, useCallback } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import {
PageSection,
PageSectionVariants,
Text,
TextContent,
} from '@patternfly/react-core'
import Project from '../containers/project/Project'
import JobGraph from '../containers/jobgraph/JobGraph'
import { fetchProjectIfNeeded } from '../actions/project'
import { Fetchable } from '../containers/Fetching'
class ProjectPage extends React.Component {
static propTypes = {
match: PropTypes.object.isRequired,
tenant: PropTypes.object,
remoteData: PropTypes.object,
dispatch: PropTypes.func
}
function ProjectPage(props) {
const { tenant, fetchProjectIfNeeded, remoteData } = props
const { projectName } = props.match.params
const tenantProjects = remoteData.projects[tenant.name]
updateData = (force) => {
this.props.dispatch(fetchProjectIfNeeded(
this.props.tenant, this.props.match.params.projectName, force))
}
componentDidMount () {
document.title = 'Zuul Project | ' + this.props.match.params.projectName
if (this.props.tenant.name) {
this.updateData()
const updateData = useCallback((force) => {
if (tenant.name) {
fetchProjectIfNeeded(tenant, projectName, force)
}
}
}, [tenant, projectName, fetchProjectIfNeeded])
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
}
useEffect(() => {
document.title = 'Zuul Project | ' + projectName
updateData()
}, [tenant, projectName, updateData])
render () {
const { remoteData } = this.props
const tenantProjects = remoteData.projects[this.props.tenant.name]
const projectName = this.props.match.params.projectName
return (
return (
<>
<PageSection variant={PageSectionVariants.light}>
<PageSection style={{paddingRight: '5px'}}>
<Fetchable
isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
<TextContent>
<Text component="h2">Project {projectName}</Text>
<Fetchable
isFetching={remoteData.isFetching}
fetchCallback={updateData}
/>
</TextContent>
{tenantProjects && tenantProjects[projectName] &&
<>
<Project project={tenantProjects[projectName]} />
<JobGraph project={tenantProjects[projectName]} />
</>
}
</PageSection>
)
</PageSection>
</>
)
}
ProjectPage.propTypes = {
match: PropTypes.object.isRequired,
tenant: PropTypes.object,
remoteData: PropTypes.object,
fetchProjectIfNeeded: PropTypes.func,
}
function mapStateToProps(state) {
return {
tenant: state.tenant,
remoteData: state.project,
}
}
export default connect(state => ({
tenant: state.tenant,
remoteData: state.project,
}))(ProjectPage)
const mapDispatchToProps = {
fetchProjectIfNeeded,
}
export default connect(mapStateToProps, mapDispatchToProps)(ProjectPage)