ui/perfherder/compare/CompareTableView.jsx (415 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { Alert, Col, Row, Container } from 'reactstrap'; import { Link } from 'react-router-dom'; import ErrorMessages from '../../shared/ErrorMessages'; import { genericErrorMessage, errorMessageClass, } from '../../helpers/constants'; import { compareDefaultTimeRange, endpoints, phTimeRanges, } from '../perf-helpers/constants'; import ErrorBoundary from '../../shared/ErrorBoundary'; import { getData } from '../../helpers/http'; import { createApiUrl, createQueryParams, getPerfCompareBaseURL, getPerfCompareOvertimeURL, getPerfCompareBaseSubtestsURL, getPerfCompareOvertimeSubtestsURL, } from '../../helpers/url'; import { getFrameworkData, scrollWithOffset, getResultsMap, } from '../perf-helpers/helpers'; import TruncatedText from '../../shared/TruncatedText'; import LoadingSpinner from '../../shared/LoadingSpinner'; import ComparePageTitle from '../../shared/ComparePageTitle'; import RevisionInformation from '../../shared/RevisionInformation'; import CompareTableControls from './CompareTableControls'; import NoiseTable from './NoiseTable'; export default class CompareTableView extends React.Component { constructor(props) { super(props); this.state = { compareResults: new Map(), testsNoResults: null, testsWithNoise: [], failureMessages: [], loading: false, timeRange: this.setTimeRange(), framework: getFrameworkData(this.props), title: '', tabTitle: null, }; } componentDidMount() { const { compareData, location } = this.props; if ( compareData && compareData.size > 0 && location.pathname === '/compare' ) { this.setState({ compareResults: compareData }); } else { this.getPerformanceData(); } if (location.hash) { setTimeout(() => { const el = document.querySelector(location.hash); if (el) scrollWithOffset(el); }, 1500); } } componentDidUpdate(prevProps) { const { validated } = this.props; if ( validated.originalProject !== prevProps.validated.originalProject || validated.originalRevision !== prevProps.validated.originalRevision || validated.newProject !== prevProps.validated.newProject || validated.newRevision !== prevProps.validated.newRevision ) { this.getPerformanceData(); } } setTimeRange = () => { const { selectedTimeRange, originalRevision } = this.props.validated; if (originalRevision) { return null; } let timeRange; if (selectedTimeRange) { timeRange = phTimeRanges.find( (timeRange) => timeRange.value === parseInt(selectedTimeRange, 10), ); } return timeRange || compareDefaultTimeRange; }; getPerformanceData = async () => { const { getQueryParams, hasSubtests, getDisplayResults } = this.props; const { originalProject, originalRevision, newProject, newRevision, } = this.props.validated; const { framework, timeRange, failureMessages } = this.state; this.setState({ loading: true }); const [originalParams, newParams] = getQueryParams(timeRange, framework); const [originalResults, newResults] = await Promise.all([ getData(createApiUrl(endpoints.summary, originalParams)), getData(createApiUrl(endpoints.summary, newParams)), ]); if (originalResults.failureStatus) { return this.setState({ failureMessages: [originalResults.data, ...failureMessages], loading: false, }); } if (newResults.failureStatus) { return this.setState({ failureMessages: [newResults.data, ...failureMessages], loading: false, }); } const data = [...originalResults.data, ...newResults.data]; let rowNames; let tableNames; let title; if (!data.length) { return this.setState({ loading: false }); } const { testNames: origTestNames, names: origNames, platforms: origPlatforms, resultsMap: origResultsMap, } = getResultsMap(originalResults.data); const { testNames: newTestNames, names: newNames, platforms: newPlatforms, resultsMap: newResultsMap, } = getResultsMap(newResults.data); if (hasSubtests) { let subtestName = data[0].name.split(' '); subtestName.splice(1, 1); subtestName = subtestName.join(' '); title = `${data[0].platform}: ${subtestName}`; tableNames = [subtestName]; rowNames = [...new Set([...origTestNames, ...newTestNames])].sort(); } else { tableNames = [...new Set([...origNames, ...newNames])].sort(); rowNames = [...new Set([...origPlatforms, ...newPlatforms])].sort(); } const text = originalRevision ? `${originalRevision} (${originalProject})` : originalProject; this.setState({ tabTitle: title || `Comparison between ${text} and ${newRevision} (${newProject})`, }); const updates = getDisplayResults(origResultsMap, newResultsMap, { ...this.state, ...{ tableNames, rowNames }, }); updates.title = title; return this.setState(updates); }; updateFramework = (selection) => { const { updateParams } = this.props.validated; const { frameworks } = this.props; const framework = frameworks.find((item) => item.name === selection); updateParams({ framework: framework.id, page: 1 }); this.setState({ framework }, () => this.getPerformanceData()); }; updateTimeRange = (selection) => { const { updateParams } = this.props.validated; const timeRange = phTimeRanges.find((item) => item.text === selection); updateParams({ selectedTimeRange: timeRange.value, page: 1 }); this.setState({ timeRange }, () => this.getPerformanceData()); }; notifyFailure = (message, severity) => { const { failureMessages } = this.state; if (severity === 'danger') { this.setState({ failureMessages: [message, ...failureMessages], }); } }; render() { const { originalProject, newProject, originalRevision, newRevision, originalResultSet, newResultSet, pageTitle, originalSignature, newSignature, } = this.props.validated; const { filterByFramework, hasSubtests, frameworks, projects } = this.props; const { compareResults, loading, failureMessages, testsWithNoise, timeRange, testsNoResults, title, framework, tabTitle, } = this.state; const frameworkNames = frameworks && frameworks.length ? frameworks.map((item) => item.name) : []; const compareDropdowns = []; const params = { originalProject, newProject, newRevision, framework: framework.id, }; if (originalRevision) { params.originalRevision = originalRevision; } else if (timeRange) { params.selectedTimeRange = timeRange.value; } if (filterByFramework) { compareDropdowns.push({ options: frameworkNames, selectedItem: framework.name, updateData: (framework) => this.updateFramework(framework), }); } if (!originalRevision) { compareDropdowns.push({ options: phTimeRanges.map((option) => option.text), selectedItem: timeRange.text, updateData: (timeRange) => this.updateTimeRange(timeRange), }); } let perfCompareURL; if (originalRevision) { // compare with base url perfCompareURL = hasSubtests ? getPerfCompareBaseSubtestsURL( originalProject, originalRevision, newProject, newRevision, framework.id, originalSignature, newSignature, ) : getPerfCompareBaseURL( originalProject, originalRevision, newProject, newRevision, framework.id, ); } else if (timeRange) { // compareOverTime URL perfCompareURL = hasSubtests ? getPerfCompareOvertimeSubtestsURL( originalProject, newProject, newRevision, framework.id, timeRange.value, originalSignature, newSignature, ) : getPerfCompareOvertimeURL( originalProject, newProject, newRevision, framework.id, timeRange.value, ); } return ( <Container fluid className="max-width-default"> {loading && !failureMessages.length && <LoadingSpinner />} <ErrorBoundary errorClasses={errorMessageClass} message={genericErrorMessage} > <React.Fragment> <Row className="justify-content-center"> <Alert color="info"> Try out the same comparison{' '} <a href={perfCompareURL} target="_blank" rel="noopener noreferrer" > with our new PerfCompare tool </a> ! </Alert> </Row> {hasSubtests && ( <Link to={{ pathname: './compare', search: createQueryParams(params), }} > Back to all tests and platforms </Link> )} <div className="mx-auto"> <Row className="justify-content-center"> <Col sm="8" className="text-center"> {failureMessages.length !== 0 && ( <ErrorMessages errorMessages={failureMessages} /> )} </Col> </Row> {newRevision && newProject && (originalRevision || timeRange) && ( <Row> <Col sm="12" className="text-center pb-1"> <h1> <ComparePageTitle title={ hasSubtests ? `${title} subtest summary` : 'Perfherder Compare Revisions' } pageTitleQueryParam={pageTitle} defaultPageTitle={tabTitle} /> </h1> <RevisionInformation originalProject={originalProject} originalRevision={originalRevision} originalResultSet={originalResultSet} newProject={newProject} newRevision={newRevision} newResultSet={newResultSet} selectedTimeRange={timeRange} /> </Col> </Row> )} {testsNoResults && ( <Row className="pt-5 justify-content-center"> <Col small="12" className="px-0 max-width-default"> <Alert color="warning"> <TruncatedText title="Tests without results: " maxLength={174} text={testsNoResults} /> </Alert> </Col> </Row> )} <CompareTableControls {...this.props} frameworkName={framework.name} dropdownOptions={compareDropdowns} updateState={(state) => this.setState(state)} compareResults={compareResults} isBaseAggregate={!originalRevision} notify={this.notifyFailure} projects={projects} showTestsWithNoise={ testsWithNoise.length > 0 && ( <Row> <Col sm="12" className="text-center"> <NoiseTable testsWithNoise={testsWithNoise} hasSubtests={hasSubtests} /> </Col> </Row> ) } /> </div> </React.Fragment> </ErrorBoundary> </Container> ); } } CompareTableView.propTypes = { validated: PropTypes.shape({ originalResultSet: PropTypes.shape({}), newResultSet: PropTypes.shape({}), newRevision: PropTypes.string, originalProject: PropTypes.string, newProject: PropTypes.string, originalRevision: PropTypes.string, selectedTimeRange: PropTypes.string, updateParams: PropTypes.func.isRequired, originalSignature: PropTypes.string, newSignature: PropTypes.string, framework: PropTypes.string, }), user: PropTypes.shape({}).isRequired, dateRangeOptions: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]), filterByFramework: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]), getDisplayResults: PropTypes.func.isRequired, getQueryParams: PropTypes.func.isRequired, projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired, hasSubtests: PropTypes.bool, frameworks: PropTypes.arrayOf(PropTypes.shape({})).isRequired, }; CompareTableView.defaultProps = { dateRangeOptions: null, filterByFramework: null, validated: PropTypes.shape({}), hasSubtests: false, };