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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user