ui/shared/tabs/failureSummary/FailureSummaryTab.jsx (300 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { Button } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { thBugSuggestionLimit, thEvents } from '../../../helpers/constants'; import { getResultState, isReftest } from '../../../helpers/job'; import { getReftestUrl } from '../../../helpers/url'; import BugFiler from '../../BugFiler'; import BugSuggestionsModel from '../../../models/bugSuggestions'; import ErrorsList from './ErrorsList'; import ListItem from './ListItem'; import SuggestionsListItem from './SuggestionsListItem'; class FailureSummaryTab extends React.Component { constructor(props) { super(props); this.state = { isBugFilerOpen: false, suggestions: [], errors: [], bugSuggestionsLoading: false, }; } componentDidMount() { this.loadBugSuggestions(); } componentDidUpdate(prevProps) { const { selectedJob } = this.props; if ( !!selectedJob && !!prevProps.selectedJob && selectedJob.id !== prevProps.selectedJob.id ) { this.loadBugSuggestions(); } } fileBug = (suggestion) => { const { selectedJob, pinJob } = this.props; pinJob(selectedJob); this.setState({ isBugFilerOpen: true, suggestion, }); }; toggleBugFiler = () => { this.setState((prevState) => ({ isBugFilerOpen: !prevState.isBugFilerOpen, })); }; bugFilerCallback = (data) => { const { addBug } = this.props; addBug({ id: data.id, newBug: data.id }); window.dispatchEvent(new CustomEvent(thEvents.saveClassification)); // Open the newly filed bug in a new tab or window for further editing window.open(data.url); }; loadBugSuggestions = () => { const { selectedJob } = this.props; if (!selectedJob) { return; } this.setState({ bugSuggestionsLoading: true }); BugSuggestionsModel.get(selectedJob.id).then(async (suggestions) => { suggestions.forEach((suggestion) => { const simpleCase = []; // HACK: if not a test failure for any test in error set, ignore let crashLeak = false; if (suggestion.search.startsWith('PROCESS-CRASH')) { crashLeak = true; } let isPerfTest = false; if ( suggestion.search.includes('browser/base/content/test/performance') ) { isPerfTest = true; } if (suggestion.bugs.open_recent.length > 0) { suggestion.bugs.open_recent.forEach((bug) => { if (bug.summary.endsWith('single tracking bug')) { simpleCase.push(bug); } }); } if (simpleCase.length === 0 && suggestion.bugs.all_others.length > 0) { suggestion.bugs.all_others.forEach((bug) => { if (bug.summary.endsWith('single tracking bug')) { simpleCase.push(bug); } }); } // HACK: use the simple case if found. if (simpleCase.length > 0 && !isPerfTest && !crashLeak) { suggestion.bugs.open_recent = simpleCase; // HACK: remove any other bugs, keep this simple. suggestion.bugs.all_others = []; } suggestion.bugs.too_many_open_recent = suggestion.bugs.open_recent.length > thBugSuggestionLimit; suggestion.bugs.too_many_all_others = suggestion.bugs.all_others.length > thBugSuggestionLimit; suggestion.valid_open_recent = suggestion.bugs.open_recent.length > 0 && !suggestion.bugs.too_many_open_recent; suggestion.valid_all_others = suggestion.bugs.all_others.length > 0 && !suggestion.bugs.too_many_all_others && // If we have too many open_recent bugs, we're unlikely to have // relevant all_others bugs, so don't show them either. !suggestion.bugs.too_many_open_recent; }); this.setState({ bugSuggestionsLoading: false, suggestions }, () => { const scrollArea = document.querySelector( '#failure-summary-scroll-area', ); if (scrollArea.scrollTo) { scrollArea.scrollTo(0, 0); window.getSelection().removeAllRanges(); } }); }); }; render() { const { jobLogUrls, logParseStatus, logViewerFullUrl, selectedJob, addBug, currentRepo, developerMode, } = this.props; const { isBugFilerOpen, suggestion, bugSuggestionsLoading, suggestions, errors, } = this.state; const logs = jobLogUrls; const jobLogsAllParsed = logs.length > 0 && logs.every((jlu) => jlu.parse_status !== 'pending'); selectedJob.newFailure = 0; suggestions.forEach((suggestion) => { suggestion.showNewButton = false; // small hack here to use counter==0 and try for display only if ( suggestion.search.split(' | ').length === 3 && (suggestion.failure_new_in_rev === true || (suggestion.counter === 0 && currentRepo.name === 'try')) ) { if (selectedJob.newFailure === 0) { suggestion.showNewButton = true; } selectedJob.newFailure++; } }); return ( <div className="w-100 h-100" role="region" aria-label="Failure Summary"> <ul className={`${ !developerMode && 'font-size-11' } list-unstyled w-100 h-100 mb-0 overflow-auto text-small`} ref={this.fsMount} id="failure-summary-scroll-area" > {selectedJob.newFailure > 0 && ( <Button className="failure-summary-new-message" outline title="New Test Failure" > {selectedJob.newFailure} new failure line(s). First one is flagged, it might be good to look at all failures in this job. </Button> )} {suggestions.map((suggestion, index) => ( <SuggestionsListItem key={`${selectedJob.id}-${index}`} // eslint-disable-line react/no-array-index-key index={index} suggestion={suggestion} toggleBugFiler={() => this.fileBug(suggestion)} selectedJob={selectedJob} addBug={addBug} currentRepo={currentRepo} developerMode={developerMode} /> ))} {!!errors.length && <ErrorsList errors={errors} />} {!jobLogsAllParsed && <ListItem text="Log parsing not complete" />} {!bugSuggestionsLoading && jobLogsAllParsed && !logs.length && !suggestions.length && !errors.length && <ListItem text="Failure summary is empty" />} {!bugSuggestionsLoading && jobLogsAllParsed && !!logs.length && logParseStatus === 'success' && ( <li> <p className="failure-summary-line-empty mb-0"> Log parsing complete. Generating bug suggestions. <br /> <span> The content of this panel will refresh in 5 seconds. </span> </p> </li> )} {!bugSuggestionsLoading && !jobLogsAllParsed && logs.map((jobLog) => ( <li key={jobLog.id}> <p className="failure-summary-line-empty mb-0"> Log parsing in progress. <br /> <a title="Open the raw log in a new window" target="_blank" rel="noopener noreferrer" href={jobLog.url} > The raw log </a>{' '} is available. This panel will automatically recheck every 5 seconds. </p> </li> ))} {!bugSuggestionsLoading && logParseStatus === 'failed' && ( <ListItem text="Log parsing failed. Unable to generate failure summary." /> )} {!bugSuggestionsLoading && logParseStatus === 'skipped-size' && ( <ListItem text="Log parsing was skipped since the log exceeds the size limit." /> )} {!bugSuggestionsLoading && !logs.length && ( <ListItem text={`No logs yet available for this ${getResultState( selectedJob, )} job.`} /> )} {bugSuggestionsLoading && ( <div className="overlay"> <div> <FontAwesomeIcon icon={faSpinner} pulse className="th-spinner-lg" title="Loading..." /> </div> </div> )} </ul> {isBugFilerOpen && ( <BugFiler isOpen={isBugFilerOpen} toggle={this.toggleBugFiler} suggestion={suggestion} suggestions={suggestions} fullLog={jobLogUrls[0].url} parsedLog={logViewerFullUrl} reftestUrl={ isReftest(selectedJob) ? getReftestUrl(jobLogUrls[0].url) : '' } successCallback={this.bugFilerCallback} selectedJob={selectedJob} currentRepo={currentRepo} platform={selectedJob.platform} /> )} </div> ); } } FailureSummaryTab.propTypes = { selectedJob: PropTypes.shape({}).isRequired, jobLogUrls: PropTypes.arrayOf({ id: PropTypes.number.isRequired, job_id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, url: PropTypes.string.isRequired, parse_status: PropTypes.string.isRequired, }), logParseStatus: PropTypes.string, logViewerFullUrl: PropTypes.string, currentRepo: PropTypes.shape({}).isRequired, addBug: PropTypes.func, pinJob: PropTypes.func, developerMode: PropTypes.bool, }; FailureSummaryTab.defaultProps = { jobLogUrls: [], logParseStatus: 'pending', logViewerFullUrl: null, addBug: null, pinJob: null, developerMode: false, }; export default FailureSummaryTab;