ui/perfherder/compare/CompareTableControls.jsx (412 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { Container, Row } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from 'reactstrap/lib/Button'; import { faFileDownload } from '@fortawesome/free-solid-svg-icons'; import { filterText } from '../perf-helpers/constants'; import { convertParams, containsText, onPermalinkClick, retriggerMultipleJobs, } from '../perf-helpers/helpers'; import { parseQueryParams } from '../../helpers/url'; import PaginationGroup from '../shared/Pagination'; import FilterControls from '../../shared/FilterControls'; import CompareTable from './CompareTable'; import RetriggerModal from './RetriggerModal'; export default class CompareTableControls extends React.Component { constructor(props) { super(props); this.validated = this.props.validated; this.state = { hideUncomparable: convertParams(this.validated, 'showOnlyComparable'), showImportant: convertParams(this.validated, 'showOnlyImportant'), hideUncertain: convertParams(this.validated, 'showOnlyConfident'), showNoise: convertParams(this.validated, 'showOnlyNoise'), useReplicates: convertParams(this.validated, 'replicates'), results: new Map(), filteredText: this.getDefaultFilterText(this.validated), showRetriggerModal: false, currentRetriggerRow: {}, totalPagesList: 0, page: this.validated.page ? parseInt(this.validated.page, 10) : 1, countPages: 0, }; } componentDidMount() { this.updateFilteredResults(); } componentDidUpdate(prevProps, prevState) { const { compareResults, location } = this.props; const { countPages } = this.state; const params = parseQueryParams(location.search); const prevParams = parseQueryParams(prevProps.location.search); if (params.replicates !== prevParams.replicates) { window.location.reload(false); } if (prevState.countPages !== countPages) { this.setState({ totalPagesList: this.generatePages(countPages) }); } if (prevProps.compareResults !== compareResults) { this.updateFilteredResults(); } if (params.page && params.page !== prevParams.page) { this.setState({ page: parseInt(params.page, 10) }, () => { this.updateFilteredResults(); }); } } getDefaultFilterText = (validated) => { const { filter } = validated; return filter === undefined || filter === null ? '' : filter; }; updateFilterText = (filterText) => { this.setState({ filteredText: filterText, page: 1 }, () => this.updateFilteredResults(), ); }; updateFilter = (filter) => { this.setState( (prevState) => ({ [filter]: !prevState[filter], page: 1 }), () => this.updateFilteredResults(), ); }; updateReplicates = () => { this.setState( (prevState) => ({ useReplicates: !prevState.useReplicates }), () => this.updateUrlParams(), ); }; filterResult = (testName, result) => { const { filteredText, showImportant, hideUncertain, showNoise, hideUncomparable, } = this.state; const matchesFilters = (!showImportant || result.isMeaningful) && (!hideUncomparable || 'newIsBetter' in result) && (!hideUncertain || result.isConfident) && (!showNoise || result.isNoiseMetric); if (!filteredText) return matchesFilters; const textToSearch = `${testName} ${result.name}`; // searching with filter input and one or more metricFilter buttons on // will produce different results compared to when all filters are off return containsText(textToSearch, filteredText) && matchesFilters; }; updateFilteredResults = () => { const { filteredText, hideUncomparable, showImportant, hideUncertain, showNoise, page, } = this.state; const { compareResults } = this.props; let results; const toEnd = page * 10; const fromStart = toEnd - 10; let countPages = Math.ceil(compareResults.size / 10); let totalPagesList = this.generatePages(countPages); this.updateUrlParams(); if ( !filteredText && !hideUncomparable && !showImportant && !hideUncertain && !showNoise ) { results = Array.from(compareResults).slice(fromStart, toEnd); results = new Map(results); return this.setState({ results, countPages, totalPagesList }); } const filteredResults = new Map(compareResults); for (const [testName, values] of filteredResults) { const filteredValues = values.filter((result) => this.filterResult(testName, result), ); if (filteredValues.length) { filteredResults.set(testName, filteredValues); } else { filteredResults.delete(testName); } } countPages = Math.ceil(filteredResults.size / 10); totalPagesList = this.generatePages(countPages); results = Array.from(filteredResults).slice(fromStart, toEnd); results = new Map(results); this.setState({ results, countPages, totalPagesList }); }; toggleRetriggerModal = () => { this.setState((prevState) => ({ showRetriggerModal: !prevState.showRetriggerModal, })); }; updateAndClose = async (event, params) => { const { currentRetriggerRow } = this.state; const { baseRetriggerTimes, newRetriggerTimes } = params; event.preventDefault(); await retriggerMultipleJobs( currentRetriggerRow, baseRetriggerTimes, newRetriggerTimes, this.props, ); this.toggleRetriggerModal(); }; updateUrlParams = () => { const { updateParams } = this.props.validated; const { filteredText, hideUncomparable, showImportant, hideUncertain, showNoise, useReplicates, page, } = this.state; const compareURLParams = {}; const paramsToRemove = []; if (filteredText !== '') compareURLParams.filter = filteredText; else paramsToRemove.push('filter'); if (hideUncomparable) compareURLParams.showOnlyComparable = 1; else paramsToRemove.push('showOnlyComparable'); if (showImportant) compareURLParams.showOnlyImportant = 1; else paramsToRemove.push('showOnlyImportant'); if (hideUncertain) compareURLParams.showOnlyConfident = 1; else paramsToRemove.push('showOnlyConfident'); if (showNoise) compareURLParams.showOnlyNoise = 1; else paramsToRemove.push('showOnlyNoise'); if (useReplicates) compareURLParams.replicates = 1; else paramsToRemove.push('replicates'); compareURLParams.page = page; updateParams(compareURLParams, paramsToRemove); }; onModalOpen = (rowResults) => { this.setState({ currentRetriggerRow: rowResults }); this.toggleRetriggerModal(); }; getCurrentPages = () => { const { page, totalPagesList } = this.state; if (totalPagesList.length <= 5 || !totalPagesList.length) { return totalPagesList; } if (page + 4 > totalPagesList.length) { return totalPagesList.slice(-5); } return totalPagesList.slice(page - 1, page + 4); }; generatePages = (countPages) => { const pages = []; for (let num = 1; num <= countPages; num++) { pages.push(num); } return pages; }; formatDownloadData = (data) => { const mapped = Array.from(data).map((info) => info); const newStructure = mapped.map((test) => { return { [test[0]]: test[1].map((entry) => { const { links, ...newEntry } = entry; return newEntry; }), }; }); return newStructure; }; render() { const { frameworkName, showTestsWithNoise, dropdownOptions, user, isBaseAggregate, notify, hasSubtests, onPermalinkClick, projects, history, validated, compareResults, } = this.props; const { hideUncomparable, hideUncertain, showImportant, showNoise, useReplicates, results, showRetriggerModal, currentRetriggerRow, filteredText, countPages, page, } = this.state; const compareFilters = [ { tooltipText: 'At least 1 result for old and new revision', text: filterText.hideUncomparable, state: hideUncomparable, stateName: 'hideUncomparable', }, { tooltipText: 'Non-trivial changes (2%+)', text: filterText.showImportant, state: showImportant, stateName: 'showImportant', }, { tooltipText: 'At least 6 datapoints OR 2+ datapoints and a large difference', text: filterText.hideUncertain, state: hideUncertain, stateName: 'hideUncertain', }, { tooltipText: 'Display Noise Metric to compare noisy tests at a platform level', text: filterText.showNoise, state: showNoise, stateName: 'showNoise', }, ]; const viewablePagesList = this.getCurrentPages(); const hasMorePages = () => viewablePagesList.length > 0 && countPages !== 1; const hasTryProject = () => validated.newProject === 'try' || validated.originalProject === 'try'; const formattedJSONData = this.formatDownloadData(compareResults); return ( <Container fluid className="my-3 px-0"> <RetriggerModal showModal={showRetriggerModal} toggle={this.toggleRetriggerModal} updateAndClose={this.updateAndClose} currentRetriggerRow={currentRetriggerRow} isBaseAggregate={isBaseAggregate} /> <FilterControls filteredTextValue={filteredText} filterOptions={compareFilters} updateFilter={this.updateFilter} updateFilterText={this.updateFilterText} dropdownOptions={dropdownOptions} /> <div /> {viewablePagesList ? hasMorePages() && ( <Row className="justify-content-center"> <PaginationGroup viewablePageNums={viewablePagesList} updateParams={validated.updateParams} currentPage={page} count={countPages} /> </Row> ) : null} <div className="download-json-container"> {hasTryProject() && ( <Button color="darker-info" outline className="btn" type="button" onClick={this.updateReplicates} active={useReplicates} > Use replicates (try-only) </Button> )} {formattedJSONData.length > 0 && ( <Button className="btn download-button" type="button" href={`data:text/json;charset=utf-8,${encodeURIComponent( JSON.stringify(formattedJSONData), )}`} download="perf-compare.json" data-testid="download-button" > <FontAwesomeIcon icon={faFileDownload} className="download-json-icon" /> JSON (experimental) </Button> )} </div> {showNoise && showTestsWithNoise} {results.size > 0 ? ( Array.from(results).map(([testName, data]) => ( <CompareTable key={testName} data={data} testName={testName} frameworkName={frameworkName} onPermalinkClick={onPermalinkClick} user={user} isBaseAggregate={isBaseAggregate} notify={notify} hasSubtests={hasSubtests} projects={projects} history={history} onModalOpen={this.onModalOpen} /> )) ) : ( <p className="lead text-center">No results to show</p> )} {viewablePagesList ? hasMorePages() && ( <Row className="justify-content-center bottom-pagination-container"> <PaginationGroup viewablePageNums={viewablePagesList} updateParams={validated.updateParams} currentPage={page} count={countPages} /> </Row> ) : null} </Container> ); } } CompareTableControls.propTypes = { compareResults: PropTypes.shape({}).isRequired, dropdownOptions: PropTypes.arrayOf(PropTypes.shape({})), user: PropTypes.shape({}).isRequired, isBaseAggregate: PropTypes.bool.isRequired, notify: PropTypes.func.isRequired, projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired, hasSubtests: PropTypes.bool, validated: PropTypes.shape({ showOnlyImportant: PropTypes.string, showOnlyComparable: PropTypes.string, showOnlyConfident: PropTypes.string, showOnlyNoise: PropTypes.string, }), showTestsWithNoise: PropTypes.oneOfType([ PropTypes.shape({}), PropTypes.bool, ]), onPermalinkClick: PropTypes.func, }; CompareTableControls.defaultProps = { dropdownOptions: [], hasSubtests: false, validated: { showOnlyImportant: undefined, showOnlyComparable: undefined, showOnlyConfident: undefined, showOnlyNoise: undefined, }, showTestsWithNoise: null, onPermalinkClick, };