summaryrefslogtreecommitdiff
path: root/web/src/pages/Builds.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/pages/Builds.jsx')
-rw-r--r--web/src/pages/Builds.jsx241
1 files changed, 118 insertions, 123 deletions
diff --git a/web/src/pages/Builds.jsx b/web/src/pages/Builds.jsx
index 6777c3b76..144f9ab85 100644
--- a/web/src/pages/Builds.jsx
+++ b/web/src/pages/Builds.jsx
@@ -15,166 +15,161 @@
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'
import { fetchBuilds } from '../api'
-import TableFilters from '../containers/TableFilters'
+import {
+ buildQueryString,
+ FilterToolbar,
+ getFiltersFromUrl,
+ writeFiltersToUrl,
+} from '../containers/FilterToolbar'
+import BuildTable from '../containers/build/BuildTable'
-
-class BuildsPage extends TableFilters {
+class BuildsPage extends React.Component {
static propTypes = {
tenant: PropTypes.object,
- timezone: PropTypes.string
+ timezone: PropTypes.string,
+ location: PropTypes.object,
+ history: PropTypes.object,
}
- constructor () {
+ constructor(props) {
super()
- this.prepareTableHeaders()
+ this.filterCategories = [
+ {
+ key: 'job_name',
+ title: 'Job',
+ placeholder: 'Filter by Job...',
+ type: 'search',
+ },
+ {
+ key: 'project',
+ title: 'Project',
+ placeholder: 'Filter by Project...',
+ type: 'search',
+ },
+ {
+ key: 'branch',
+ title: 'Branch',
+ placeholder: 'Filter by Branch...',
+ type: 'search',
+ },
+ {
+ key: 'pipeline',
+ title: 'Pipeline',
+ placeholder: 'Filter by Pipeline...',
+ type: 'search',
+ },
+ {
+ key: 'change',
+ title: 'Change',
+ placeholder: 'Filter by Change...',
+ type: 'search',
+ },
+ // TODO (felix): We could change the result filter to a dropdown later on
+ {
+ key: 'result',
+ title: 'Result',
+ placeholder: 'Filter by Result...',
+ type: 'search',
+ },
+ {
+ key: 'uuid',
+ title: 'Build',
+ placeholder: 'Filter by Build UUID...',
+ type: 'search',
+ },
+ ]
+
this.state = {
- builds: null,
- currentFilterType: this.filterTypes[0],
- activeFilters: [],
- currentValue: ''
+ builds: [],
+ fetching: false,
+ filters: getFiltersFromUrl(props.location, this.filterCategories),
}
}
updateData = (filters) => {
- let queryString = ''
- if (filters) {
- filters.forEach(item => queryString += '&' + item.key + '=' + item.value)
- }
- this.setState({builds: null})
- fetchBuilds(this.props.tenant.apiPrefix, queryString).then(response => {
- this.setState({builds: response.data})
+ // When building the filter query for the API we can't rely on the location
+ // search parameters. Although, we've updated them in theu URL directly
+ // they always have the same value in here (the values when the page was
+ // first loaded). Most probably that's the case because the location is
+ // passed as prop and doesn't change since the page itself wasn't
+ // re-rendered.
+ const queryString = buildQueryString(filters)
+ 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,
+ })
})
}
- componentDidMount () {
+ componentDidMount() {
document.title = 'Zuul Builds'
if (this.props.tenant.name) {
- this.updateData(this.getFilterFromUrl())
+ this.updateData(this.state.filters)
}
}
- componentDidUpdate (prevProps) {
- if (this.props.tenant.name !== prevProps.tenant.name ||
- this.props.timezone !== prevProps.timezone) {
- this.updateData(this.getFilterFromUrl())
+ componentDidUpdate(prevProps) {
+ const { filters } = this.state
+ if (
+ this.props.tenant.name !== prevProps.tenant.name ||
+ this.props.timezone !== prevProps.timezone
+ ) {
+ this.updateData(filters)
}
}
- 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',
- })
+ handleFilterChange = (filters) => {
+ const { location, history } = this.props
+ // We must update the URL parameters before the state. Otherwise, the URL
+ // will always be one filter selection behind the state. But as the URL
+ // reflects our state this should be ok.
+ writeFiltersToUrl(filters, location, history)
+ this.updateData(filters)
+ this.setState(() => {
+ return {
+ filters: filters,
}
})
- // 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 } = this.state
+ const { builds, fetching, filters } = this.state
return (
<PageSection variant={PageSectionVariants.light}>
- {this.renderFilter()}
- {builds ? this.renderTable(builds) : <p>Loading...</p>}
+ <FilterToolbar
+ filterCategories={this.filterCategories}
+ onFilterChange={this.handleFilterChange}
+ filters={filters}
+ />
+ <BuildTable
+ builds={builds}
+ fetching={fetching}
+ onClearFilters={this.handleClearFilters}
+ />
</PageSection>
)
}
}
-export default connect(state => ({tenant: state.tenant, timezone: state.timezone}))(BuildsPage)
+export default connect((state) => ({
+ tenant: state.tenant,
+ timezone: state.timezone,
+}))(BuildsPage)