ui/perfherder/alerts/StatusDropdown.jsx (479 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { UncontrolledDropdown, DropdownMenu, DropdownItem, DropdownToggle, Col, Label, } from 'reactstrap'; import template from 'lodash/template'; import templateSettings from 'lodash/templateSettings'; import { getFrameworkName, getFilledBugSummary, getStatus, updateAlertSummary, } from '../perf-helpers/helpers'; import { getData, create } from '../../helpers/http'; import TextualSummary from '../perf-helpers/textualSummary'; import { getApiUrl, bzBaseUrl, bugzillaBugsApi } from '../../helpers/url'; import { summaryStatusMap } from '../perf-helpers/constants'; import DropdownMenuItems from '../../shared/DropdownMenuItems'; import BrowsertimeAlertsExtraData from '../../models/browsertimeAlertsExtraData'; import { isWeekend } from '../perf-helpers/alertCountdownHelper'; import AlertModal from './AlertModal'; import FileBugModal from './FileBugModal'; import NotesModal from './NotesModal'; import TagsModal from './TagsModal'; import AlertStatusCountdown from './AlertStatusCountdown'; export default class StatusDropdown extends React.Component { constructor(props) { super(props); this.state = { showBugModal: false, showFileBugModal: false, showNotesModal: false, showTagsModal: false, selectedValue: this.props.issueTrackers[0].text, browsertimeAlertsExtraData: new BrowsertimeAlertsExtraData( this.props.alertSummary, this.props.frameworks, ), isWeekend: isWeekend(), fileBugErrorMessage: null, }; } getCulpritDetails = async (culpritId) => { const bugDetails = await getData(bugzillaBugsApi(`bug/${culpritId}`)); if (bugDetails.failureStatus) { return bugDetails; } const bugData = bugDetails.data.bugs[0]; const bugVersion = 'Default'; let needinfoFrom = ''; if (bugData.assigned_to !== 'nobody@mozilla.org') { needinfoFrom = bugData.assigned_to; } else { const componentInfo = await getData( bugzillaBugsApi(`component/${bugData.product}/${bugData.component}`), ); needinfoFrom = componentInfo.data.triage_owner; } // Using set because it doesn't keep duplicates by default const ccList = new Set([bugData.creator]); return { bug_version: bugVersion, needinfoFrom, ccList, component: bugData.component, product: bugData.product, }; }; fileBug = async (culpritId) => { const { alertSummary, repoModel, bugTemplate, updateViewState, filteredAlerts, frameworks, user, } = this.props; const { browsertimeAlertsExtraData } = this.state; let result = bugTemplate; if (!result) { const { data, failureStatus } = await getData( getApiUrl( `/performance/bug-template/?framework=${alertSummary.framework}`, ), ); if (failureStatus) { updateViewState({ errorMessages: [`Failed to retrieve bug template: ${data}`], }); } else { [result] = data; updateViewState({ bugTemplate: result }); } } const textualSummary = new TextualSummary( frameworks, filteredAlerts, alertSummary, null, await browsertimeAlertsExtraData.enrichAndRetrieveAlerts(), ); const templateArgs = { bugType: 'defect', framework: getFrameworkName(frameworks, alertSummary.framework), revision: alertSummary.revision, revisionHref: repoModel.getPushLogHref(alertSummary.revision), alertHref: `${window.location.origin}/perfherder/alerts?id=${alertSummary.id}`, alertSummary: textualSummary.markdown, alertSummaryId: alertSummary.id, user: user.email, }; templateSettings.interpolate = /{{([\s\S]+?)}}/g; const fillTemplate = template(result.text); const commentText = fillTemplate(templateArgs); const bugTitle = `${getFilledBugSummary(alertSummary)}`; const culpritDetails = await this.getCulpritDetails(culpritId); const componentInfo = await getData( bugzillaBugsApi( `component/${result.default_product}/${result.default_component}`, ), ); let defaultParams = { type: templateArgs.bugType, version: 'unspecified', cc: [result.cc_list], description: commentText, component: result.default_component, product: result.default_product, keywords: result.keywords, summary: bugTitle, whiteboard: result.status_whiteboard, needinfo_from: componentInfo.data.triage_owner, by_treeherder: true, }; if (!culpritDetails.failureStatus) { let cc = culpritDetails.ccList.add(result.cc_list); cc = Array.from(cc); defaultParams = { ...defaultParams, cc, needinfo_from: culpritDetails.needinfoFrom, component: culpritDetails.component, product: culpritDetails.product, regressed_by: culpritId, }; } const createResult = await create( getApiUrl('/bugzilla/create_bug/'), defaultParams, ); if (createResult.failureStatus) { return { failureStatus: createResult.failureStatus, data: createResult.data, }; } window.open(`${bzBaseUrl}show_bug.cgi?id=${createResult.data.id}`); return { failureStatus: null, }; }; copySummary = async () => { const { alertSummary, repoModel, filteredAlerts, frameworks } = this.props; const { browsertimeAlertsExtraData } = this.state; const textualSummary = new TextualSummary( frameworks, filteredAlerts, alertSummary, null, await browsertimeAlertsExtraData.enrichAndRetrieveAlerts(), ); const templateArgs = { bugType: 'defect', framework: getFrameworkName(frameworks, alertSummary.framework), revision: alertSummary.revision, revisionHref: repoModel.getPushLogHref(alertSummary.revision), alertHref: `${window.location.origin}/perfherder/alerts?id=${alertSummary.id}`, alertSummary: textualSummary.markdown, alertSummaryId: alertSummary.id, }; const containsRegression = textualSummary.alerts.some( (item) => item.is_regression === true, ); const regressionTemplate = ` Perfherder has detected a {{ framework }} performance change from push [{{ revision }}]({{ revisionHref }}). As author of one of the patches included in that push, we need your help to address this regression. Please **acknowledge, and begin investigating this alert within 3 business days, or the patch(es) may be backed out** in accordance with our [regression policy](https://www.mozilla.org/en-US/about/governance/policies/regressions/). Our [guide to handling regression bugs](https://firefox-source-docs.mozilla.org/testing/perfdocs/perftest-in-a-nutshell.html#help-i-have-a-regression-what-do-i-do) has information about how you can proceed with this investigation. If you have any questions or need any help with the investigation, please reach out to a performance sheriff. Alternatively, you can find help on Slack by joining [#perf-help](https://mozilla.enterprise.slack.com/archives/C03U19JCSFQ), and on Matrix you can find help by joining [#perftest](https://matrix.to/#/#perftest:mozilla.org). {{ alertSummary }} Details of the alert can be found in the [alert summary]({{ alertHref }}), including links to graphs and comparisons for each of the affected tests. If you need the profiling jobs [you can trigger them yourself from treeherder job view](https://firefox-source-docs.mozilla.org/testing/perfdocs/perftest-in-a-nutshell.html#using-the-firefox-profiler) or ask a performance sheriff to do that for you. You can run all of these tests on try with \`./mach try perf --alert {{ alertSummaryId }}\` The following [documentation link](https://firefox-source-docs.mozilla.org/testing/perfdocs/mach-try-perf.html#running-alert-tests) provides more information about this command. `; const improvementTemplate = ` Perfherder has detected a {{ framework }} performance change from push [{{ revision }}]({{ revisionHref }}). If you have any questions, please reach out to a performance sheriff. Alternatively, you can find help on Slack by joining [#perf-help](https://mozilla.enterprise.slack.com/archives/C03U19JCSFQ), and on Matrix you can find help by joining [#perftest](https://matrix.to/#/#perftest:mozilla.org). {{ alertSummary }} Details of the alert can be found in the [alert summary]({{ alertHref }}), including links to graphs and comparisons for each of the affected tests. If you need the profiling jobs [you can trigger them yourself from treeherder job view](https://firefox-source-docs.mozilla.org/testing/perfdocs/perftest-in-a-nutshell.html#using-the-firefox-profiler) or ask a performance sheriff to do that for you. You can run all of these tests on try with \`./mach try perf --alert {{ alertSummaryId }}\` The following [documentation link](https://firefox-source-docs.mozilla.org/testing/perfdocs/mach-try-perf.html#running-alert-tests) provides more information about this command. `; const templateText = containsRegression ? regressionTemplate : improvementTemplate; templateSettings.interpolate = /{{([\s\S]+?)}}/g; const fillTemplate = template(templateText); const commentText = fillTemplate(templateArgs); // can't access the clipboardData on event unless it's done from react's // onCopy, onCut or onPaste props so using this workaround navigator.clipboard.writeText(commentText).then(() => {}); }; toggle = (state) => { this.setState((prevState) => ({ [state]: !prevState[state], })); if (this.state.showFileBugModal) { this.setState({ fileBugErrorMessage: null, }); } }; updateAndClose = async (event, params, state) => { event.preventDefault(); this.changeAlertSummary(params); this.toggle(state); }; fileBugAndClose = async (event, params, state) => { event.preventDefault(); const culpritId = params.bug_number; const createResult = await this.fileBug(culpritId); if (createResult.failureStatus) { this.setState({ fileBugErrorMessage: `Failure: ${createResult.data}`, }); } else { this.toggle(state); } }; changeAlertSummary = async (params) => { const { alertSummary, updateState, updateViewState } = this.props; const { data, failureStatus } = await updateAlertSummary( alertSummary.id, params, ); if (failureStatus) { return updateViewState({ errorMessages: [ `Failed to update alert summary ${alertSummary.id}: ${data}`, ], }); } updateState({ alertSummary: data }); }; isResolved = (alertStatus) => alertStatus === 'backedout' || alertStatus === 'fixed' || alertStatus === 'wontfix'; isValidStatus = (alertStatus, status) => alertStatus === 'investigating' || (alertStatus !== status && this.isResolved(alertStatus)); render() { const { alertSummary, user, issueTrackers, performanceTags } = this.props; const { showBugModal, showFileBugModal, showNotesModal, showTagsModal, selectedValue, isWeekend, } = this.state; const alertStatus = getStatus(alertSummary.status); const alertSummaryActiveTags = alertSummary.performance_tags || []; return ( <React.Fragment> {issueTrackers.length > 0 && ( <AlertModal showModal={showBugModal} toggle={() => this.toggle('showBugModal')} updateAndClose={(event, inputValue) => this.updateAndClose( event, { bug_number: parseInt(inputValue, 10), issue_tracker: issueTrackers.find( (item) => item.text === selectedValue, ).id, }, 'showBugModal', ) } header="Link to Bug" title="Bug Number" submitButtonText="Assign" dropdownOption={ <Col> <Label for="issueTrackerSelector">Select Bug Tracker</Label> <UncontrolledDropdown> <DropdownToggle caret outline> {selectedValue} </DropdownToggle> <DropdownMenuItems updateData={(selectedValue) => this.setState({ selectedValue }) } selectedItem={selectedValue} options={issueTrackers.map((item) => item.text)} /> </UncontrolledDropdown> </Col> } /> )} <FileBugModal showModal={showFileBugModal} toggle={() => this.toggle('showFileBugModal')} updateAndClose={(event, inputValue) => this.fileBugAndClose( event, { bug_number: parseInt(inputValue, 10), issue_tracker: issueTrackers.find( (item) => item.text === selectedValue, ).id, }, 'showFileBugModal', ) } header="File Regression Bug for" title="Enter Bug Number" submitButtonText="File Bug" user={user} errorMessage={this.state.fileBugErrorMessage} /> <NotesModal showModal={showNotesModal} toggle={() => this.toggle('showNotesModal')} alertSummary={alertSummary} updateAndClose={this.updateAndClose} /> <TagsModal showModal={showTagsModal} toggle={() => this.toggle('showTagsModal')} alertSummary={alertSummary} performanceTags={performanceTags} updateAndClose={this.updateAndClose} /> <UncontrolledDropdown tag="span" className="status-drop-down-container"> <DropdownToggle className="btn-xs" color="darker-secondary" caret> {getStatus(alertSummary.status)} </DropdownToggle> <DropdownMenu> <DropdownItem tag="a" onClick={this.copySummary}> Copy Summary </DropdownItem> {!alertSummary.bug_number && user.isStaff && ( <DropdownItem tag="a" onClick={() => this.toggle('showFileBugModal')} > File bug </DropdownItem> )} {user.isStaff && ( <React.Fragment> {!alertSummary.bug_number ? ( <DropdownItem tag="a" onClick={() => this.toggle('showBugModal')} > Link to bug </DropdownItem> ) : ( <DropdownItem tag="a" onClick={() => this.changeAlertSummary({ bug_number: null, }) } > Unlink from bug </DropdownItem> )} <DropdownItem tag="a" onClick={() => this.toggle('showNotesModal')} > {!alertSummary.notes ? 'Add notes' : 'Edit notes'} </DropdownItem> {this.isResolved(alertStatus) && ( <DropdownItem tag="a" onClick={() => this.changeAlertSummary({ status: summaryStatusMap.investigating, }) } > Re-open </DropdownItem> )} {this.isValidStatus(alertStatus, 'wontfix') && ( <DropdownItem tag="a" onClick={() => this.changeAlertSummary({ status: summaryStatusMap.wontfix, }) } > Mark as won&apos;t fix </DropdownItem> )} {this.isValidStatus(alertStatus, 'backedout') && ( <DropdownItem tag="a" onClick={() => this.changeAlertSummary({ status: summaryStatusMap.backedout, }) } > Mark as backed out </DropdownItem> )} {this.isValidStatus(alertStatus, 'fixed') && ( <DropdownItem tag="a" onClick={() => this.changeAlertSummary({ status: summaryStatusMap.fixed, }) } > Mark as fixed </DropdownItem> )} <DropdownItem tag="a" onClick={() => this.toggle('showTagsModal')} > {!alertSummaryActiveTags.length ? 'Add tags' : 'Edit tags'} </DropdownItem> </React.Fragment> )} </DropdownMenu> {!isWeekend && <AlertStatusCountdown alertSummary={alertSummary} />} </UncontrolledDropdown> </React.Fragment> ); } } StatusDropdown.propTypes = { alertSummary: PropTypes.shape({}).isRequired, user: PropTypes.shape({}).isRequired, updateState: PropTypes.func.isRequired, issueTrackers: PropTypes.arrayOf( PropTypes.shape({ text: PropTypes.string, }), ), repoModel: PropTypes.shape({}).isRequired, updateViewState: PropTypes.func.isRequired, bugTemplate: PropTypes.shape({}), filteredAlerts: PropTypes.arrayOf(PropTypes.shape({})), performanceTags: PropTypes.arrayOf(PropTypes.shape({})).isRequired, }; StatusDropdown.defaultProps = { issueTrackers: [], bugTemplate: null, filteredAlerts: [], };