treeherder/ui/push-health/Health.jsx

332 строки
11 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import { Container, Spinner, Navbar, Nav } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import camelCase from 'lodash/camelCase';
import { Helmet } from 'react-helmet';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import faviconBroken from '../img/push-health-broken.png';
import faviconOk from '../img/push-health-ok.png';
import ErrorMessages from '../shared/ErrorMessages';
import PushModel from '../models/push';
import RepositoryModel from '../models/repository';
import StatusProgress from '../shared/StatusProgress';
import { scrollToLine } from '../helpers/utils';
import { resultColorMap, getIcon } from '../helpers/display';
import {
createQueryParams,
parseQueryParams,
updateQueryParams,
} from '../helpers/url';
import InputFilter from '../shared/InputFilter';
import TestMetric from './TestMetric';
import JobListMetric from './JobListMetric';
import CommitHistory from './CommitHistory';
export default class Health extends React.PureComponent {
constructor(props) {
super(props);
const params = new URLSearchParams(props.location.search);
this.state = {
revision: params.get('revision'),
repo: params.get('repo'),
currentRepo: null,
metrics: {},
jobs: null,
result: null,
failureMessage: null,
defaultTabIndex: 0,
testGroup: params.get('testGroup') || '',
selectedTest: params.get('selectedTest') || '',
selectedTaskId: params.get('selectedTaskId') || '',
selectedJobName: params.get('selectedJobName') || '',
searchStr: params.get('searchStr') || '',
regressionsOrderBy: params.get('regressionsOrderBy') || 'count',
regressionsGroupBy: params.get('regressionsGroupBy') || 'path',
knownIssuesOrderBy: params.get('knownIssuesOrderBy') || 'count',
knownIssuesGroupBy: params.get('knownIssuesGroupBy') || 'path',
};
}
async componentDidMount() {
const { repo, testGroup } = this.state;
const { location } = this.props;
const {
metrics: { linting, builds, tests },
} = await this.updatePushHealth();
const params = parseQueryParams(location.search);
let defaultTabIndex;
if (params.tab !== undefined) {
defaultTabIndex = ['linting', 'builds', 'tests'].findIndex(
(metric) => metric === params.tab,
);
} else if (testGroup) {
defaultTabIndex = 2;
} else {
defaultTabIndex = [linting, builds, tests].findIndex(
(metric) => metric.result === 'fail',
);
}
const repos = await RepositoryModel.getList();
const currentRepo = repos.find((repoObj) => repoObj.name === repo);
this.setState({ defaultTabIndex, currentRepo });
// Update the tests every two minutes.
this.testTimerId = setInterval(() => this.updatePushHealth(), 120000);
this.notificationsId = setInterval(() => {
this.props.clearNotification();
}, 4000);
}
componentWillUnmount() {
clearInterval(this.testTimerId);
}
updateParamsAndState = (stateObj) => {
const { location, history } = this.props;
const newParams = {
...parseQueryParams(location.search),
...stateObj,
};
const queryString = createQueryParams(newParams);
updateQueryParams(queryString, history, location);
this.setState(stateObj);
};
updatePushHealth = async () => {
const { repo, revision, status } = this.state;
if (status) {
const { running, pending, completed } = status;
if (completed > 0 && pending === 0 && running === 0) {
clearInterval(this.testTimerId);
return;
}
}
const { data, failureStatus } = await PushModel.getHealth(repo, revision);
const newState = !failureStatus ? data : { failureMessage: data };
this.setState(newState);
return newState;
};
setExpanded = (metricName, expanded) => {
const root = camelCase(metricName);
const key = `${root}Expanded`;
const { [key]: oldExpanded } = this.state;
if (oldExpanded !== expanded) {
this.setState({
[key]: expanded,
});
} else if (expanded) {
scrollToLine(`#${root}Metric`, 0, 0, {
behavior: 'smooth',
block: 'center',
});
}
};
filter = (searchStr) => {
const { location, history } = this.props;
const newParams = { ...parseQueryParams(location.search), searchStr };
if (!searchStr.length) {
delete newParams.searchStr;
}
const queryString = createQueryParams(newParams);
updateQueryParams(queryString, history, location);
this.setState({ searchStr });
};
render() {
const {
metrics,
result,
repo,
revision,
jobs,
failureMessage,
status,
searchStr,
currentRepo,
testGroup,
selectedTest,
defaultTabIndex,
selectedTaskId,
selectedJobName,
regressionsOrderBy,
regressionsGroupBy,
knownIssuesOrderBy,
knownIssuesGroupBy,
} = this.state;
const { tests, commitHistory, linting, builds } = metrics;
const { notify } = this.props;
return (
<React.Fragment>
<Navbar color="light" light expand="sm" className="w-100">
{!!tests && (
<Nav className="mb-2 pt-2 pl-3 justify-content-between w-100">
<span />
<span className="mr-2 d-flex">
<InputFilter
updateFilterText={this.filter}
placeholder="filter path or platform"
/>
</span>
</Nav>
)}
</Navbar>
<Helmet>
<link
rel="shortcut icon"
href={result === 'fail' ? faviconBroken : faviconOk}
/>
<title>{`[${
(status && status.testfailed) || 0
} failures] Push Health`}</title>
</Helmet>
<Container fluid className="mt-2 mb-5 max-width-default">
{!!tests && !!currentRepo && (
<React.Fragment>
<div className="d-flex my-5">
<StatusProgress
counts={status}
customStyle="progress-relative"
/>
<div className="mt-4 ml-2">
{commitHistory.details && (
<CommitHistory
history={commitHistory.details}
revision={revision}
currentRepo={currentRepo}
compareWithParent={this.compareWithParent}
/>
)}
</div>
</div>
<div className="mb-3" />
<Tabs
className="w-100 h-100 mr-5 mt-2"
selectedTabClassName="selected-detail-tab"
defaultIndex={defaultTabIndex}
>
<TabList className="font-weight-500 text-secondary d-flex justify-content-end border-bottom font-size-18">
{linting.result !== 'none' && (
<Tab className="pb-2 list-inline-item ml-4 pointable">
<span className="text-success">
<FontAwesomeIcon
icon={getIcon(linting.result)}
className={`mr-1 text-${
resultColorMap[linting.result]
}`}
/>
</span>
Linting
</Tab>
)}
{builds.result !== 'none' && (
<Tab className="list-inline-item ml-4 pointable">
<FontAwesomeIcon
icon={getIcon(builds.result)}
className={`mr-1 text-${resultColorMap[builds.result]}`}
/>
Builds
</Tab>
)}
{tests.result !== 'none' && (
<Tab className="list-inline-item ml-4 pointable">
<FontAwesomeIcon
fill={resultColorMap[tests.result]}
icon={getIcon(tests.result)}
className={`mr-1 text-${resultColorMap[tests.result]}`}
/>
Tests
</Tab>
)}
</TabList>
<div>
<TabPanel>
<JobListMetric
data={linting}
currentRepo={currentRepo}
revision={revision}
setExpanded={this.setExpanded}
updateParamsAndState={this.updateParamsAndState}
notify={notify}
selectedTaskId={selectedTaskId}
selectedJobName={selectedJobName}
/>
</TabPanel>
<TabPanel>
<JobListMetric
data={builds}
currentRepo={currentRepo}
revision={revision}
setExpanded={this.setExpanded}
updateParamsAndState={this.updateParamsAndState}
notify={notify}
selectedTaskId={selectedTaskId}
selectedJobName={selectedJobName}
/>
</TabPanel>
<TabPanel>
<TestMetric
jobs={jobs}
data={tests}
repo={repo}
currentRepo={currentRepo}
revision={revision}
notify={notify}
setExpanded={this.setExpanded}
searchStr={searchStr}
testGroup={testGroup}
selectedTest={selectedTest}
regressionsOrderBy={regressionsOrderBy}
regressionsGroupBy={regressionsGroupBy}
knownIssuesOrderBy={knownIssuesOrderBy}
knownIssuesGroupBy={knownIssuesGroupBy}
selectedTaskId={selectedTaskId}
selectedJobName={selectedJobName}
updateParamsAndState={this.updateParamsAndState}
investigateTest={this.investigateTest}
unInvestigateTest={this.unInvestigateTest}
updatePushHealth={this.updatePushHealth}
/>
</TabPanel>
</div>
</Tabs>
</React.Fragment>
)}
{failureMessage && <ErrorMessages failureMessage={failureMessage} />}
{!failureMessage && !tests && (
<h4>
<Spinner />
<span className="ml-2 pb-1">Gathering health data...</span>
</h4>
)}
</Container>
</React.Fragment>
);
}
}
Health.propTypes = {
location: PropTypes.shape({}).isRequired,
};