2019-01-24 19:15:46 +03:00
|
|
|
import React from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2020-07-08 06:30:39 +03:00
|
|
|
import { Button, Navbar, Nav, Container, Spinner } from 'reactstrap';
|
2020-01-08 02:24:02 +03:00
|
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
|
|
import {
|
2020-07-08 06:30:39 +03:00
|
|
|
faClock,
|
2020-01-08 02:24:02 +03:00
|
|
|
faExclamationTriangle,
|
2020-07-08 06:30:39 +03:00
|
|
|
faCheck,
|
2020-01-08 02:24:02 +03:00
|
|
|
} from '@fortawesome/free-solid-svg-icons';
|
2020-02-27 21:01:46 +03:00
|
|
|
import camelCase from 'lodash/camelCase';
|
2020-03-28 04:10:04 +03:00
|
|
|
import { Helmet } from 'react-helmet';
|
2020-07-08 06:30:39 +03:00
|
|
|
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
2019-01-24 19:15:46 +03:00
|
|
|
|
2020-03-28 04:10:04 +03:00
|
|
|
import faviconBroken from '../img/push-health-broken.png';
|
|
|
|
import faviconOk from '../img/push-health-ok.png';
|
2019-02-01 20:58:20 +03:00
|
|
|
import ErrorMessages from '../shared/ErrorMessages';
|
2019-04-06 02:51:45 +03:00
|
|
|
import NotificationList from '../shared/NotificationList';
|
2019-06-07 00:39:50 +03:00
|
|
|
import {
|
|
|
|
clearNotificationAtIndex,
|
|
|
|
clearExpiredTransientNotifications,
|
|
|
|
} from '../helpers/notifications';
|
2019-02-01 20:58:20 +03:00
|
|
|
import PushModel from '../models/push';
|
2020-03-10 02:32:03 +03:00
|
|
|
import RepositoryModel from '../models/repository';
|
2019-12-17 00:36:58 +03:00
|
|
|
import StatusProgress from '../shared/StatusProgress';
|
|
|
|
import { getPercentComplete } from '../helpers/display';
|
2020-02-27 21:01:46 +03:00
|
|
|
import { scrollToLine } from '../helpers/utils';
|
2020-01-23 19:59:53 +03:00
|
|
|
import {
|
|
|
|
createQueryParams,
|
|
|
|
parseQueryParams,
|
|
|
|
updateQueryParams,
|
|
|
|
} from '../helpers/url';
|
|
|
|
import InputFilter from '../shared/InputFilter';
|
2019-01-24 19:15:46 +03:00
|
|
|
|
2020-01-08 02:24:02 +03:00
|
|
|
import { resultColorMap } from './helpers';
|
2019-01-24 19:15:46 +03:00
|
|
|
import Navigation from './Navigation';
|
2019-11-22 21:34:57 +03:00
|
|
|
import TestMetric from './TestMetric';
|
|
|
|
import JobListMetric from './JobListMetric';
|
2020-02-29 01:24:30 +03:00
|
|
|
import CommitHistory from './CommitHistory';
|
2019-01-24 19:15:46 +03:00
|
|
|
|
2019-06-07 00:39:50 +03:00
|
|
|
export default class Health extends React.PureComponent {
|
2019-01-24 19:15:46 +03:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
const params = new URLSearchParams(props.location.search);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
user: { isLoggedIn: false },
|
|
|
|
revision: params.get('revision'),
|
|
|
|
repo: params.get('repo'),
|
2020-03-10 02:32:03 +03:00
|
|
|
currentRepo: null,
|
2019-11-22 21:19:04 +03:00
|
|
|
metrics: {},
|
|
|
|
result: null,
|
2019-02-01 20:58:20 +03:00
|
|
|
failureMessage: null,
|
2019-06-07 00:39:50 +03:00
|
|
|
notifications: [],
|
2020-07-08 06:30:39 +03:00
|
|
|
defaultTabIndex: 0,
|
2020-03-27 02:59:38 +03:00
|
|
|
showParentMatches: false,
|
2020-01-23 19:59:53 +03:00
|
|
|
searchStr: params.get('searchStr') || '',
|
2019-01-24 19:15:46 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-01-08 02:24:02 +03:00
|
|
|
async componentDidMount() {
|
2020-03-10 02:32:03 +03:00
|
|
|
const { repo } = this.state;
|
2020-07-08 06:30:39 +03:00
|
|
|
const {
|
|
|
|
metrics: { linting, builds, tests },
|
|
|
|
} = await this.updatePushHealth();
|
|
|
|
const defaultTabIndex = [linting, builds, tests].findIndex(
|
|
|
|
(metric) => metric.result === 'fail',
|
2020-01-08 02:24:02 +03:00
|
|
|
);
|
2020-03-10 02:32:03 +03:00
|
|
|
const repos = await RepositoryModel.getList();
|
2020-04-30 22:40:38 +03:00
|
|
|
const currentRepo = repos.find((repoObj) => repoObj.name === repo);
|
2020-03-10 02:32:03 +03:00
|
|
|
|
2020-07-08 06:30:39 +03:00
|
|
|
this.setState({ defaultTabIndex, currentRepo });
|
2019-01-24 19:15:46 +03:00
|
|
|
|
|
|
|
// Update the tests every two minutes.
|
|
|
|
this.testTimerId = setInterval(() => this.updatePushHealth(), 120000);
|
2019-06-07 00:39:50 +03:00
|
|
|
this.notificationsId = setInterval(() => {
|
|
|
|
const { notifications } = this.state;
|
|
|
|
|
|
|
|
this.setState(clearExpiredTransientNotifications(notifications));
|
|
|
|
}, 4000);
|
2019-01-24 19:15:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
clearInterval(this.testTimerId);
|
|
|
|
}
|
|
|
|
|
2020-04-30 22:40:38 +03:00
|
|
|
setUser = (user) => {
|
2019-01-24 19:15:46 +03:00
|
|
|
this.setState({ user });
|
|
|
|
};
|
|
|
|
|
2019-02-01 20:58:20 +03:00
|
|
|
updatePushHealth = async () => {
|
|
|
|
const { repo, revision } = this.state;
|
|
|
|
const { data, failureStatus } = await PushModel.getHealth(repo, revision);
|
2019-11-22 21:19:04 +03:00
|
|
|
const newState = !failureStatus ? data : { failureMessage: data };
|
2019-02-01 20:58:20 +03:00
|
|
|
|
|
|
|
this.setState(newState);
|
2020-01-08 02:24:02 +03:00
|
|
|
return newState;
|
2019-01-24 19:15:46 +03:00
|
|
|
};
|
|
|
|
|
2019-06-07 00:39:50 +03:00
|
|
|
notify = (message, severity, options = {}) => {
|
|
|
|
const { notifications } = this.state;
|
|
|
|
const notification = {
|
|
|
|
...options,
|
|
|
|
message,
|
2020-02-27 21:01:46 +03:00
|
|
|
severity: severity || 'darker-info',
|
2019-06-07 00:39:50 +03:00
|
|
|
created: Date.now(),
|
|
|
|
};
|
|
|
|
const newNotifications = [notification, ...notifications];
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
notifications: newNotifications,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-04-30 22:40:38 +03:00
|
|
|
clearNotification = (index) => {
|
2019-06-07 00:39:50 +03:00
|
|
|
const { notifications } = this.state;
|
|
|
|
|
|
|
|
this.setState(clearNotificationAtIndex(notifications, index));
|
|
|
|
};
|
|
|
|
|
2020-02-27 21:01:46 +03:00
|
|
|
setExpanded = (metricName, expanded) => {
|
|
|
|
const root = camelCase(metricName);
|
|
|
|
const key = `${root}Expanded`;
|
|
|
|
const { [key]: oldExpanded } = this.state;
|
2020-01-08 02:24:02 +03:00
|
|
|
|
2020-02-27 21:01:46 +03:00
|
|
|
if (oldExpanded !== expanded) {
|
|
|
|
this.setState({
|
|
|
|
[key]: expanded,
|
|
|
|
});
|
|
|
|
} else if (expanded) {
|
|
|
|
scrollToLine(`#${root}Metric`, 0, 0, {
|
|
|
|
behavior: 'smooth',
|
|
|
|
block: 'center',
|
|
|
|
});
|
|
|
|
}
|
2020-01-08 02:24:02 +03:00
|
|
|
};
|
|
|
|
|
2020-04-30 22:40:38 +03:00
|
|
|
filter = (searchStr) => {
|
2020-01-23 19:59:53 +03:00
|
|
|
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 });
|
|
|
|
};
|
|
|
|
|
2020-07-08 06:30:39 +03:00
|
|
|
getIcon = (result) => {
|
|
|
|
switch (result) {
|
|
|
|
case 'pass':
|
|
|
|
return faCheck;
|
|
|
|
case 'fail':
|
|
|
|
return faExclamationTriangle;
|
|
|
|
}
|
|
|
|
return faClock;
|
|
|
|
};
|
|
|
|
|
2019-01-24 19:15:46 +03:00
|
|
|
render() {
|
2019-06-07 00:39:50 +03:00
|
|
|
const {
|
2019-11-22 21:19:04 +03:00
|
|
|
metrics,
|
|
|
|
result,
|
2019-06-07 00:39:50 +03:00
|
|
|
user,
|
|
|
|
repo,
|
|
|
|
revision,
|
|
|
|
failureMessage,
|
|
|
|
notifications,
|
2019-12-17 00:36:58 +03:00
|
|
|
status,
|
2020-01-23 19:59:53 +03:00
|
|
|
searchStr,
|
2020-03-10 02:32:03 +03:00
|
|
|
currentRepo,
|
2020-03-27 02:59:38 +03:00
|
|
|
showParentMatches,
|
2020-07-08 06:30:39 +03:00
|
|
|
defaultTabIndex,
|
2019-06-07 00:39:50 +03:00
|
|
|
} = this.state;
|
2020-03-18 22:28:36 +03:00
|
|
|
const { tests, commitHistory, linting, builds } = metrics;
|
2019-12-17 00:36:58 +03:00
|
|
|
const percentComplete = status ? getPercentComplete(status) : 0;
|
2020-03-28 04:10:04 +03:00
|
|
|
const needInvestigationCount = tests
|
|
|
|
? tests.details.needInvestigation.length
|
|
|
|
: 0;
|
2019-01-24 19:15:46 +03:00
|
|
|
|
|
|
|
return (
|
2019-06-07 00:39:50 +03:00
|
|
|
<React.Fragment>
|
2020-03-28 04:10:04 +03:00
|
|
|
<Helmet>
|
|
|
|
<link
|
|
|
|
rel="shortcut icon"
|
|
|
|
href={result === 'fail' ? faviconBroken : faviconOk}
|
|
|
|
/>
|
|
|
|
<title>{`[${needInvestigationCount}] Push Health`}</title>
|
|
|
|
</Helmet>
|
2019-12-17 00:36:58 +03:00
|
|
|
<Navigation
|
|
|
|
user={user}
|
|
|
|
setUser={this.setUser}
|
|
|
|
notify={this.notify}
|
|
|
|
result={result}
|
|
|
|
repo={repo}
|
|
|
|
revision={revision}
|
2020-01-08 02:24:02 +03:00
|
|
|
>
|
2020-01-21 21:01:43 +03:00
|
|
|
<Navbar color="light" light expand="sm" className="w-100">
|
2020-01-08 02:24:02 +03:00
|
|
|
{!!tests && (
|
2020-07-08 06:30:39 +03:00
|
|
|
<Nav className="mb-2 pt-2 pl-3 justify-content-between w-100">
|
|
|
|
<span />
|
2020-03-27 02:59:38 +03:00
|
|
|
<span className="mr-2 d-flex">
|
|
|
|
<Button
|
|
|
|
size="sm"
|
|
|
|
className="text-nowrap mr-1"
|
|
|
|
title="Toggle failures that also failed in the parent"
|
|
|
|
onClick={() =>
|
|
|
|
this.setState({ showParentMatches: !showParentMatches })
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{showParentMatches ? 'Hide' : 'Show'} parent matches
|
|
|
|
</Button>
|
2020-01-23 19:59:53 +03:00
|
|
|
<InputFilter
|
|
|
|
updateFilterText={this.filter}
|
|
|
|
placeholder="filter path or platform"
|
|
|
|
/>
|
|
|
|
</span>
|
2020-01-08 02:24:02 +03:00
|
|
|
</Nav>
|
|
|
|
)}
|
|
|
|
</Navbar>
|
|
|
|
</Navigation>
|
2020-07-06 20:40:13 +03:00
|
|
|
<Container fluid className="mt-2 mb-5 max-width-default">
|
2019-06-07 00:39:50 +03:00
|
|
|
<NotificationList
|
|
|
|
notifications={notifications}
|
|
|
|
clearNotification={this.clearNotification}
|
|
|
|
/>
|
2019-11-22 21:19:04 +03:00
|
|
|
{!!tests && !!currentRepo && (
|
2020-07-08 06:30:39 +03:00
|
|
|
<React.Fragment>
|
|
|
|
<div>{percentComplete}% Complete</div>
|
|
|
|
<StatusProgress counts={status} />
|
|
|
|
<div className="mb-3" />
|
2020-03-18 22:28:36 +03:00
|
|
|
{commitHistory.details && (
|
2020-07-08 06:30:39 +03:00
|
|
|
<CommitHistory
|
|
|
|
history={commitHistory.details}
|
2020-01-08 02:24:02 +03:00
|
|
|
revision={revision}
|
|
|
|
currentRepo={currentRepo}
|
2020-07-08 06:30:39 +03:00
|
|
|
compareWithParent={this.compareWithParent}
|
2020-01-08 02:24:02 +03:00
|
|
|
/>
|
2020-07-08 06:30:39 +03:00
|
|
|
)}
|
|
|
|
<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">
|
|
|
|
<Tab className="pb-2 list-inline-item ml-4 pointable">
|
|
|
|
<span className="text-success">
|
|
|
|
<FontAwesomeIcon
|
|
|
|
icon={this.getIcon(linting.result)}
|
|
|
|
className={`mr-1 text-${
|
|
|
|
resultColorMap[linting.result]
|
|
|
|
}`}
|
|
|
|
/>
|
|
|
|
</span>
|
|
|
|
Linting
|
|
|
|
</Tab>
|
|
|
|
<Tab className="list-inline-item ml-4 pointable">
|
|
|
|
<FontAwesomeIcon
|
|
|
|
icon={this.getIcon(builds.result)}
|
|
|
|
className={`mr-1 text-${resultColorMap[builds.result]}`}
|
|
|
|
/>
|
|
|
|
Builds
|
|
|
|
</Tab>
|
|
|
|
<Tab className="list-inline-item ml-4 pointable">
|
|
|
|
<FontAwesomeIcon
|
|
|
|
fill={resultColorMap[tests.result]}
|
|
|
|
icon={this.getIcon(tests.result)}
|
|
|
|
className={`mr-1 text-${resultColorMap[tests.result]}`}
|
|
|
|
/>
|
|
|
|
Tests
|
|
|
|
</Tab>
|
|
|
|
</TabList>
|
|
|
|
<div>
|
|
|
|
<TabPanel>
|
|
|
|
<JobListMetric
|
|
|
|
data={linting}
|
|
|
|
repo={repo}
|
|
|
|
revision={revision}
|
|
|
|
setExpanded={this.setExpanded}
|
|
|
|
showParentMatches={showParentMatches}
|
|
|
|
/>
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel>
|
|
|
|
<JobListMetric
|
|
|
|
data={builds}
|
|
|
|
repo={repo}
|
|
|
|
revision={revision}
|
|
|
|
setExpanded={this.setExpanded}
|
|
|
|
showParentMatches={showParentMatches}
|
|
|
|
/>
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel>
|
|
|
|
<TestMetric
|
|
|
|
data={tests}
|
|
|
|
repo={repo}
|
|
|
|
currentRepo={currentRepo}
|
|
|
|
revision={revision}
|
|
|
|
notify={this.notify}
|
|
|
|
setExpanded={this.setExpanded}
|
|
|
|
searchStr={searchStr}
|
|
|
|
showParentMatches={showParentMatches}
|
|
|
|
/>
|
|
|
|
</TabPanel>
|
|
|
|
</div>
|
|
|
|
</Tabs>
|
|
|
|
</React.Fragment>
|
2019-06-07 00:39:50 +03:00
|
|
|
)}
|
|
|
|
{failureMessage && <ErrorMessages failureMessage={failureMessage} />}
|
2020-03-27 02:59:38 +03:00
|
|
|
{!failureMessage && !tests && (
|
|
|
|
<h4>
|
|
|
|
<Spinner />
|
|
|
|
<span className="ml-2 pb-1">
|
|
|
|
Gathering health data and comparing with parent push...
|
|
|
|
</span>
|
|
|
|
</h4>
|
|
|
|
)}
|
2019-06-07 00:39:50 +03:00
|
|
|
</Container>
|
|
|
|
</React.Fragment>
|
2019-01-24 19:15:46 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Health.propTypes = {
|
2020-04-23 22:19:25 +03:00
|
|
|
location: PropTypes.shape({}).isRequired,
|
2019-01-24 19:15:46 +03:00
|
|
|
};
|