ui/push-health/Test.jsx (350 lines of code) (raw):

import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Button, Collapse, Nav, Navbar, NavItem, UncontrolledButtonDropdown, ButtonGroup, DropdownMenu, DropdownToggle, DropdownItem, Input, FormGroup, Label, } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCaretDown, faCaretRight, faRedo, } from '@fortawesome/free-solid-svg-icons'; import { create, destroy } from '../helpers/http'; import { getProjectUrl } from '../helpers/location'; import { investigatedTestsEndPoint } from '../helpers/url'; import JobModel from '../models/job'; import Clipboard from '../shared/Clipboard'; import PlatformConfig from './PlatformConfig'; import TaskSelection from './TaskSelection'; class Test extends PureComponent { constructor(props) { super(props); this.state = { clipboardVisible: null, detailsShowing: false, selectedTests: new Set(), allPlatformsSelected: false, }; } componentDidMount() { const { selectedTest, testGroup, test } = this.props; if (testGroup && selectedTest === test.id) { this.setState({ detailsShowing: true }); } } addSelectedTest = (test) => { this.setState((prevState) => ({ selectedTests: prevState.selectedTests.add(test), })); }; removeSelectedTest = (test) => { const { selectedTests } = this.state; selectedTests.delete(test); this.setState({ selectedTests, }); }; retriggerSelected = (times) => { const { notify, currentRepo, jobs } = this.props; const { selectedTests } = this.state; // Reduce down to the unique jobs const testJobs = Array.from(selectedTests) .filter((test) => test.isInvestigated) .reduce( (acc, test) => ({ ...acc, ...jobs[test.jobName].reduce((fjAcc, job) => ({ [job.id]: job }), {}), }), {}, ); const uniqueJobs = Object.values(testJobs); JobModel.retrigger(uniqueJobs, currentRepo, notify, times); }; markAsInvestigated = async () => { const { selectedTests } = this.state; const { notify, currentRepo, revision, updatePushHealth } = this.props; const projectUrl = `${getProjectUrl( investigatedTestsEndPoint, currentRepo.name, )}?revision=${revision}`; // TODO check if user is logged in, and if not log them in first // verify user is same user for this push before allowing this action if (selectedTests.size === 0) { notify(`Select at least one test`, 'warning'); } else { const results = await Promise.all( [...selectedTests.entries()] .filter((test) => !test.isInvestigated) .map((test) => create(projectUrl, { test: test.testName, jobName: test.jobName, jobSymbol: test.jobSymbol, }), ), ); const firstFailed = results.find((test) => test.failureStatus); if (firstFailed) { notify( `Failed to update one or more tests: ${firstFailed.data}`, 'warning', ); } this.setState({ selectedTests: new Set() }); updatePushHealth(); } }; markAsUninvestigated = async () => { const { selectedTests } = this.state; const { notify, currentRepo, revision, updatePushHealth } = this.props; if (selectedTests.size === 0) { notify(`Select at least one test`, 'warning'); } else { const results = await Promise.all( [...selectedTests.entries()] .filter((test) => test.isInvestigated) .map((test) => destroy( `${getProjectUrl( `${investigatedTestsEndPoint}${test.investigatedTestId}/`, currentRepo.name, )}?revision=${revision}`, ), ), ); const firstFailed = results.find((test) => test.failureStatus); if (firstFailed) { notify( `Failed to update one or more tests: ${firstFailed.data}`, 'warning', ); } this.setState({ selectedTests: new Set() }); updatePushHealth(); } }; setClipboardVisible = (key) => { this.setState({ clipboardVisible: key }); }; toggleDetails = () => { let { detailsShowing } = this.state; const { updateParamsAndState, test } = this.props; detailsShowing = !detailsShowing; if (detailsShowing) { updateParamsAndState({ selectedTest: test.id, selectedTaskId: '', }); } this.setState({ detailsShowing, }); }; getGroupHtml = (text) => { const splitter = text.includes('/') ? '/' : ':'; const parts = text.split(splitter); if (splitter === '/') { const bolded = parts.pop(); return ( <span> {parts.join(splitter)} {splitter} <strong data-testid="group-slash-bolded">{bolded}</strong> </span> ); } const bolded = parts.shift(); return ( <span> <strong data-testid="group-colon-bolded">{bolded}</strong> {splitter} {parts.join(splitter)} </span> ); }; selectAll = () => { const { tests } = this.props.test; const { allPlatformsSelected } = this.state; const newSelectedTests = allPlatformsSelected ? new Set() : new Set(tests); this.setState({ allPlatformsSelected: !allPlatformsSelected, selectedTests: newSelectedTests, }); }; render() { const { test: { key, id, tests }, revision, notify, currentRepo, groupedBy, jobs, selectedJobName, selectedTaskId, updateParamsAndState, } = this.props; const { clipboardVisible, detailsShowing, allPlatformsSelected, } = this.state; return ( <div> <div key={id} data-testid="test-grouping"> <span className="d-flex w-100 p-2" onMouseEnter={() => this.setClipboardVisible(key)} onMouseLeave={() => this.setClipboardVisible(null)} > <Button onClick={this.toggleDetails} className="pr-0 text-left border-0" title="Click to expand for test detail" outline > <FontAwesomeIcon icon={detailsShowing ? faCaretDown : faCaretRight} className="mr-2 min-width-1 mt-1" /> </Button> <Button onClick={this.toggleDetails} className="text-left border-0" title="Click to expand for test detail" outline > {key === 'none' ? 'All' : this.getGroupHtml(key)} <span className="ml-2 text-break"> ({tests.length} failure{tests.length > 1 && 's'}) </span> </Button> <Clipboard text={key} description="group text" visible={clipboardVisible === key} /> </span> <Collapse isOpen={detailsShowing}> <Navbar className="mb-3"> <Nav> <NavItem> <ButtonGroup size="sm" className="ml-5"> <Button title="Retrigger selected jobs once" onClick={() => this.retriggerSelected(1)} size="sm" > <FontAwesomeIcon icon={faRedo} title="Retrigger" className="mr-2" alt="" /> Retrigger Selected </Button> <UncontrolledButtonDropdown size="sm"> <DropdownToggle caret /> <DropdownMenu> {[5, 10, 15].map((times) => ( <DropdownItem key={times} title={`Retrigger selected jobs ${times} times`} onClick={() => this.retriggerSelected(times)} className="pointable" tag="a" > Retrigger selected {times} times </DropdownItem> ))} </DropdownMenu> </UncontrolledButtonDropdown> </ButtonGroup> <Button size="sm" outline color="primary" className="mx-3" title="Mark selected jobs as investigated" onClick={() => this.markAsInvestigated()} > Mark as investigated </Button> <Button size="sm" outline color="primary" className="mx-3" title="Mark selected jobs as Uninvestigated" onClick={() => this.markAsUninvestigated()} > Mark as Uninvestigated </Button> </NavItem> </Nav> </Navbar> <div className="ml-5 pl-2 position-relative"> <FormGroup className="mb-1 pl-4"> <Input aria-label="Select all platforms for this test" type="checkbox" checked={allPlatformsSelected} onChange={this.selectAll} /> <Label className="text-darker-secondary ml-4">select all</Label> </FormGroup> </div> {tests.map((failure) => ( <PlatformConfig key={failure.key} testName={failure.testName} jobName={failure.jobName} jobs={jobs[failure.jobName]} revision={revision} notify={notify} selectedJobName={selectedJobName} selectedTaskId={selectedTaskId} updateParamsAndState={(stateObj) => { stateObj.selectedTest = id; updateParamsAndState(stateObj); }} currentRepo={currentRepo} > <TaskSelection failure={failure} groupedBy={groupedBy} addSelectedTest={this.addSelectedTest} removeSelectedTest={this.removeSelectedTest} allPlatformsSelected={allPlatformsSelected} currentRepo={currentRepo} /> </PlatformConfig> ))} </Collapse> </div> </div> ); } } Test.propTypes = { test: PropTypes.shape({ key: PropTypes.string.isRequired, tests: PropTypes.arrayOf(PropTypes.shape({})).isRequired, }).isRequired, groupedBy: PropTypes.string.isRequired, revision: PropTypes.string.isRequired, currentRepo: PropTypes.shape({}).isRequired, notify: PropTypes.func.isRequired, }; export default Test;