// Copyright 2018 Red Hat, Inc // // 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 * as React from 'react' import { isEqual } from 'lodash' import PropTypes from 'prop-types' import { connect } from 'react-redux' import 'moment-duration-format' import { PageSection, PageSectionVariants, Pagination } from '@patternfly/react-core' import { fetchBuilds } from '../api' import { buildQueryString, FilterToolbar, getFiltersFromUrl, writeFiltersToUrl, } from '../containers/FilterToolbar' import BuildTable from '../containers/build/BuildTable' class BuildsPage extends React.Component { static propTypes = { tenant: PropTypes.object, timezone: PropTypes.string, location: PropTypes.object, history: PropTypes.object, } constructor(props) { super() 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', }, { key: 'result', title: 'Result', placeholder: 'Any result', type: 'select', // TODO there should be a single source of truth for this options: [ 'SUCCESS', 'FAILURE', 'RETRY_LIMIT', 'POST_FAILURE', 'SKIPPED', 'NODE_FAILURE', 'MERGE_CONFLICT', 'MERGE_FAILURE', 'CONFIG_ERROR', 'TIMED_OUT', 'CANCELED', 'ERROR', 'RETRY', 'DISK_FULL', 'NO_JOBS', 'DISCONNECT', 'ABORTED', 'LOST', 'EXCEPTION', 'NO_HANDLE'], }, { key: 'uuid', title: 'Build', placeholder: 'Filter by Build UUID...', type: 'search', }, { key: 'held', title: 'Held', placeholder: 'Choose Hold Status...', type: 'ternary', options: [ 'All', 'Held Builds Only', 'Non Held Builds Only', ] }, { key: 'voting', title: 'Voting', placeholder: 'Choose Voting Status...', type: 'ternary', options: [ 'All', 'Voting Only', 'Non-Voting Only', ] }, ] const _filters = getFiltersFromUrl(props.location, this.filterCategories) const perPage = _filters.limit[0] ? parseInt(_filters.limit[0]) : 50 const currentPage = _filters.skip[0] ? Math.floor(parseInt(_filters.skip[0] / perPage)) + 1 : 1 this.state = { builds: [], fetching: false, filters: _filters, resultsPerPage: perPage, currentPage: currentPage, itemCount: null, } } updateData = (filters) => { // 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 { itemCount } = this.state let paginationOptions = { skip: filters.skip.length > 0 ? filters.skip : [0,], limit: filters.limit.length > 0 ? filters.limit : [50,] } let _filters = { ...filters, ...paginationOptions } const queryString = buildQueryString(_filters, true) 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) => { // if we have already an itemCount for this query (ie we're scrolling backwards through results) // keep this value. Otherwise, check if we've got all the results. let finalItemCount = itemCount ? itemCount : (response.data.length < paginationOptions.limit[0] ? parseInt(paginationOptions.skip[0]) + response.data.length : null) this.setState({ builds: response.data, fetching: false, itemCount: finalItemCount, }) }) } componentDidMount() { document.title = 'Zuul Builds' if (this.props.tenant.name) { this.updateData(this.state.filters) } } componentDidUpdate(prevProps) { const { filters } = this.state if ( this.props.tenant.name !== prevProps.tenant.name || this.props.timezone !== prevProps.timezone ) { this.updateData(filters) } } filterInputValidation = (filterKey, filterValue) => { // Input value should not be empty for all cases if (!filterValue) { return { success: false, message: 'Input should not be empty' } } // For change filter, it must be an integer if (filterKey === 'change' && isNaN(filterValue)) { return { success: false, message: 'Change must be an integer (do not include revision)' } } return { success: true } } handleFilterChange = (newFilters) => { const { location, history } = this.props const { filters, itemCount } = this.state /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ let { 'skip': x1, 'limit': y1, ..._oldFilters } = filters let { 'skip': x2, 'limit': y2, ..._newFilters } = newFilters // If filters have changed, reinitialize skip let equalTest = isEqual(_oldFilters, _newFilters) let finalFilters = equalTest ? newFilters : { ...newFilters, skip: [0,] } // 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(finalFilters, location, history) let newState = { filters: finalFilters, // if filters haven't changed besides skip or limit, keep our itemCount and currentPage itemCount: equalTest ? itemCount : null, } if (!equalTest) { newState.currentPage = 1 } this.setState( newState, () => { this.updateData(finalFilters) }) } handleClearFilters = () => { // Delete the values for each filter category const filters = this.filterCategories.reduce((filterDict, category) => { filterDict[category.key] = [] return filterDict }, {}) this.handleFilterChange(filters) } handlePerPageSelect = (event, perPage) => { const { filters } = this.state this.setState({ resultsPerPage: perPage }) const newFilters = { ...filters, limit: [perPage,] } this.handleFilterChange(newFilters) } handleSetPage = (event, pageNumber) => { const { filters, resultsPerPage } = this.state this.setState({ currentPage: pageNumber }) let offset = resultsPerPage * (pageNumber - 1) const newFilters = { ...filters, skip: [offset,] } this.handleFilterChange(newFilters) } render() { const { history } = this.props const { builds, fetching, filters, resultsPerPage, currentPage, itemCount } = this.state return ( ( {firstIndex} - {lastIndex}   of   {itemCount ? itemCount : 'many'} )} itemCount={itemCount} perPage={resultsPerPage} page={currentPage} widgetId="pagination-menu" onPerPageSelect={this.handlePerPageSelect} onSetPage={this.handleSetPage} isCompact /> ) } } export default connect((state) => ({ tenant: state.tenant, timezone: state.timezone, }))(BuildsPage)