metricChart.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. import * as React from 'react';
  2. import {withRouter, WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import color from 'color';
  5. import moment from 'moment';
  6. import momentTimezone from 'moment-timezone';
  7. import {Client} from 'app/api';
  8. import Feature from 'app/components/acl/feature';
  9. import Button from 'app/components/button';
  10. import ChartZoom from 'app/components/charts/chartZoom';
  11. import Graphic from 'app/components/charts/components/graphic';
  12. import MarkArea from 'app/components/charts/components/markArea';
  13. import MarkLine from 'app/components/charts/components/markLine';
  14. import EventsRequest from 'app/components/charts/eventsRequest';
  15. import LineChart, {LineChartSeries} from 'app/components/charts/lineChart';
  16. import SessionsRequest from 'app/components/charts/sessionsRequest';
  17. import {SectionHeading} from 'app/components/charts/styles';
  18. import {
  19. parseStatsPeriod,
  20. StatsPeriodType,
  21. } from 'app/components/organizations/globalSelectionHeader/getParams';
  22. import {Panel, PanelBody, PanelFooter} from 'app/components/panels';
  23. import Placeholder from 'app/components/placeholder';
  24. import {IconCheckmark, IconFire, IconWarning} from 'app/icons';
  25. import {t} from 'app/locale';
  26. import ConfigStore from 'app/stores/configStore';
  27. import space from 'app/styles/space';
  28. import {AvatarProject, DateString, Organization, Project} from 'app/types';
  29. import {ReactEchartsRef, Series} from 'app/types/echarts';
  30. import {getUtcDateString} from 'app/utils/dates';
  31. import {getCrashFreeRateSeries} from 'app/utils/sessions';
  32. import theme from 'app/utils/theme';
  33. import {alertDetailsLink} from 'app/views/alerts/details';
  34. import {makeDefaultCta} from 'app/views/alerts/incidentRules/incidentRulePresets';
  35. import {Dataset, IncidentRule} from 'app/views/alerts/incidentRules/types';
  36. import {AlertWizardAlertNames} from 'app/views/alerts/wizard/options';
  37. import {getAlertTypeFromAggregateDataset} from 'app/views/alerts/wizard/utils';
  38. import {Incident, IncidentActivityType, IncidentStatus} from '../../types';
  39. import {
  40. ALERT_CHART_MIN_MAX_BUFFER,
  41. alertAxisFormatter,
  42. alertTooltipValueFormatter,
  43. isSessionAggregate,
  44. SESSION_AGGREGATE_TO_FIELD,
  45. shouldScaleAlertChart,
  46. } from '../../utils';
  47. import {TimePeriodType} from './constants';
  48. const X_AXIS_BOUNDARY_GAP = 20;
  49. const VERTICAL_PADDING = 22;
  50. type Props = WithRouterProps & {
  51. api: Client;
  52. rule: IncidentRule;
  53. incidents?: Incident[];
  54. timePeriod: TimePeriodType;
  55. selectedIncident?: Incident | null;
  56. organization: Organization;
  57. projects: Project[] | AvatarProject[];
  58. interval: string;
  59. filter: React.ReactNode;
  60. query: string;
  61. orgId: string;
  62. handleZoom: (start: DateString, end: DateString) => void;
  63. };
  64. type State = {
  65. width: number;
  66. height: number;
  67. };
  68. function formatTooltipDate(date: moment.MomentInput, format: string): string {
  69. const {
  70. options: {timezone},
  71. } = ConfigStore.get('user');
  72. return momentTimezone.tz(date, timezone).format(format);
  73. }
  74. function createThresholdSeries(lineColor: string, threshold: number): LineChartSeries {
  75. return {
  76. seriesName: 'Threshold Line',
  77. type: 'line',
  78. markLine: MarkLine({
  79. silent: true,
  80. lineStyle: {color: lineColor, type: 'dashed', width: 1},
  81. data: [{yAxis: threshold} as any],
  82. label: {
  83. show: false,
  84. },
  85. }),
  86. data: [],
  87. };
  88. }
  89. function createStatusAreaSeries(
  90. lineColor: string,
  91. startTime: number,
  92. endTime: number,
  93. yPosition: number
  94. ): LineChartSeries {
  95. return {
  96. seriesName: 'Status Area',
  97. type: 'line',
  98. markLine: MarkLine({
  99. silent: true,
  100. lineStyle: {color: lineColor, type: 'solid', width: 4},
  101. data: [[{coord: [startTime, yPosition]}, {coord: [endTime, yPosition]}] as any],
  102. }),
  103. data: [],
  104. };
  105. }
  106. function createIncidentSeries(
  107. router: Props['router'],
  108. organization: Organization,
  109. lineColor: string,
  110. incidentTimestamp: number,
  111. incident: Incident,
  112. dataPoint?: LineChartSeries['data'][0],
  113. seriesName?: string
  114. ) {
  115. const series = {
  116. seriesName: 'Incident Line',
  117. type: 'line',
  118. markLine: MarkLine({
  119. silent: false,
  120. lineStyle: {color: lineColor, type: 'solid'},
  121. data: [
  122. {
  123. xAxis: incidentTimestamp,
  124. onClick: () => {
  125. router.push({
  126. pathname: alertDetailsLink(organization, incident),
  127. query: {alert: incident.identifier},
  128. });
  129. },
  130. },
  131. ] as any,
  132. label: {
  133. show: incident.identifier,
  134. position: 'insideEndBottom',
  135. formatter: incident.identifier,
  136. color: lineColor,
  137. fontSize: 10,
  138. fontFamily: 'Rubik',
  139. } as any,
  140. }),
  141. data: [],
  142. };
  143. // tooltip conflicts with MarkLine types
  144. (series.markLine as any).tooltip = {
  145. trigger: 'item',
  146. alwaysShowContent: true,
  147. formatter: ({value, marker}) => {
  148. const time = formatTooltipDate(moment(value), 'MMM D, YYYY LT');
  149. return [
  150. `<div class="tooltip-series"><div>`,
  151. `<span class="tooltip-label">${marker} <strong>${t('Alert')} #${
  152. incident.identifier
  153. }</strong></span>${seriesName} ${dataPoint?.value?.toLocaleString()}`,
  154. `</div></div>`,
  155. `<div class="tooltip-date">${time}</div>`,
  156. `<div class="tooltip-arrow"></div>`,
  157. ].join('');
  158. },
  159. };
  160. return series;
  161. }
  162. class MetricChart extends React.PureComponent<Props, State> {
  163. state = {
  164. width: -1,
  165. height: -1,
  166. };
  167. ref: null | ReactEchartsRef = null;
  168. /**
  169. * Syncs component state with the chart's width/heights
  170. */
  171. updateDimensions = () => {
  172. const chartRef = this.ref?.getEchartsInstance?.();
  173. if (!chartRef) {
  174. return;
  175. }
  176. const width = chartRef.getWidth();
  177. const height = chartRef.getHeight();
  178. if (width !== this.state.width || height !== this.state.height) {
  179. this.setState({
  180. width,
  181. height,
  182. });
  183. }
  184. };
  185. handleRef = (ref: ReactEchartsRef): void => {
  186. if (ref && !this.ref) {
  187. this.ref = ref;
  188. this.updateDimensions();
  189. }
  190. if (!ref) {
  191. this.ref = null;
  192. }
  193. };
  194. getRuleChangeThresholdElements = (data: LineChartSeries[]): any[] => {
  195. const {height, width} = this.state;
  196. const {dateModified} = this.props.rule || {};
  197. if (!data.length || !data[0].data.length || !dateModified) {
  198. return [];
  199. }
  200. const seriesData = data[0].data;
  201. const seriesStart = moment(seriesData[0].name).valueOf();
  202. const seriesEnd = moment(seriesData[seriesData.length - 1].name).valueOf();
  203. const ruleChanged = moment(dateModified).valueOf();
  204. if (ruleChanged < seriesStart) {
  205. return [];
  206. }
  207. const chartWidth = width - X_AXIS_BOUNDARY_GAP;
  208. const position =
  209. X_AXIS_BOUNDARY_GAP +
  210. Math.round((chartWidth * (ruleChanged - seriesStart)) / (seriesEnd - seriesStart));
  211. return [
  212. {
  213. type: 'line',
  214. draggable: false,
  215. position: [position, 0],
  216. shape: {y1: 0, y2: height - VERTICAL_PADDING, x1: 1, x2: 1},
  217. style: {
  218. stroke: theme.gray200,
  219. },
  220. },
  221. {
  222. type: 'rect',
  223. draggable: false,
  224. position: [X_AXIS_BOUNDARY_GAP, 0],
  225. shape: {
  226. // +1 makes the gray area go midway onto the dashed line above
  227. width: position - X_AXIS_BOUNDARY_GAP + 1,
  228. height: height - VERTICAL_PADDING,
  229. },
  230. style: {
  231. fill: color(theme.gray100).alpha(0.42).rgb().string(),
  232. },
  233. },
  234. ];
  235. };
  236. renderChartActions(
  237. totalDuration: number,
  238. criticalDuration: number,
  239. warningDuration: number
  240. ) {
  241. const {rule, orgId, projects, timePeriod, query} = this.props;
  242. const ctaOpts = {
  243. orgSlug: orgId,
  244. projects: projects as Project[],
  245. rule,
  246. eventType: query,
  247. start: timePeriod.start,
  248. end: timePeriod.end,
  249. };
  250. const {buttonText, ...props} = makeDefaultCta(ctaOpts);
  251. const resolvedPercent = (
  252. (100 * Math.max(totalDuration - criticalDuration - warningDuration, 0)) /
  253. totalDuration
  254. ).toFixed(2);
  255. const criticalPercent = (100 * Math.min(criticalDuration / totalDuration, 1)).toFixed(
  256. 2
  257. );
  258. const warningPercent = (100 * Math.min(warningDuration / totalDuration, 1)).toFixed(
  259. 2
  260. );
  261. return (
  262. <ChartActions>
  263. <ChartSummary>
  264. <SummaryText>{t('SUMMARY')}</SummaryText>
  265. <SummaryStats>
  266. <StatItem>
  267. <IconCheckmark color="green300" isCircled />
  268. <StatCount>{resolvedPercent}%</StatCount>
  269. </StatItem>
  270. <StatItem>
  271. <IconWarning color="yellow300" />
  272. <StatCount>{warningPercent}%</StatCount>
  273. </StatItem>
  274. <StatItem>
  275. <IconFire color="red300" />
  276. <StatCount>{criticalPercent}%</StatCount>
  277. </StatItem>
  278. </SummaryStats>
  279. </ChartSummary>
  280. {!isSessionAggregate(rule.aggregate) && (
  281. <Feature features={['discover-basic']}>
  282. <Button size="small" {...props}>
  283. {buttonText}
  284. </Button>
  285. </Feature>
  286. )}
  287. </ChartActions>
  288. );
  289. }
  290. renderChart(loading: boolean, timeseriesData?: Series[]) {
  291. const {
  292. router,
  293. selectedIncident,
  294. interval,
  295. handleZoom,
  296. filter,
  297. incidents,
  298. rule,
  299. organization,
  300. timePeriod: {start, end},
  301. } = this.props;
  302. const {dateModified, timeWindow, aggregate} = rule;
  303. if (loading || !timeseriesData) {
  304. return this.renderEmpty();
  305. }
  306. const criticalTrigger = rule.triggers.find(({label}) => label === 'critical');
  307. const warningTrigger = rule.triggers.find(({label}) => label === 'warning');
  308. const series: LineChartSeries[] = [...timeseriesData];
  309. const areaSeries: any[] = [];
  310. // Ensure series data appears above incident lines
  311. series[0].z = 100;
  312. const dataArr = timeseriesData[0].data;
  313. const maxSeriesValue = dataArr.reduce(
  314. (currMax, coord) => Math.max(currMax, coord.value),
  315. 0
  316. );
  317. // find the lowest value between chart data points, warning threshold,
  318. // critical threshold and then apply some breathing space
  319. const minChartValue = shouldScaleAlertChart(aggregate)
  320. ? Math.floor(
  321. Math.min(
  322. dataArr.reduce((currMax, coord) => Math.min(currMax, coord.value), Infinity),
  323. typeof warningTrigger?.alertThreshold === 'number'
  324. ? warningTrigger.alertThreshold
  325. : Infinity,
  326. typeof criticalTrigger?.alertThreshold === 'number'
  327. ? criticalTrigger.alertThreshold
  328. : Infinity
  329. ) / ALERT_CHART_MIN_MAX_BUFFER
  330. )
  331. : 0;
  332. const firstPoint = moment(dataArr[0].name).valueOf();
  333. const lastPoint = moment(dataArr[dataArr.length - 1].name).valueOf();
  334. const totalDuration = lastPoint - firstPoint;
  335. let criticalDuration = 0;
  336. let warningDuration = 0;
  337. series.push(
  338. createStatusAreaSeries(theme.green300, firstPoint, lastPoint, minChartValue)
  339. );
  340. if (incidents) {
  341. // select incidents that fall within the graph range
  342. const periodStart = moment.utc(firstPoint);
  343. incidents
  344. .filter(
  345. incident =>
  346. !incident.dateClosed || moment(incident.dateClosed).isAfter(periodStart)
  347. )
  348. .forEach(incident => {
  349. const statusChanges = incident.activities
  350. ?.filter(
  351. ({type, value}) =>
  352. type === IncidentActivityType.STATUS_CHANGE &&
  353. value &&
  354. [`${IncidentStatus.WARNING}`, `${IncidentStatus.CRITICAL}`].includes(
  355. value
  356. )
  357. )
  358. .sort(
  359. (a, b) => moment(a.dateCreated).valueOf() - moment(b.dateCreated).valueOf()
  360. );
  361. const incidentEnd = incident.dateClosed ?? moment().valueOf();
  362. const timeWindowMs = rule.timeWindow * 60 * 1000;
  363. const incidentColor =
  364. warningTrigger &&
  365. statusChanges &&
  366. !statusChanges.find(({value}) => value === `${IncidentStatus.CRITICAL}`)
  367. ? theme.yellow300
  368. : theme.red300;
  369. const incidentStartDate = moment(incident.dateStarted).valueOf();
  370. const incidentCloseDate = incident.dateClosed
  371. ? moment(incident.dateClosed).valueOf()
  372. : lastPoint;
  373. const incidentStartValue = dataArr.find(
  374. point => point.name >= incidentStartDate
  375. );
  376. series.push(
  377. createIncidentSeries(
  378. router,
  379. organization,
  380. incidentColor,
  381. incidentStartDate,
  382. incident,
  383. incidentStartValue,
  384. series[0].seriesName
  385. )
  386. );
  387. const areaStart = Math.max(moment(incident.dateStarted).valueOf(), firstPoint);
  388. const areaEnd = Math.min(
  389. statusChanges?.length && statusChanges[0].dateCreated
  390. ? moment(statusChanges[0].dateCreated).valueOf() - timeWindowMs
  391. : moment(incidentEnd).valueOf(),
  392. lastPoint
  393. );
  394. const areaColor = warningTrigger ? theme.yellow300 : theme.red300;
  395. if (areaEnd > areaStart) {
  396. series.push(
  397. createStatusAreaSeries(areaColor, areaStart, areaEnd, minChartValue)
  398. );
  399. if (areaColor === theme.yellow300) {
  400. warningDuration += Math.abs(areaEnd - areaStart);
  401. } else {
  402. criticalDuration += Math.abs(areaEnd - areaStart);
  403. }
  404. }
  405. statusChanges?.forEach((activity, idx) => {
  406. const statusAreaStart = Math.max(
  407. moment(activity.dateCreated).valueOf() - timeWindowMs,
  408. firstPoint
  409. );
  410. const statusAreaEnd = Math.min(
  411. idx === statusChanges.length - 1
  412. ? moment(incidentEnd).valueOf()
  413. : moment(statusChanges[idx + 1].dateCreated).valueOf() - timeWindowMs,
  414. lastPoint
  415. );
  416. const statusAreaColor =
  417. activity.value === `${IncidentStatus.CRITICAL}`
  418. ? theme.red300
  419. : theme.yellow300;
  420. if (statusAreaEnd > statusAreaStart) {
  421. series.push(
  422. createStatusAreaSeries(
  423. statusAreaColor,
  424. statusAreaStart,
  425. statusAreaEnd,
  426. minChartValue
  427. )
  428. );
  429. if (statusAreaColor === theme.yellow300) {
  430. warningDuration += Math.abs(statusAreaEnd - statusAreaStart);
  431. } else {
  432. criticalDuration += Math.abs(statusAreaEnd - statusAreaStart);
  433. }
  434. }
  435. });
  436. if (selectedIncident && incident.id === selectedIncident.id) {
  437. const selectedIncidentColor =
  438. incidentColor === theme.yellow300 ? theme.yellow100 : theme.red100;
  439. areaSeries.push({
  440. type: 'line',
  441. markArea: MarkArea({
  442. silent: true,
  443. itemStyle: {
  444. color: color(selectedIncidentColor).alpha(0.42).rgb().string(),
  445. },
  446. data: [[{xAxis: incidentStartDate}, {xAxis: incidentCloseDate}]] as any,
  447. }),
  448. data: [],
  449. });
  450. }
  451. });
  452. }
  453. let maxThresholdValue = 0;
  454. if (warningTrigger?.alertThreshold) {
  455. const {alertThreshold} = warningTrigger;
  456. const warningThresholdLine = createThresholdSeries(theme.yellow300, alertThreshold);
  457. series.push(warningThresholdLine);
  458. maxThresholdValue = Math.max(maxThresholdValue, alertThreshold);
  459. }
  460. if (criticalTrigger?.alertThreshold) {
  461. const {alertThreshold} = criticalTrigger;
  462. const criticalThresholdLine = createThresholdSeries(theme.red300, alertThreshold);
  463. series.push(criticalThresholdLine);
  464. maxThresholdValue = Math.max(maxThresholdValue, alertThreshold);
  465. }
  466. return (
  467. <ChartPanel>
  468. <StyledPanelBody withPadding>
  469. <ChartHeader>
  470. <ChartTitle>
  471. {AlertWizardAlertNames[getAlertTypeFromAggregateDataset(rule)]}
  472. </ChartTitle>
  473. {filter}
  474. </ChartHeader>
  475. <ChartZoom
  476. router={router}
  477. start={start}
  478. end={end}
  479. onZoom={zoomArgs => handleZoom(zoomArgs.start, zoomArgs.end)}
  480. >
  481. {zoomRenderProps => (
  482. <LineChart
  483. {...zoomRenderProps}
  484. isGroupedByDate
  485. showTimeInTooltip
  486. forwardedRef={this.handleRef}
  487. grid={{
  488. left: 0,
  489. right: space(2),
  490. top: space(2),
  491. bottom: 0,
  492. }}
  493. yAxis={{
  494. axisLabel: {
  495. formatter: (value: number) =>
  496. alertAxisFormatter(
  497. value,
  498. timeseriesData[0].seriesName,
  499. rule.aggregate
  500. ),
  501. },
  502. max: maxThresholdValue > maxSeriesValue ? maxThresholdValue : undefined,
  503. min: minChartValue || undefined,
  504. }}
  505. series={[...series, ...areaSeries]}
  506. graphic={Graphic({
  507. elements: this.getRuleChangeThresholdElements(timeseriesData),
  508. })}
  509. tooltip={{
  510. formatter: seriesParams => {
  511. // seriesParams can be object instead of array
  512. const pointSeries = Array.isArray(seriesParams)
  513. ? seriesParams
  514. : [seriesParams];
  515. const {marker, data: pointData, seriesName} = pointSeries[0];
  516. const [pointX, pointY] = pointData as [number, number];
  517. const pointYFormatted = alertTooltipValueFormatter(
  518. pointY,
  519. seriesName ?? '',
  520. rule.aggregate
  521. );
  522. const isModified =
  523. dateModified && pointX <= new Date(dateModified).getTime();
  524. const startTime = formatTooltipDate(moment(pointX), 'MMM D LT');
  525. const {period, periodLength} = parseStatsPeriod(interval) ?? {
  526. periodLength: 'm',
  527. period: `${timeWindow}`,
  528. };
  529. const endTime = formatTooltipDate(
  530. moment(pointX).add(
  531. parseInt(period, 10),
  532. periodLength as StatsPeriodType
  533. ),
  534. 'MMM D LT'
  535. );
  536. const title = isModified
  537. ? `<strong>${t('Alert Rule Modified')}</strong>`
  538. : `${marker} <strong>${seriesName}</strong>`;
  539. const value = isModified
  540. ? `${seriesName} ${pointYFormatted}`
  541. : pointYFormatted;
  542. return [
  543. `<div class="tooltip-series"><div>`,
  544. `<span class="tooltip-label">${title}</span>${value}`,
  545. `</div></div>`,
  546. `<div class="tooltip-date">${startTime} &mdash; ${endTime}</div>`,
  547. `<div class="tooltip-arrow"></div>`,
  548. ].join('');
  549. },
  550. }}
  551. onFinished={() => {
  552. // We want to do this whenever the chart finishes re-rendering so that we can update the dimensions of
  553. // any graphics related to the triggers (e.g. the threshold areas + boundaries)
  554. this.updateDimensions();
  555. }}
  556. />
  557. )}
  558. </ChartZoom>
  559. </StyledPanelBody>
  560. {this.renderChartActions(totalDuration, criticalDuration, warningDuration)}
  561. </ChartPanel>
  562. );
  563. }
  564. renderEmpty() {
  565. return (
  566. <ChartPanel>
  567. <PanelBody withPadding>
  568. <Placeholder height="200px" />
  569. </PanelBody>
  570. </ChartPanel>
  571. );
  572. }
  573. render() {
  574. const {api, rule, organization, timePeriod, projects, interval, query} = this.props;
  575. const {aggregate, timeWindow, environment, dataset} = rule;
  576. // If the chart duration isn't as long as the rollup duration the events-stats
  577. // endpoint will return an invalid timeseriesData data set
  578. const viableStartDate = getUtcDateString(
  579. moment.min(
  580. moment.utc(timePeriod.start),
  581. moment.utc(timePeriod.end).subtract(timeWindow, 'minutes')
  582. )
  583. );
  584. const viableEndDate = getUtcDateString(
  585. moment.utc(timePeriod.end).add(timeWindow, 'minutes')
  586. );
  587. return dataset === Dataset.SESSIONS ? (
  588. <SessionsRequest
  589. api={api}
  590. organization={organization}
  591. project={projects.filter(p => p.id).map(p => Number(p.id))}
  592. environment={environment ? [environment] : undefined}
  593. start={viableStartDate}
  594. end={viableEndDate}
  595. query={query}
  596. interval={interval}
  597. field={SESSION_AGGREGATE_TO_FIELD[aggregate]}
  598. groupBy={['session.status']}
  599. >
  600. {({loading, response}) =>
  601. this.renderChart(loading, [
  602. {
  603. seriesName:
  604. AlertWizardAlertNames[
  605. getAlertTypeFromAggregateDataset({aggregate, dataset: Dataset.SESSIONS})
  606. ],
  607. data: getCrashFreeRateSeries(
  608. response?.groups,
  609. response?.intervals,
  610. SESSION_AGGREGATE_TO_FIELD[aggregate]
  611. ),
  612. },
  613. ])
  614. }
  615. </SessionsRequest>
  616. ) : (
  617. <EventsRequest
  618. api={api}
  619. organization={organization}
  620. query={query}
  621. environment={environment ? [environment] : undefined}
  622. project={(projects as Project[])
  623. .filter(p => p && p.slug)
  624. .map(project => Number(project.id))}
  625. interval={interval}
  626. start={viableStartDate}
  627. end={viableEndDate}
  628. yAxis={aggregate}
  629. includePrevious={false}
  630. currentSeriesName={aggregate}
  631. partial={false}
  632. referrer="api.alerts.alert-rule-chart"
  633. >
  634. {({loading, timeseriesData}) => this.renderChart(loading, timeseriesData)}
  635. </EventsRequest>
  636. );
  637. }
  638. }
  639. export default withRouter(MetricChart);
  640. const ChartPanel = styled(Panel)`
  641. margin-top: ${space(2)};
  642. `;
  643. const ChartHeader = styled('div')`
  644. margin-bottom: ${space(3)};
  645. `;
  646. const ChartTitle = styled('header')`
  647. display: flex;
  648. flex-direction: row;
  649. `;
  650. const ChartActions = styled(PanelFooter)`
  651. display: flex;
  652. justify-content: flex-end;
  653. align-items: center;
  654. padding: ${space(1)} 20px;
  655. `;
  656. const ChartSummary = styled('div')`
  657. display: flex;
  658. margin-right: auto;
  659. `;
  660. const SummaryText = styled(SectionHeading)`
  661. flex: 1;
  662. display: flex;
  663. align-items: center;
  664. margin: 0;
  665. font-weight: bold;
  666. font-size: ${p => p.theme.fontSizeSmall};
  667. line-height: 1;
  668. `;
  669. const SummaryStats = styled('div')`
  670. display: flex;
  671. align-items: center;
  672. margin: 0 ${space(2)};
  673. `;
  674. const StatItem = styled('div')`
  675. display: flex;
  676. align-items: center;
  677. margin: 0 ${space(2)} 0 0;
  678. `;
  679. /* Override padding to make chart appear centered */
  680. const StyledPanelBody = styled(PanelBody)`
  681. padding-right: 6px;
  682. `;
  683. const StatCount = styled('span')`
  684. margin-left: ${space(0.5)};
  685. margin-top: ${space(0.25)};
  686. color: ${p => p.theme.textColor};
  687. `;