зеркало из https://github.com/mozilla/treeherder.git
357 строки
11 KiB
JavaScript
357 строки
11 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import countBy from 'lodash/countBy';
|
|
import { Button } from 'reactstrap';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import {
|
|
faExclamationCircle,
|
|
faTimes,
|
|
} from '@fortawesome/free-solid-svg-icons';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
import { alertStatusMap, endpoints } from '../perf-helpers/constants';
|
|
import {
|
|
getApiUrl,
|
|
getJobsUrl,
|
|
getPerfCompareBaseSubtestsURL,
|
|
} from '../../helpers/url';
|
|
import { create } from '../../helpers/http';
|
|
import RepositoryModel from '../../models/repository';
|
|
import { displayNumber, getStatus } from '../perf-helpers/helpers';
|
|
import Clipboard from '../../shared/Clipboard';
|
|
import { toMercurialDateStr } from '../../helpers/display';
|
|
|
|
const GraphTooltip = ({
|
|
testData,
|
|
infraAffectedData,
|
|
user,
|
|
updateData,
|
|
projects,
|
|
updateStateParams,
|
|
lockTooltip,
|
|
closeTooltip,
|
|
datum,
|
|
x,
|
|
y,
|
|
windowWidth,
|
|
}) => {
|
|
const testDetails = testData.find(
|
|
(item) => item.signature_id === datum.signature_id,
|
|
);
|
|
|
|
const isDatumAffected = infraAffectedData.has(datum.revision);
|
|
|
|
const flotIndex = testDetails.data.findIndex((item) =>
|
|
datum.dataPointId
|
|
? item.dataPointId === datum.dataPointId
|
|
: item.pushId === datum.pushId,
|
|
);
|
|
const dataPointDetails = testDetails.data[flotIndex];
|
|
|
|
const retriggers = countBy(testDetails.resultSetData, (resultSetId) =>
|
|
resultSetId === datum.pushId ? 'retrigger' : 'original',
|
|
);
|
|
const retriggerNum = retriggers.retrigger - 1;
|
|
const prevFlotDataPointIndex = flotIndex - 1;
|
|
const value = dataPointDetails.y;
|
|
|
|
const v0 =
|
|
prevFlotDataPointIndex !== -1
|
|
? testDetails.data[prevFlotDataPointIndex].y
|
|
: value;
|
|
const deltaValue = value - v0;
|
|
const deltaPercent = value / v0 - 1;
|
|
let alert;
|
|
let alertStatus;
|
|
let isCommonAlert = false;
|
|
let commonAlertStatus;
|
|
|
|
if (dataPointDetails.alertSummary && dataPointDetails.alertSummary.alerts) {
|
|
alert = dataPointDetails.alertSummary.alerts.find(
|
|
(alert) => alert.series_signature.id === testDetails.signature_id,
|
|
);
|
|
}
|
|
|
|
if (datum.commonAlert) {
|
|
isCommonAlert = true;
|
|
}
|
|
|
|
if (alert) {
|
|
alertStatus =
|
|
alert.status === alertStatusMap.acknowledged && testDetails.alertSummary
|
|
? getStatus(testDetails.alertSummary.status)
|
|
: getStatus(alert.status, alertStatusMap);
|
|
} else if (isCommonAlert) {
|
|
commonAlertStatus = getStatus(datum.commonAlert.status);
|
|
}
|
|
|
|
const repositoryName = projects.find(
|
|
(repositoryName) => repositoryName.name === testDetails.repository_name,
|
|
);
|
|
|
|
let prevRevision;
|
|
let prevPushId;
|
|
let pushUrl;
|
|
const originalDataPointIdx = testDetails.data.findIndex(
|
|
(e) => e.revision === dataPointDetails.revision,
|
|
);
|
|
|
|
if (prevFlotDataPointIndex !== -1 && originalDataPointIdx > 0) {
|
|
const prevDataPointIdx = originalDataPointIdx - 1;
|
|
const repoModel = new RepositoryModel(repositoryName);
|
|
|
|
prevRevision = testDetails.data[prevDataPointIdx].revision;
|
|
prevPushId = testDetails.data[prevDataPointIdx].pushId;
|
|
pushUrl = repoModel.getPushLogRangeHref({
|
|
fromchange: prevRevision,
|
|
tochange: dataPointDetails.revision,
|
|
});
|
|
}
|
|
|
|
const jobsUrl = getJobsUrl({
|
|
repo: testDetails.repository_name,
|
|
revision: dataPointDetails.revision,
|
|
selectedJob: dataPointDetails.jobId,
|
|
group_state: 'expanded',
|
|
});
|
|
|
|
const createAlert = async () => {
|
|
let data;
|
|
let failureStatus;
|
|
|
|
({ data, failureStatus } = await create(getApiUrl(endpoints.alertSummary), {
|
|
repository_id: testDetails.projectId,
|
|
framework_id: testDetails.framework_id,
|
|
push_id: dataPointDetails.pushId,
|
|
prev_push_id: prevPushId,
|
|
}));
|
|
|
|
if (failureStatus) {
|
|
return updateStateParams({
|
|
errorMessages: [
|
|
`Failed to create an alert summary for push ${dataPointDetails.push_id}: ${data}`,
|
|
],
|
|
});
|
|
}
|
|
|
|
const newAlertSummaryId = data.alert_summary_id;
|
|
({ data, failureStatus } = await create(getApiUrl(endpoints.alert), {
|
|
summary_id: newAlertSummaryId,
|
|
signature_id: testDetails.signature_id,
|
|
}));
|
|
|
|
if (failureStatus) {
|
|
updateStateParams({
|
|
errorMessages: [
|
|
`Failed to create an alert for alert summary ${newAlertSummaryId}: ${data}`,
|
|
],
|
|
});
|
|
}
|
|
|
|
updateData(
|
|
testDetails.signature_id,
|
|
testDetails.projectId,
|
|
newAlertSummaryId,
|
|
flotIndex,
|
|
);
|
|
};
|
|
|
|
const verticalOffset = 15;
|
|
const horizontalOffset = x >= 1275 && windowWidth <= 1825 ? 100 : 0;
|
|
const centered = {
|
|
x: x - 280 / 2 - horizontalOffset,
|
|
y: y - (186 + verticalOffset),
|
|
};
|
|
|
|
return (
|
|
<foreignObject width="100%" height="100%" x={centered.x} y={centered.y}>
|
|
<div
|
|
className={`graph-tooltip ${lockTooltip ? 'locked' : null}`}
|
|
xmlns="http://www.w3.org/1999/xhtml"
|
|
data-testid="graphTooltip"
|
|
>
|
|
<Button
|
|
outline
|
|
color="secondary"
|
|
className="close mr-3 my-2 ml-2"
|
|
onClick={closeTooltip}
|
|
>
|
|
<FontAwesomeIcon
|
|
className="pointer text-white"
|
|
icon={faTimes}
|
|
size="xs"
|
|
title="close tooltip"
|
|
/>
|
|
</Button>
|
|
<div className="body">
|
|
<div>
|
|
<p data-testid="repoName">({testDetails.repository_name})</p>
|
|
<p className="small" data-testid="platform">
|
|
{testDetails.platform}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p>
|
|
{displayNumber(value)}
|
|
{testDetails.measurementUnit && (
|
|
<span> {testDetails.measurementUnit}</span>
|
|
)}
|
|
<span className="text-muted">
|
|
{testDetails.lowerIsBetter
|
|
? ' (lower is better)'
|
|
: ' (higher is better)'}
|
|
</span>
|
|
</p>
|
|
<p className="small">
|
|
Δ {displayNumber(deltaValue.toFixed(1))} (
|
|
{(100 * deltaPercent).toFixed(1)}%)
|
|
</p>
|
|
{isDatumAffected && (
|
|
<p className="small text-warning">
|
|
Could be affected by infra changes.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<span>
|
|
<a href={pushUrl} target="_blank" rel="noopener noreferrer">
|
|
{dataPointDetails.revision.slice(0, 12)}
|
|
</a>{' '}
|
|
{(dataPointDetails.jobId || prevRevision) && '('}
|
|
{dataPointDetails.jobId && (
|
|
<a href={jobsUrl} target="_blank" rel="noopener noreferrer">
|
|
job
|
|
</a>
|
|
)}
|
|
{dataPointDetails.jobId && prevRevision && ', '}
|
|
{prevRevision && (
|
|
<a
|
|
href={getPerfCompareBaseSubtestsURL(
|
|
testDetails.repository_name,
|
|
prevRevision,
|
|
testDetails.repository_name,
|
|
dataPointDetails.revision,
|
|
testDetails.framework_id,
|
|
testDetails.parentSignature || testDetails.signature_id,
|
|
testDetails.parentSignature || testDetails.signature_id,
|
|
)}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
compare
|
|
</a>
|
|
)}
|
|
{(dataPointDetails.jobId || prevRevision) && ') '}
|
|
<Clipboard
|
|
text={dataPointDetails.revision}
|
|
description="Revision"
|
|
outline
|
|
/>
|
|
</span>
|
|
{dataPointDetails.alertSummary && (
|
|
<p>
|
|
<Link
|
|
to={`./alerts?id=${dataPointDetails.alertSummary.id}`}
|
|
target="_blank"
|
|
>
|
|
<FontAwesomeIcon
|
|
className="text-warning"
|
|
icon={faExclamationCircle}
|
|
size="sm"
|
|
/>
|
|
{` Alert # ${dataPointDetails.alertSummary.id}`}
|
|
</Link>
|
|
<span className="text-muted">
|
|
{` - ${alertStatus} `}
|
|
{alert && alert.related_summary_id && (
|
|
<span>
|
|
{alert.related_summary_id !==
|
|
dataPointDetails.alertSummary.id
|
|
? 'to'
|
|
: 'from'}
|
|
<Link
|
|
to={`./alerts?id=${alert.related_summary_id}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>{` alert # ${alert.related_summary_id}`}</Link>
|
|
</span>
|
|
)}
|
|
</span>
|
|
<Clipboard
|
|
text={dataPointDetails.alertSummary.id.toString()}
|
|
description="Alert Summary id"
|
|
outline
|
|
/>
|
|
</p>
|
|
)}
|
|
{isCommonAlert && !dataPointDetails.alertSummary && (
|
|
<p>
|
|
<Link
|
|
to={`./alerts?id=${datum.commonAlert.id}`}
|
|
target="_blank"
|
|
>
|
|
<FontAwesomeIcon
|
|
className="text-warning"
|
|
icon={faExclamationCircle}
|
|
size="sm"
|
|
/>
|
|
{` Alert # ${datum.commonAlert.id}`}
|
|
</Link>
|
|
<span className="text-muted">{` - ${commonAlertStatus} `}</span>
|
|
<Clipboard
|
|
text={datum.commonAlert.id.toString()}
|
|
description="Alert Summary id"
|
|
outline
|
|
/>
|
|
<p className="small text-danger">Common alert</p>
|
|
</p>
|
|
)}
|
|
{!dataPointDetails.alertSummary && prevPushId && (
|
|
<p className="pt-2">
|
|
{user.isStaff ? (
|
|
<Button
|
|
color="darker-info"
|
|
outline
|
|
size="sm"
|
|
onClick={createAlert}
|
|
>
|
|
create alert
|
|
</Button>
|
|
) : (
|
|
<span>(log in as a a sheriff to create)</span>
|
|
)}
|
|
</p>
|
|
)}
|
|
<p className="small text-white pt-2">
|
|
{toMercurialDateStr(dataPointDetails.x)}
|
|
</p>
|
|
{Boolean(retriggerNum) && (
|
|
<p className="small">{`Retriggers: ${retriggerNum}`}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="tip"
|
|
style={{ transform: `translateX(${horizontalOffset}px)` }}
|
|
/>
|
|
</div>
|
|
</foreignObject>
|
|
);
|
|
};
|
|
|
|
GraphTooltip.propTypes = {
|
|
dataPoint: PropTypes.shape({}),
|
|
testData: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
|
user: PropTypes.shape({}).isRequired,
|
|
updateData: PropTypes.func.isRequired,
|
|
projects: PropTypes.arrayOf(PropTypes.shape({})),
|
|
};
|
|
|
|
GraphTooltip.defaultProps = {
|
|
projects: [],
|
|
dataPoint: undefined,
|
|
};
|
|
|
|
export default GraphTooltip;
|