ui/perfherder/alerts/AlertHeader.jsx (352 lines of code) (raw):

import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { UncontrolledDropdown, DropdownMenu, DropdownItem, DropdownToggle, Container, Row, Col, Button, Input, InputGroup, } from 'reactstrap'; import { getJobsUrl, getPerfCompareBaseURL } from '../../helpers/url'; import { toMercurialShortDateStr } from '../../helpers/display'; import SimpleTooltip from '../../shared/SimpleTooltip'; import Assignee from './Assignee'; import TagsList from './TagsList'; import AlertHeaderTitle from './AlertHeaderTitle'; const AlertHeader = ({ frameworks, alertSummary, repoModel, issueTrackers, user, updateAssignee, changeRevision, updateViewState, }) => { const [inEditMode, setInEditMode] = useState(false); const [newRevisionTo, setnewRevisionTo] = useState(alertSummary.revision); const [newRevisionFrom, setnewRevisionFrom] = useState( alertSummary.prev_push_revision, ); const revisionToType = 'to'; const handleEditMode = () => { setnewRevisionTo(''); setnewRevisionFrom(''); setInEditMode(true); }; const handleRevisionChange = (revisionType) => (event) => { // revisionType can only be "to" or "from" if (revisionType === revisionToType) setnewRevisionTo(event.target.value); else setnewRevisionFrom(event.target.value); }; const saveRevision = async () => { const trimmedRevisionTo = newRevisionTo.trim() === '' ? alertSummary.revision : newRevisionTo.trim(); const trimmedRevisionFrom = newRevisionFrom.trim() === '' ? alertSummary.prev_push_revision : newRevisionFrom.trim(); const longHashMatch = /\b[a-f0-9]{40}\b/; if ( !longHashMatch.test(trimmedRevisionTo) || !longHashMatch.test(trimmedRevisionFrom) ) { updateViewState({ errorMessages: [ `Invalid Revision format, expected a 40 character hash.`, ], }); return; } const response = await changeRevision( trimmedRevisionTo, trimmedRevisionFrom, ); if (!response.failureStatus) { setInEditMode(false); } }; const cancelEditMode = () => { setInEditMode(false); }; const getIssueTrackerUrl = () => { const { issue_tracker_url: issueTrackerUrl } = issueTrackers.find( (tracker) => tracker.id === alertSummary.issue_tracker, ); return issueTrackerUrl + alertSummary.bug_number; }; const handleRevertRevision = (revisionType) => () => { // revisionType can only be "to" or "from" if (revisionType === revisionToType) setnewRevisionTo(alertSummary.original_revision); else setnewRevisionFrom(alertSummary.original_prev_push_revision); }; const bugNumber = alertSummary.bug_number ? `Bug ${alertSummary.bug_number}` : ''; const performanceTags = alertSummary.performance_tags || []; const alertSummaryDatetime = new Date(alertSummary.push_timestamp * 1000); const formattedSummaryRevision = alertSummary.revision.slice(0, 12); const created = new Date(alertSummary.created.slice(0, 19)); return ( <Container> <AlertHeaderTitle alertSummary={alertSummary} frameworks={frameworks} /> <Row className="font-weight-normal"> <Col className="p-0 pr-1" xs="auto"> <Row className="m-0 px-0 py-0"> <SimpleTooltip text={toMercurialShortDateStr(alertSummaryDatetime)} tooltipText="Push date" /> </Row> <Row className="m-0 px-0 py-0"> <SimpleTooltip text={toMercurialShortDateStr(created)} tooltipText="Alert Summary created" /> </Row> </Col> {user.isStaff && ( <Col className="p-0" xs="auto"> <Button className="ml-1" color="darker-secondary" size="xs" onClick={handleEditMode} title="Click to edit revision" > Edit Revisions </Button> </Col> )} <Col className="p-0" xs="auto"> <UncontrolledDropdown tag="span"> <DropdownToggle className="btn-xs ml-1" color="secondary" caret data-testid="push-dropdown" > {formattedSummaryRevision} </DropdownToggle> <DropdownMenu> <DropdownItem tag="a" className="text-dark" href={getJobsUrl({ repo: alertSummary.repository, fromchange: alertSummary.prev_push_revision, tochange: alertSummary.revision, })} target="_blank" rel="noopener noreferrer" > Jobs </DropdownItem> <DropdownItem tag="a" className="text-dark" href={repoModel.getPushLogRangeHref({ fromchange: alertSummary.prev_push_revision, tochange: alertSummary.revision, })} target="_blank" rel="noopener noreferrer" > Pushlog </DropdownItem> <DropdownItem className="text-dark" disabled data-testid="prev-push-revision" > From: {`${alertSummary.prev_push_revision.slice(0, 12)}`} </DropdownItem> <DropdownItem className="text-dark" disabled data-testid="to-push-revision" > To: {formattedSummaryRevision} </DropdownItem> </DropdownMenu> </UncontrolledDropdown> </Col> {bugNumber && ( <Col className="p-0" xs="auto"> {alertSummary.issue_tracker && issueTrackers.length > 0 ? ( <a className="btn btn-secondary btn-xs ml-1 text-white" href={getIssueTrackerUrl(alertSummary)} target="_blank" rel="noopener noreferrer" > {bugNumber} </a> ) : ( { bugNumber } )} </Col> )} <Col className="p-0" xs="auto"> <Assignee assigneeUsername={alertSummary.assignee_username} updateAssignee={updateAssignee} user={user} /> </Col> </Row> <Row className="px-0 py-2"> <a href={getPerfCompareBaseURL( alertSummary.repository, alertSummary.prev_push_revision, alertSummary.repository, alertSummary.revision, alertSummary.framework, )} target="_blank" rel="noopener noreferrer" > PerfCompare comparison </a> {(alertSummary.original_revision !== alertSummary.revision || alertSummary.original_prev_push_revision !== alertSummary.prev_push_revision) && ( <span className="px-2">Revisions have been modified.</span> )} </Row> {alertSummary.duplicated_summaries_ids.length > 0 && ( <Row> Duplicated summaries: {alertSummary.duplicated_summaries_ids.map((id, index) => ( <Link className="text-dark mr-1" target="_blank" to={`./alerts?id=${id}&hideDwnToInv=0`} id={`duplicated alert summary ${id.toString()} `} style={{ marginLeft: '5px' }} > Alert #{id} {alertSummary.duplicated_summaries_ids.length - 1 !== index && ', '} </Link> ))} </Row> )} <Row> {performanceTags.length > 0 && ( <Col className="p-0" xs="auto"> <TagsList tags={alertSummary.performance_tags} /> </Col> )} </Row> {inEditMode && ( <div> <Row className="mb-2"> <Col xs="2" className="p-0 align-content-center"> <span className="align-middle">Current From: </span> </Col> <Col xs="2" className="p-0 align-content-center"> <span className="align-middle"> {`${alertSummary.prev_push_revision.slice(0, 12)}`}{' '} </span> </Col> <Col xs="5" className="p-0"> <InputGroup size="sm"> <Input value={newRevisionFrom} placeholder="Enter desired revision" onChange={handleRevisionChange('from')} autoFocus /> </InputGroup> </Col> <Col xs="3" className="p-0"> <Button className="ml-1" size="sm" disabled={ alertSummary.original_prev_push_revision === alertSummary.prev_push_revision } onClick={handleRevertRevision('from')} > Reset Revision </Button> </Col> </Row> <Row className="mb-2"> <Col xs="2" className="p-0 align-content-center"> <span className="align-middle">Current To: </span> </Col> <Col xs="2" className="p-0 align-content-center"> <span className="align-middle">{formattedSummaryRevision} </span> </Col> <Col xs="5" className="p-0"> <InputGroup size="sm"> <Input value={newRevisionTo} placeholder="Enter desired revision" onChange={handleRevisionChange('to')} autoFocus /> </InputGroup> </Col> <Col xs="3" className="p-0"> <Button className="ml-1" size="sm" disabled={ alertSummary.original_revision === alertSummary.revision } onClick={handleRevertRevision('to')} > Reset Revision </Button> </Col> </Row> <Row> <Col className="p-0"> <Button color="primary" className="ml-1" size="xs" disabled={newRevisionTo === '' && newRevisionFrom === ''} onClick={saveRevision} > Save </Button> <Button color="secondary" className="ml-1" size="xs" onClick={cancelEditMode} > Cancel </Button> </Col> </Row> </div> )} </Container> ); }; AlertHeader.propTypes = { alertSummary: PropTypes.shape({}).isRequired, repoModel: PropTypes.shape({}).isRequired, user: PropTypes.shape({}).isRequired, issueTrackers: PropTypes.arrayOf(PropTypes.shape({})), }; AlertHeader.defaultProps = { issueTrackers: [], }; export default AlertHeader;