PF4: Update builds and buildsets tables + use newest patternfly release
This updates the tables shown on the builds and buildsets pages with new PF4 components. While the data is loading, a fetching animation is shown and in case no results could be found the page shows an empty state. Each table row is clickable and points to the build/buildset result page. The table uses the same hover effect as the build list on the buildset result page. This change also updates the used @patternfly/react-core version to match the current version of @patternfly/react-table [1]. Otherwise, some of the styles in the different @patternfly/react-core versions conflict with each other leading to wrong font-weights and wrong text colors in labels. [1] patternfly.org/v4/documentation/react/overview/release-notes#202010-release-notes-2020-08-17 Change-Id: I0f5e0815c53d18e8ae3570dc94594c77fecb90ce
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@patternfly/react-core": "^4.18.5",
|
||||
"@patternfly/react-core": "^4.40.4",
|
||||
"@patternfly/react-table": "^4.15.5",
|
||||
"axios": "^0.19.0",
|
||||
"immutability-helper": "^2.8.1",
|
||||
"js-yaml": "^3.13.0",
|
||||
|
||||
268
web/src/containers/build/BuildTable.jsx
Normal file
268
web/src/containers/build/BuildTable.jsx
Normal file
@@ -0,0 +1,268 @@
|
||||
// Copyright 2020 BMW Group
|
||||
//
|
||||
// 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
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
EmptyStateSecondaryActions,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
BuildIcon,
|
||||
CodeBranchIcon,
|
||||
CodeIcon,
|
||||
CubeIcon,
|
||||
OutlinedCalendarAltIcon,
|
||||
OutlinedClockIcon,
|
||||
PollIcon,
|
||||
StreamIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
import 'moment-duration-format'
|
||||
import * as moment from 'moment'
|
||||
|
||||
import { BuildResult, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { ExternalLink } from '../../Misc'
|
||||
|
||||
function BuildTable(props) {
|
||||
const { builds, fetching, onClearFilters, tenant, timezone } = props
|
||||
const columns = [
|
||||
{
|
||||
title: <IconProperty icon={<BuildIcon />} value="Job" />,
|
||||
dataLabel: 'Job',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CubeIcon />} value="Project" />,
|
||||
dataLabel: 'Project',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CodeBranchIcon />} value="Branch" />,
|
||||
dataLabel: 'Branch',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<StreamIcon />} value="Pipeline" />,
|
||||
dataLabel: 'Pipeline',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CodeIcon />} value="Change" />,
|
||||
dataLabel: 'Change',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<OutlinedClockIcon />} value="Duration" />,
|
||||
dataLabel: 'Duration',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<IconProperty icon={<OutlinedCalendarAltIcon />} value="Start time" />
|
||||
),
|
||||
dataLabel: 'Start time',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<PollIcon />} value="Result" />,
|
||||
dataLabel: 'Result',
|
||||
},
|
||||
]
|
||||
|
||||
function createBuildRow(build) {
|
||||
// This link will be defined on each cell of the current row as this is the
|
||||
// only way to define a valid HTML link on a table row. Although we could
|
||||
// simply define an onClick handler on the whole row and programatically
|
||||
// switch to the buildresult page, this wouldn't provide the same
|
||||
// look-and-feel as a plain HTML link.
|
||||
const buildResultLink = (
|
||||
<Link
|
||||
to={`${tenant.linkPrefix}/build/${build.uuid}`}
|
||||
className="zuul-stretched-link"
|
||||
/>
|
||||
)
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
// To allow passing anything else than simple string values to a table
|
||||
// cell, we must use the title attribute.
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
<BuildResultWithIcon result={build.result} colored={build.voting}>
|
||||
{build.job_name}
|
||||
{!build.voting && ' (non-voting)'}
|
||||
</BuildResultWithIcon>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
<span>{build.project}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
<span>{build.branch ? build.branch : build.ref}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
<span>{build.pipeline}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
{build.change && (
|
||||
<span style={{ zIndex: 1, position: 'relative' }}>
|
||||
<ExternalLink target={build.ref_url}>
|
||||
{build.change},{build.patchset}
|
||||
</ExternalLink>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
<span>
|
||||
{moment
|
||||
.duration(build.duration, 'seconds')
|
||||
.format('h [hr] m [min] s [sec]')}
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
<span>
|
||||
{moment
|
||||
.utc(build.start_time)
|
||||
.tz(timezone)
|
||||
.format('YYYY-MM-DD HH:mm:ss')}
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildResultLink}
|
||||
<BuildResult result={build.result} colored={build.voting} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
// The dataLabel property is used to show the column header in a list-like
|
||||
// format for smaller viewports. When we are fetching, we don't want the
|
||||
// fetching row to be prepended by a "Job" column header. The other column
|
||||
// headers are not relevant here since we only have a single cell in the
|
||||
// fetcihng row.
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
rows = builds.map((build) => createBuildRow(build))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
aria-label="Builds Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-build-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any builds but are also not
|
||||
fetching */}
|
||||
{!fetching && builds.length === 0 && (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={BuildIcon} />
|
||||
<Title headingLevel="h1">No builds found</Title>
|
||||
<EmptyStateBody>
|
||||
No builds match this filter criteria. Remove some filters or clear
|
||||
all to show results.
|
||||
</EmptyStateBody>
|
||||
<EmptyStateSecondaryActions>
|
||||
<Button variant="link" onClick={onClearFilters}>
|
||||
Clear all filters
|
||||
</Button>
|
||||
</EmptyStateSecondaryActions>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BuildTable.propTypes = {
|
||||
builds: PropTypes.array.isRequired,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
onClearFilters: PropTypes.func.isRequired,
|
||||
tenant: PropTypes.object.isRequired,
|
||||
timezone: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
tenant: state.tenant,
|
||||
timezone: state.timezone,
|
||||
}))(BuildTable)
|
||||
212
web/src/containers/build/BuildsetTable.jsx
Normal file
212
web/src/containers/build/BuildsetTable.jsx
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright 2020 BMW Group
|
||||
//
|
||||
// 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
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
EmptyStateSecondaryActions,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
BuildIcon,
|
||||
CodeBranchIcon,
|
||||
CodeIcon,
|
||||
CubeIcon,
|
||||
PollIcon,
|
||||
StreamIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
|
||||
import { BuildResult, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { ExternalLink } from '../../Misc'
|
||||
|
||||
function BuildsetTable(props) {
|
||||
const { buildsets, fetching, onClearFilters, tenant } = props
|
||||
const columns = [
|
||||
{
|
||||
title: <IconProperty icon={<CubeIcon />} value="Project" />,
|
||||
dataLabel: 'Project',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CodeBranchIcon />} value="Branch" />,
|
||||
dataLabel: 'Branch',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<StreamIcon />} value="Pipeline" />,
|
||||
dataLabel: 'Pipeline',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CodeIcon />} value="Change" />,
|
||||
dataLabel: 'Change',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<PollIcon />} value="Result" />,
|
||||
dataLabel: 'Result',
|
||||
},
|
||||
]
|
||||
|
||||
function createBuildsetRow(buildset) {
|
||||
// This link will be defined on each cell of the current row as this is the
|
||||
// only way to define a valid HTML link on a table row. Although we could
|
||||
// simply define an onClick handler on the whole row and programatically
|
||||
// switch to the buildresult page, this wouldn't provide the same
|
||||
// look-and-feel as a plain HTML link.
|
||||
const buildsetResultLink = (
|
||||
<Link
|
||||
to={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}
|
||||
className="zuul-stretched-link"
|
||||
/>
|
||||
)
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
// To allow passing anything else than simple string values to a table
|
||||
// cell, we must use the title attribute.
|
||||
title: (
|
||||
<>
|
||||
{buildsetResultLink}
|
||||
<BuildResultWithIcon result={buildset.result}>
|
||||
{buildset.project}
|
||||
</BuildResultWithIcon>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildsetResultLink}
|
||||
<span>{buildset.branch ? buildset.branch : buildset.ref}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildsetResultLink}
|
||||
<span>{buildset.pipeline}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildsetResultLink}
|
||||
{buildset.change && (
|
||||
<span style={{ zIndex: 1, position: 'relative' }}>
|
||||
<ExternalLink target={buildset.ref_url}>
|
||||
{buildset.change},{buildset.patchset}
|
||||
</ExternalLink>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
{buildsetResultLink}
|
||||
<BuildResult result={buildset.result} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
// The dataLabel property is used to show the column header in a list-like
|
||||
// format for smaller viewports. When we are fetching, we don't want the
|
||||
// fetching row to be prepended by a "Project" column header. The other
|
||||
// column headers are not relevant here since we only have a single cell in
|
||||
// the fetching row.
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
rows = buildsets.map((buildset) => createBuildsetRow(buildset))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
aria-label="Builds Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-build-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any buildsets but are also
|
||||
not fetching */}
|
||||
{!fetching && buildsets.length === 0 && (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={BuildIcon} />
|
||||
<Title headingLevel="h1">No buildsets found</Title>
|
||||
<EmptyStateBody>
|
||||
No buildsets match this filter criteria. Remove some filters or
|
||||
clear all to show results.
|
||||
</EmptyStateBody>
|
||||
<EmptyStateSecondaryActions>
|
||||
<Button variant="link" onClick={onClearFilters}>
|
||||
Clear all filters
|
||||
</Button>
|
||||
</EmptyStateSecondaryActions>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BuildsetTable.propTypes = {
|
||||
buildsets: PropTypes.array.isRequired,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
onClearFilters: PropTypes.func.isRequired,
|
||||
tenant: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default connect((state) => ({ tenant: state.tenant }))(BuildsetTable)
|
||||
@@ -38,16 +38,82 @@ a.refresh {
|
||||
margin-left: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
/* Build Lists */
|
||||
/*
|
||||
* Build Lists and Tables
|
||||
*/
|
||||
|
||||
/* Improve the hover effect of selected lines in the selectable data list*/
|
||||
.pf-c-data-list__item.pf-m-selectable:hover:not(.pf-m-selected),
|
||||
.pf-c-data-list__item.pf-m-selectable:focus:not(.pf-m-selected) {
|
||||
/* Improve the hover/focus effect of selected lines */
|
||||
--pf-c-data-list__item--before--BackgroundColor: var(
|
||||
--pf-c-data-list__item--m-selected--before--BackgroundColor
|
||||
);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.zuul-build-list:hover a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Keep the normal font-size for compact tables */
|
||||
.zuul-build-table td {
|
||||
font-size: var(--pf-global--FontSize--md);
|
||||
}
|
||||
|
||||
/* Use the same hover effect on table rows like for the selectable data list */
|
||||
.zuul-build-table tbody tr:hover {
|
||||
box-shadow: var(--pf-global--BoxShadow--sm-top),
|
||||
var(--pf-global--BoxShadow--sm-bottom);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
/* For the small-screen table layout the before element is already used to
|
||||
show the column names. Thus, we fall back to the border to show the hover
|
||||
effect. The drawback with that is, that we can't show a nice transition.
|
||||
*/
|
||||
.zuul-build-table tbody tr:hover {
|
||||
border-left-color: var(--pf-global--active-color--100);
|
||||
border-left-width: var(--pf-global--BorderWidth--lg);
|
||||
border-left-style: solid;
|
||||
/* Compensate the border width with a negative margin */
|
||||
margin-left: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
/* For the larger screens (normal table layout) we can use the before
|
||||
element on the first table cell to show the same hover effect like for
|
||||
the data list */
|
||||
.zuul-build-table tbody tr td:first-child::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: var(--pf-global--BorderWidth--lg);
|
||||
content: "";
|
||||
background-color: transparent;
|
||||
transition: var(--pf-global--Transition);
|
||||
}
|
||||
|
||||
.zuul-build-table tbody tr:hover td:first-child::before {
|
||||
background-color: var(--pf-global--active-color--100);
|
||||
}
|
||||
}
|
||||
|
||||
/* Make a link stretch the whole parent element (in this case the whole table
|
||||
cell) */
|
||||
.zuul-stretched-link::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
pointer-events: auto;
|
||||
content: "";
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Build/Buildset result page
|
||||
*/
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Table } from 'patternfly-react'
|
||||
import * as moment from 'moment-timezone'
|
||||
import 'moment-duration-format'
|
||||
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
|
||||
@@ -28,6 +25,7 @@ import {
|
||||
getFiltersFromUrl,
|
||||
writeFiltersToUrl,
|
||||
} from '../containers/FilterToolbar'
|
||||
import BuildTable from '../containers/build/BuildTable'
|
||||
|
||||
class BuildsPage extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -39,7 +37,6 @@ class BuildsPage extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super()
|
||||
this.prepareTableHeaders()
|
||||
this.filterCategories = [
|
||||
{
|
||||
key: 'job_name',
|
||||
@@ -87,7 +84,8 @@ class BuildsPage extends React.Component {
|
||||
]
|
||||
|
||||
this.state = {
|
||||
builds: null,
|
||||
builds: [],
|
||||
fetching: false,
|
||||
filters: getFiltersFromUrl(props.location, this.filterCategories),
|
||||
}
|
||||
}
|
||||
@@ -100,9 +98,15 @@ class BuildsPage extends React.Component {
|
||||
// passed as prop and doesn't change since the page itself wasn't
|
||||
// re-rendered.
|
||||
const queryString = buildQueryString(filters)
|
||||
this.setState({builds: null})
|
||||
fetchBuilds(this.props.tenant.apiPrefix, queryString).then(response => {
|
||||
this.setState({builds: response.data})
|
||||
this.setState({ fetching: true })
|
||||
// TODO (felix): What happens in case of a broken network connection? Is the
|
||||
// fetching shows infinitely or can we catch this and show an erro state in
|
||||
// the table instead?
|
||||
fetchBuilds(this.props.tenant.apiPrefix, queryString).then((response) => {
|
||||
this.setState({
|
||||
builds: response.data,
|
||||
fetching: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,107 +141,17 @@ class BuildsPage extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
prepareTableHeaders() {
|
||||
const headerFormat = value => <Table.Heading>{value}</Table.Heading>
|
||||
const cellFormat = (value) => (
|
||||
<Table.Cell>{value}</Table.Cell>)
|
||||
const linkBuildFormat = (value, rowdata) => (
|
||||
<Table.Cell>
|
||||
<Link to={this.props.tenant.linkPrefix + '/build/' + rowdata.rowData.uuid}>{value}</Link>
|
||||
</Table.Cell>
|
||||
)
|
||||
const linkChangeFormat = (value, rowdata) => (
|
||||
<Table.Cell>
|
||||
<a href={rowdata.rowData.ref_url}>{value ? rowdata.rowData.change+','+rowdata.rowData.patchset : rowdata.rowData.newrev ? rowdata.rowData.newrev.substr(0, 7) : rowdata.rowData.branch}</a>
|
||||
</Table.Cell>
|
||||
)
|
||||
const durationFormat = (value) => (
|
||||
<Table.Cell>
|
||||
{moment.duration(value, 'seconds').format('h [hr] m [min] s [sec]')}
|
||||
</Table.Cell>
|
||||
)
|
||||
const timeFormat = (value) => (
|
||||
<Table.Cell>
|
||||
{moment.utc(value).tz(this.props.timezone).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</Table.Cell>
|
||||
)
|
||||
this.columns = []
|
||||
this.filterTypes = []
|
||||
const myColumns = [
|
||||
'job',
|
||||
'project',
|
||||
'branch',
|
||||
'pipeline',
|
||||
'change',
|
||||
'duration',
|
||||
'start time',
|
||||
'result']
|
||||
myColumns.forEach(column => {
|
||||
let prop = column
|
||||
let formatter = cellFormat
|
||||
// Adapt column name and property name
|
||||
if (column === 'job') {
|
||||
prop = 'job_name'
|
||||
} else if (column === 'start time') {
|
||||
prop = 'start_time'
|
||||
formatter = timeFormat
|
||||
} else if (column === 'change') {
|
||||
prop = 'change'
|
||||
formatter = linkChangeFormat
|
||||
} else if (column === 'result') {
|
||||
formatter = linkBuildFormat
|
||||
} else if (column === 'duration') {
|
||||
formatter = durationFormat
|
||||
}
|
||||
const label = column.charAt(0).toUpperCase() + column.slice(1)
|
||||
this.columns.push({
|
||||
header: {label: label, formatters: [headerFormat]},
|
||||
property: prop,
|
||||
cell: {formatters: [formatter]}
|
||||
})
|
||||
if (prop !== 'start_time' && prop !== 'ref_url' && prop !== 'duration'
|
||||
&& prop !== 'log_url' && prop !== 'uuid') {
|
||||
this.filterTypes.push({
|
||||
id: prop,
|
||||
title: label,
|
||||
placeholder: 'Filter by ' + label,
|
||||
filterType: 'text',
|
||||
})
|
||||
}
|
||||
})
|
||||
// Add build filter at the end
|
||||
this.filterTypes.push({
|
||||
id: 'uuid',
|
||||
title: 'Build',
|
||||
placeholder: 'Filter by Build UUID',
|
||||
filterType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
renderTable (builds) {
|
||||
return (
|
||||
<Table.PfProvider
|
||||
striped
|
||||
bordered
|
||||
columns={this.columns}
|
||||
>
|
||||
<Table.Header/>
|
||||
<Table.Body
|
||||
rows={builds}
|
||||
rowKey='uuid'
|
||||
onRow={(row) => {
|
||||
switch (row.result) {
|
||||
case 'SUCCESS':
|
||||
return { className: 'success' }
|
||||
default:
|
||||
return { className: 'warning' }
|
||||
}
|
||||
}} />
|
||||
</Table.PfProvider>)
|
||||
handleClearFilters = () => {
|
||||
// Delete the values for each filter category
|
||||
const filters = this.filterCategories.reduce((filterDict, category) => {
|
||||
filterDict[category.key] = []
|
||||
return filterDict
|
||||
}, {})
|
||||
this.handleFilterChange(filters)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { builds, filters } = this.state
|
||||
const { builds, fetching, filters } = this.state
|
||||
return (
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<FilterToolbar
|
||||
@@ -245,7 +159,11 @@ class BuildsPage extends React.Component {
|
||||
onFilterChange={this.handleFilterChange}
|
||||
filters={filters}
|
||||
/>
|
||||
{builds ? this.renderTable(builds) : <p>Loading...</p>}
|
||||
<BuildTable
|
||||
builds={builds}
|
||||
fetching={fetching}
|
||||
onClearFilters={this.handleClearFilters}
|
||||
/>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Table } from 'patternfly-react'
|
||||
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
|
||||
import { fetchBuildsets } from '../api'
|
||||
@@ -26,7 +24,7 @@ import {
|
||||
getFiltersFromUrl,
|
||||
writeFiltersToUrl,
|
||||
} from '../containers/FilterToolbar'
|
||||
|
||||
import BuildsetTable from '../containers/build/BuildsetTable'
|
||||
|
||||
class BuildsetsPage extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -35,9 +33,8 @@ class BuildsetsPage extends React.Component {
|
||||
history: PropTypes.object,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super()
|
||||
this.prepareTableHeaders()
|
||||
this.filterCategories = [
|
||||
{
|
||||
key: 'project',
|
||||
@@ -79,10 +76,10 @@ class BuildsetsPage extends React.Component {
|
||||
]
|
||||
|
||||
this.state = {
|
||||
buildsets: null,
|
||||
buildsets: [],
|
||||
fetching: false,
|
||||
filters: getFiltersFromUrl(props.location, this.filterCategories),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
updateData = (filters) => {
|
||||
@@ -93,20 +90,25 @@ class BuildsetsPage extends React.Component {
|
||||
// passed as prop and doesn't change since the page itself wasn't
|
||||
// re-rendered.
|
||||
const queryString = buildQueryString(filters)
|
||||
this.setState({buildsets: null})
|
||||
fetchBuildsets(this.props.tenant.apiPrefix, queryString).then(response => {
|
||||
this.setState({buildsets: response.data})
|
||||
})
|
||||
this.setState({ fetching: true })
|
||||
fetchBuildsets(this.props.tenant.apiPrefix, queryString).then(
|
||||
(response) => {
|
||||
this.setState({
|
||||
buildsets: response.data,
|
||||
fetching: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
document.title = 'Zuul Buildsets'
|
||||
if (this.props.tenant.name) {
|
||||
this.updateData(this.state.filters)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const { filters } = this.state
|
||||
if (this.props.tenant.name !== prevProps.tenant.name) {
|
||||
this.updateData(filters)
|
||||
@@ -127,93 +129,17 @@ class BuildsetsPage extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
prepareTableHeaders() {
|
||||
const headerFormat = value => <Table.Heading>{value}</Table.Heading>
|
||||
const cellFormat = (value) => <Table.Cell>{value}</Table.Cell>
|
||||
const linkChangeFormat = (value, rowdata) => (
|
||||
<Table.Cell>
|
||||
<a href={rowdata.rowData.ref_url}>
|
||||
{value ?
|
||||
rowdata.rowData.change + ',' + rowdata.rowData.patchset :
|
||||
rowdata.rowData.newrev ?
|
||||
rowdata.rowData.newrev.substr(0, 7) :
|
||||
rowdata.rowData.branch}
|
||||
</a>
|
||||
</Table.Cell>
|
||||
)
|
||||
const linkBuildsetFormat = (value, rowdata) => (
|
||||
<Table.Cell>
|
||||
<Link
|
||||
to={this.props.tenant.linkPrefix +
|
||||
'/buildset/' + rowdata.rowData.uuid}>
|
||||
{value}
|
||||
</Link>
|
||||
</Table.Cell>
|
||||
)
|
||||
this.columns = []
|
||||
this.filterTypes = []
|
||||
const myColumns = [
|
||||
'project',
|
||||
'branch',
|
||||
'pipeline',
|
||||
'change',
|
||||
'result']
|
||||
myColumns.forEach(column => {
|
||||
let prop = column
|
||||
let formatter = cellFormat
|
||||
if (column === 'change') {
|
||||
formatter = linkChangeFormat
|
||||
} else if (column === 'result') {
|
||||
formatter = linkBuildsetFormat
|
||||
}
|
||||
const label = column.charAt(0).toUpperCase() + column.slice(1)
|
||||
this.columns.push({
|
||||
header: {label: label, formatters: [headerFormat]},
|
||||
property: prop,
|
||||
cell: {formatters: [formatter]}
|
||||
})
|
||||
if (column !== 'builds') {
|
||||
this.filterTypes.push({
|
||||
id: prop,
|
||||
title: label,
|
||||
placeholder: 'Filter by ' + label,
|
||||
filterType: 'text',
|
||||
})
|
||||
}
|
||||
})
|
||||
// Add buildset filter at the end
|
||||
this.filterTypes.push({
|
||||
id: 'uuid',
|
||||
title: 'Buildset',
|
||||
placeholder: 'Filter by Buildset UUID',
|
||||
filterType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
renderTable (buildsets) {
|
||||
return (
|
||||
<Table.PfProvider
|
||||
striped
|
||||
bordered
|
||||
columns={this.columns}
|
||||
>
|
||||
<Table.Header/>
|
||||
<Table.Body
|
||||
rows={buildsets}
|
||||
rowKey='uuid'
|
||||
onRow={(row) => {
|
||||
switch (row.result) {
|
||||
case 'SUCCESS':
|
||||
return { className: 'success' }
|
||||
default:
|
||||
return { className: 'warning' }
|
||||
}
|
||||
}} />
|
||||
</Table.PfProvider>)
|
||||
handleClearFilters = () => {
|
||||
// Delete the values for each filter category
|
||||
const filters = this.filterCategories.reduce((filterDict, category) => {
|
||||
filterDict[category.key] = []
|
||||
return filterDict
|
||||
}, {})
|
||||
this.handleFilterChange(filters)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { buildsets, filters } = this.state
|
||||
const { buildsets, fetching, filters } = this.state
|
||||
return (
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<FilterToolbar
|
||||
@@ -221,10 +147,14 @@ class BuildsetsPage extends React.Component {
|
||||
onFilterChange={this.handleFilterChange}
|
||||
filters={filters}
|
||||
/>
|
||||
{buildsets ? this.renderTable(buildsets) : <p>Loading...</p>}
|
||||
<BuildsetTable
|
||||
buildsets={buildsets}
|
||||
fetching={fetching}
|
||||
onClearFilters={this.handleClearFilters}
|
||||
/>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({tenant: state.tenant}))(BuildsetsPage)
|
||||
export default connect((state) => ({ tenant: state.tenant }))(BuildsetsPage)
|
||||
|
||||
@@ -1510,33 +1510,51 @@
|
||||
dependencies:
|
||||
"@types/node" ">= 8"
|
||||
|
||||
"@patternfly/react-core@^4.18.5":
|
||||
version "4.18.5"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.18.5.tgz#465ee3be0e58f7fdead9644ed2667f18eff0d684"
|
||||
integrity sha512-wUHLXPOklcAVA9nCnmUvGwdfTZnypxNUnA0l+eEiq1QWhQoSRdI7S/HNOelYhpRjMMwPwy3yMsJUjHsXdqv2FQ==
|
||||
"@patternfly/patternfly@4.31.6":
|
||||
version "4.31.6"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.31.6.tgz#ef9919df610171760cd19920a904ca9b09a74593"
|
||||
integrity sha512-gp8tpbE4Z6C1PIQwNiWMjO5XSr/UGjXs4InL/zmxgZbToyizUxsudwJyCObtdvDNoN57ZJp0gYWYy0tIuwEyMA==
|
||||
|
||||
"@patternfly/react-core@^4.40.4":
|
||||
version "4.40.4"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.40.4.tgz#e4409f89327e2fcdcd07a08833c0149e6f2f6966"
|
||||
integrity sha512-NQuUgIVEty7BBNJMJAVRXejOGRGpRQwgQ8Rw/J/JlgkhtOrCSFX5cEbpAXMXLYWkJrz0++XfRK/FQMoQbvS2hQ==
|
||||
dependencies:
|
||||
"@patternfly/react-icons" "^4.3.5"
|
||||
"@patternfly/react-styles" "^4.3.4"
|
||||
"@patternfly/react-tokens" "^4.4.4"
|
||||
"@patternfly/react-icons" "^4.7.2"
|
||||
"@patternfly/react-styles" "^4.7.2"
|
||||
"@patternfly/react-tokens" "^4.9.4"
|
||||
focus-trap "4.0.2"
|
||||
react-dropzone "9.0.0"
|
||||
tippy.js "5.1.2"
|
||||
tslib "^1.11.1"
|
||||
|
||||
"@patternfly/react-icons@^4.3.5":
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.3.5.tgz#b98c5af80f4729e6203c8e799ace2f57308b3b9a"
|
||||
integrity sha512-+GublxpFXR+y/5zygf9q00/LvIvso8jr0mxZGhVxsKmi2dUu7xAvN+T+5vjS9fiMbXf7WXsSPXST/UTiBIVTdQ==
|
||||
"@patternfly/react-icons@^4.7.2":
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.7.2.tgz#f4ad252cb5682bd95da474ce9ce6ddf7fb3a1ac1"
|
||||
integrity sha512-r1yCVHxUtRSblo8VwfaUM0d49z4eToZXAI0VzOnfKPRgSmGZrn6l8soQgDDtyQsSDr534Qvm55y/qLrlR9JCnw==
|
||||
|
||||
"@patternfly/react-styles@^4.3.4":
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.3.4.tgz#ba62f983f1bd2586d2878d8a38912442ebca85e8"
|
||||
integrity sha512-d5W5G9g7sr7DthGPFiF6Oa33w8JFJ+ocLZDogyZcS1Oq0BJJX8j+hZNXZfhXxmHoXufxQL6RJ4dOyoa2zEZUvA==
|
||||
"@patternfly/react-styles@^4.7.2":
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.7.2.tgz#6671a243401ef55adddcb0e0922f5f5f4eea840e"
|
||||
integrity sha512-r3zyrt1mXcqdXaEq+otl1cGsN0Ou1k8uIJSY+4EGe2A5jLGbX3vBTwUrpPKLB6tUdNL+mZriFf+3oKhWbVZDkw==
|
||||
|
||||
"@patternfly/react-tokens@^4.4.4":
|
||||
version "4.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.4.4.tgz#4a6fc7234908343087ccbc8e1ae53336d7fc7311"
|
||||
integrity sha512-vhDBtwkp1PTAqXDjAsUPRf/ewBh2Asong8MPr9ZGeXAeOULW8creW7GJx+JZX9eaEJMA3ESMeZ6wZ5j/yyWMGQ==
|
||||
"@patternfly/react-table@^4.15.5":
|
||||
version "4.15.5"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-4.15.5.tgz#7fc3fcd37a6fd4dca00cc32d24c76199ee41a7f1"
|
||||
integrity sha512-GlyKrEDMY+yLvczj5rWpNKcUp90Ib7alKV9JK8rVLOpTsukQ0QplXxYFsnIrombcaw2V54XVdflZGjsB0GoHEw==
|
||||
dependencies:
|
||||
"@patternfly/patternfly" "4.31.6"
|
||||
"@patternfly/react-core" "^4.40.4"
|
||||
"@patternfly/react-icons" "^4.7.2"
|
||||
"@patternfly/react-styles" "^4.7.2"
|
||||
"@patternfly/react-tokens" "^4.9.4"
|
||||
lodash "^4.17.19"
|
||||
tslib "^1.11.1"
|
||||
|
||||
"@patternfly/react-tokens@^4.9.4":
|
||||
version "4.9.4"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.9.4.tgz#71ea3c33045fb29bcc8d98f2c0f07bfcdc89a12c"
|
||||
integrity sha512-AJpcAvzWXvfThT2mx24rV7OJSHvZnIsOP1bVrXiubpFAJhi/Suq+LGe/lTPUnuSXaflwyDBRZDXWWmJb4yaWqg==
|
||||
|
||||
"@semantic-release/commit-analyzer@^6.1.0":
|
||||
version "6.3.3"
|
||||
@@ -8896,6 +8914,11 @@ lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.17.10, lodash@^4.17.11, lodash@^4.1
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@^4.17.19:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||
|
||||
loglevel@^1.6.6:
|
||||
version "1.6.8"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
|
||||
|
||||
Reference in New Issue
Block a user