metricChart.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. import {Fragment, PureComponent} from 'react';
  2. import type {WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import color from 'color';
  5. import type {LineSeriesOption} from 'echarts';
  6. import moment from 'moment-timezone';
  7. import type {Client} from 'sentry/api';
  8. import Feature from 'sentry/components/acl/feature';
  9. import {OnDemandMetricAlert} from 'sentry/components/alerts/onDemandMetricAlert';
  10. import {Button} from 'sentry/components/button';
  11. import type {AreaChartSeries} from 'sentry/components/charts/areaChart';
  12. import {AreaChart} from 'sentry/components/charts/areaChart';
  13. import ChartZoom from 'sentry/components/charts/chartZoom';
  14. import MarkArea from 'sentry/components/charts/components/markArea';
  15. import MarkLine from 'sentry/components/charts/components/markLine';
  16. import EventsRequest from 'sentry/components/charts/eventsRequest';
  17. import LineSeries from 'sentry/components/charts/series/lineSeries';
  18. import SessionsRequest from 'sentry/components/charts/sessionsRequest';
  19. import {
  20. ChartControls,
  21. HeaderTitleLegend,
  22. InlineContainer,
  23. SectionHeading,
  24. SectionValue,
  25. } from 'sentry/components/charts/styles';
  26. import {isEmptySeries} from 'sentry/components/charts/utils';
  27. import CircleIndicator from 'sentry/components/circleIndicator';
  28. import type {StatsPeriodType} from 'sentry/components/organizations/pageFilters/parse';
  29. import {parseStatsPeriod} from 'sentry/components/organizations/pageFilters/parse';
  30. import Panel from 'sentry/components/panels/panel';
  31. import PanelBody from 'sentry/components/panels/panelBody';
  32. import Placeholder from 'sentry/components/placeholder';
  33. import {Tooltip} from 'sentry/components/tooltip';
  34. import {IconCheckmark, IconClock, IconFire, IconWarning} from 'sentry/icons';
  35. import {t} from 'sentry/locale';
  36. import ConfigStore from 'sentry/stores/configStore';
  37. import {space} from 'sentry/styles/space';
  38. import {ActivationConditionType, MonitorType} from 'sentry/types/alerts';
  39. import type {DateString} from 'sentry/types/core';
  40. import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
  41. import type {Organization} from 'sentry/types/organization';
  42. import type {Project} from 'sentry/types/project';
  43. import toArray from 'sentry/utils/array/toArray';
  44. import {browserHistory} from 'sentry/utils/browserHistory';
  45. import {getUtcDateString} from 'sentry/utils/dates';
  46. import {DiscoverDatasets, SavedQueryDatasets} from 'sentry/utils/discover/types';
  47. import getDuration from 'sentry/utils/duration/getDuration';
  48. import getDynamicText from 'sentry/utils/getDynamicText';
  49. import {getForceMetricsLayerQueryExtras} from 'sentry/utils/metrics/features';
  50. import {shouldShowOnDemandMetricAlertUI} from 'sentry/utils/onDemandMetrics/features';
  51. import {MINUTES_THRESHOLD_TO_DISPLAY_SECONDS} from 'sentry/utils/sessions';
  52. import {capitalize} from 'sentry/utils/string/capitalize';
  53. import theme from 'sentry/utils/theme';
  54. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  55. // eslint-disable-next-line no-restricted-imports
  56. import withSentryRouter from 'sentry/utils/withSentryRouter';
  57. import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
  58. import {makeDefaultCta} from 'sentry/views/alerts/rules/metric/metricRulePresets';
  59. import type {MetricRule} from 'sentry/views/alerts/rules/metric/types';
  60. import {
  61. AlertRuleTriggerType,
  62. Dataset,
  63. TimePeriod,
  64. } from 'sentry/views/alerts/rules/metric/types';
  65. import {getChangeStatus} from 'sentry/views/alerts/utils/getChangeStatus';
  66. import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
  67. import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
  68. import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
  69. import type {Incident} from '../../../types';
  70. import {
  71. alertDetailsLink,
  72. alertTooltipValueFormatter,
  73. isSessionAggregate,
  74. SESSION_AGGREGATE_TO_FIELD,
  75. } from '../../../utils';
  76. import {getMetricDatasetQueryExtras} from '../utils/getMetricDatasetQueryExtras';
  77. import {isCrashFreeAlert} from '../utils/isCrashFreeAlert';
  78. import type {TimePeriodType} from './constants';
  79. import {
  80. getMetricAlertChartOption,
  81. transformSessionResponseToSeries,
  82. } from './metricChartOption';
  83. type Props = WithRouterProps & {
  84. api: Client;
  85. filter: string[] | null;
  86. interval: string;
  87. organization: Organization;
  88. project: Project;
  89. query: string;
  90. rule: MetricRule;
  91. timePeriod: TimePeriodType;
  92. formattedAggregate?: string;
  93. incidents?: Incident[];
  94. isOnDemandAlert?: boolean;
  95. selectedIncident?: Incident | null;
  96. };
  97. type State = Record<string, never>;
  98. function formatTooltipDate(date: moment.MomentInput, format: string): string {
  99. const {
  100. options: {timezone},
  101. } = ConfigStore.get('user');
  102. return moment.tz(date, timezone).format(format);
  103. }
  104. function getRuleChangeSeries(
  105. rule: MetricRule,
  106. data: AreaChartSeries[]
  107. ): LineSeriesOption[] {
  108. const {dateModified} = rule;
  109. if (!data.length || !data[0].data.length || !dateModified) {
  110. return [];
  111. }
  112. const seriesData = data[0].data;
  113. const seriesStart = new Date(seriesData[0].name).getTime();
  114. const ruleChanged = new Date(dateModified).getTime();
  115. if (ruleChanged < seriesStart) {
  116. return [];
  117. }
  118. return [
  119. {
  120. type: 'line',
  121. markLine: MarkLine({
  122. silent: true,
  123. animation: false,
  124. lineStyle: {color: theme.gray200, type: 'solid', width: 1},
  125. data: [{xAxis: ruleChanged}],
  126. label: {
  127. show: false,
  128. },
  129. }),
  130. markArea: MarkArea({
  131. silent: true,
  132. itemStyle: {
  133. color: color(theme.gray100).alpha(0.42).rgb().string(),
  134. },
  135. data: [[{xAxis: seriesStart}, {xAxis: ruleChanged}]],
  136. }),
  137. data: [],
  138. },
  139. ];
  140. }
  141. function shouldUseErrorsDataset(dataset: Dataset, query: string): boolean {
  142. return dataset === Dataset.ERRORS && /\bis:unresolved\b/.test(query);
  143. }
  144. class MetricChart extends PureComponent<Props, State> {
  145. ref: null | ReactEchartsRef = null;
  146. handleZoom = (start: DateString, end: DateString) => {
  147. const {location} = this.props;
  148. browserHistory.push({
  149. pathname: location.pathname,
  150. query: {
  151. start,
  152. end,
  153. },
  154. });
  155. };
  156. renderChartActions(
  157. totalDuration: number,
  158. criticalDuration: number,
  159. warningDuration: number,
  160. waitingForDataDuration: number
  161. ) {
  162. const {rule, organization, project, timePeriod, query} = this.props;
  163. let dataset: DiscoverDatasets | undefined = undefined;
  164. if (shouldUseErrorsDataset(rule.dataset, query)) {
  165. dataset = DiscoverDatasets.ERRORS;
  166. }
  167. let openInDiscoverDataset: SavedQueryDatasets | undefined = undefined;
  168. if (hasDatasetSelector(organization)) {
  169. if (rule.dataset === Dataset.ERRORS) {
  170. openInDiscoverDataset = SavedQueryDatasets.ERRORS;
  171. } else if (
  172. rule.dataset === Dataset.TRANSACTIONS ||
  173. rule.dataset === Dataset.GENERIC_METRICS
  174. ) {
  175. openInDiscoverDataset = SavedQueryDatasets.TRANSACTIONS;
  176. }
  177. }
  178. const {buttonText, ...props} = makeDefaultCta({
  179. organization,
  180. projects: [project],
  181. rule,
  182. timePeriod,
  183. query,
  184. dataset,
  185. openInDiscoverDataset,
  186. });
  187. const resolvedPercent =
  188. (100 *
  189. Math.max(
  190. totalDuration - waitingForDataDuration - criticalDuration - warningDuration,
  191. 0
  192. )) /
  193. totalDuration;
  194. const criticalPercent = 100 * Math.min(criticalDuration / totalDuration, 1);
  195. const warningPercent = 100 * Math.min(warningDuration / totalDuration, 1);
  196. const waitingForDataPercent =
  197. 100 *
  198. Math.min(
  199. (waitingForDataDuration - criticalDuration - warningDuration) / totalDuration,
  200. 1
  201. );
  202. return (
  203. <StyledChartControls>
  204. <StyledInlineContainer>
  205. <Fragment>
  206. <SectionHeading>{t('Summary')}</SectionHeading>
  207. <StyledSectionValue>
  208. <ValueItem>
  209. <IconCheckmark color="successText" isCircled />
  210. {resolvedPercent ? resolvedPercent.toFixed(2) : 0}%
  211. </ValueItem>
  212. <ValueItem>
  213. <IconWarning color="warningText" />
  214. {warningPercent ? warningPercent.toFixed(2) : 0}%
  215. </ValueItem>
  216. <ValueItem>
  217. <IconFire color="errorText" />
  218. {criticalPercent ? criticalPercent.toFixed(2) : 0}%
  219. </ValueItem>
  220. {waitingForDataPercent > 0 && (
  221. <StyledTooltip
  222. underlineColor="gray200"
  223. showUnderline
  224. title={t(
  225. 'The time spent waiting for metrics matching the filters used.'
  226. )}
  227. >
  228. <ValueItem>
  229. <IconClock />
  230. {waitingForDataPercent.toFixed(2)}%
  231. </ValueItem>
  232. </StyledTooltip>
  233. )}
  234. </StyledSectionValue>
  235. </Fragment>
  236. </StyledInlineContainer>
  237. {!isSessionAggregate(rule.aggregate) && (
  238. <Feature features="discover-basic">
  239. <Button size="sm" {...props}>
  240. {buttonText}
  241. </Button>
  242. </Feature>
  243. )}
  244. </StyledChartControls>
  245. );
  246. }
  247. renderChart(
  248. loading: boolean,
  249. timeseriesData?: Series[],
  250. minutesThresholdToDisplaySeconds?: number,
  251. comparisonTimeseriesData?: Series[]
  252. ) {
  253. const {
  254. router,
  255. selectedIncident,
  256. interval,
  257. filter,
  258. incidents,
  259. rule,
  260. organization,
  261. timePeriod: {start, end},
  262. formattedAggregate,
  263. } = this.props;
  264. const {dateModified, timeWindow} = rule;
  265. if (loading || !timeseriesData) {
  266. return this.renderEmpty();
  267. }
  268. const handleIncidentClick = (incident: Incident) => {
  269. router.push(
  270. normalizeUrl({
  271. pathname: alertDetailsLink(organization, incident),
  272. query: {alert: incident.identifier},
  273. })
  274. );
  275. };
  276. const {
  277. criticalDuration,
  278. warningDuration,
  279. totalDuration,
  280. waitingForDataDuration,
  281. chartOption,
  282. } = getMetricAlertChartOption({
  283. timeseriesData,
  284. rule,
  285. seriesName: formattedAggregate,
  286. incidents,
  287. selectedIncident,
  288. showWaitingForData:
  289. shouldShowOnDemandMetricAlertUI(organization) && this.props.isOnDemandAlert,
  290. handleIncidentClick,
  291. });
  292. const comparisonSeriesName = capitalize(
  293. COMPARISON_DELTA_OPTIONS.find(({value}) => value === rule.comparisonDelta)?.label ||
  294. ''
  295. );
  296. const additionalSeries: LineSeriesOption[] = [
  297. ...(comparisonTimeseriesData || []).map(({data: _data, ...otherSeriesProps}) =>
  298. LineSeries({
  299. name: comparisonSeriesName,
  300. data: _data.map(({name, value}) => [name, value]),
  301. lineStyle: {color: theme.gray200, type: 'dashed', width: 1},
  302. itemStyle: {color: theme.gray200},
  303. animation: false,
  304. animationThreshold: 1,
  305. animationDuration: 0,
  306. ...otherSeriesProps,
  307. })
  308. ),
  309. ...getRuleChangeSeries(rule, timeseriesData),
  310. ];
  311. const queryFilter =
  312. filter?.join(' ') + t(' over ') + getDuration(rule.timeWindow * 60);
  313. return (
  314. <ChartPanel>
  315. <StyledPanelBody withPadding>
  316. <ChartHeader>
  317. <HeaderTitleLegend>
  318. {AlertWizardAlertNames[getAlertTypeFromAggregateDataset(rule)]}
  319. </HeaderTitleLegend>
  320. </ChartHeader>
  321. <ChartFilters>
  322. <StyledCircleIndicator size={8} />
  323. <Filters>{formattedAggregate ?? rule.aggregate}</Filters>
  324. <Tooltip
  325. title={queryFilter}
  326. isHoverable
  327. skipWrapper
  328. overlayStyle={{maxWidth: '90vw', lineBreak: 'anywhere', textAlign: 'left'}}
  329. showOnlyOnOverflow
  330. >
  331. <QueryFilters>{queryFilter}</QueryFilters>
  332. </Tooltip>
  333. </ChartFilters>
  334. {getDynamicText({
  335. value: (
  336. <ChartZoom
  337. router={router}
  338. start={start}
  339. end={end}
  340. onZoom={zoomArgs => this.handleZoom(zoomArgs.start, zoomArgs.end)}
  341. >
  342. {zoomRenderProps => (
  343. <AreaChart
  344. {...zoomRenderProps}
  345. {...chartOption}
  346. showTimeInTooltip
  347. minutesThresholdToDisplaySeconds={minutesThresholdToDisplaySeconds}
  348. additionalSeries={additionalSeries}
  349. tooltip={{
  350. formatter: seriesParams => {
  351. // seriesParams can be object instead of array
  352. const pointSeries = toArray(seriesParams);
  353. const {marker, data: pointData} = pointSeries[0];
  354. const seriesName =
  355. formattedAggregate ?? pointSeries[0].seriesName ?? '';
  356. const [pointX, pointY] = pointData as [number, number];
  357. const pointYFormatted = alertTooltipValueFormatter(
  358. pointY,
  359. seriesName,
  360. rule.aggregate
  361. );
  362. const isModified =
  363. dateModified && pointX <= new Date(dateModified).getTime();
  364. const startTime = formatTooltipDate(moment(pointX), 'MMM D LT');
  365. const {period, periodLength} = parseStatsPeriod(interval) ?? {
  366. periodLength: 'm',
  367. period: `${timeWindow}`,
  368. };
  369. const endTime = formatTooltipDate(
  370. moment(pointX).add(
  371. parseInt(period, 10),
  372. periodLength as StatsPeriodType
  373. ),
  374. 'MMM D LT'
  375. );
  376. const comparisonSeries =
  377. pointSeries.length > 1
  378. ? pointSeries.find(
  379. ({seriesName: _sn}) => _sn === comparisonSeriesName
  380. )
  381. : undefined;
  382. const comparisonPointY = comparisonSeries?.data[1] as
  383. | number
  384. | undefined;
  385. const comparisonPointYFormatted =
  386. comparisonPointY !== undefined
  387. ? alertTooltipValueFormatter(
  388. comparisonPointY,
  389. seriesName,
  390. rule.aggregate
  391. )
  392. : undefined;
  393. const changePercentage =
  394. comparisonPointY === undefined
  395. ? NaN
  396. : ((pointY - comparisonPointY) * 100) / comparisonPointY;
  397. const changeStatus = getChangeStatus(
  398. changePercentage,
  399. rule.thresholdType,
  400. rule.triggers
  401. );
  402. const changeStatusColor =
  403. changeStatus === AlertRuleTriggerType.CRITICAL
  404. ? theme.red300
  405. : changeStatus === AlertRuleTriggerType.WARNING
  406. ? theme.yellow300
  407. : theme.green300;
  408. return [
  409. `<div class="tooltip-series">`,
  410. isModified &&
  411. `<div><span class="tooltip-label"><strong>${t(
  412. 'Alert Rule Modified'
  413. )}</strong></span></div>`,
  414. `<div><span class="tooltip-label">${marker} <strong>${seriesName}</strong></span>${pointYFormatted}</div>`,
  415. comparisonSeries &&
  416. `<div><span class="tooltip-label">${comparisonSeries.marker} <strong>${comparisonSeriesName}</strong></span>${comparisonPointYFormatted}</div>`,
  417. `</div>`,
  418. `<div class="tooltip-footer">`,
  419. `<span>${startTime} &mdash; ${endTime}</span>`,
  420. comparisonPointY !== undefined &&
  421. Math.abs(changePercentage) !== Infinity &&
  422. !isNaN(changePercentage) &&
  423. `<span style="color:${changeStatusColor};margin-left:10px;">${
  424. Math.sign(changePercentage) === 1 ? '+' : '-'
  425. }${Math.abs(changePercentage).toFixed(2)}%</span>`,
  426. `</div>`,
  427. '<div class="tooltip-arrow"></div>',
  428. ]
  429. .filter(e => e)
  430. .join('');
  431. },
  432. }}
  433. />
  434. )}
  435. </ChartZoom>
  436. ),
  437. fixed: <Placeholder height="200px" testId="skeleton-ui" />,
  438. })}
  439. </StyledPanelBody>
  440. {this.renderChartActions(
  441. totalDuration,
  442. criticalDuration,
  443. warningDuration,
  444. waitingForDataDuration
  445. )}
  446. </ChartPanel>
  447. );
  448. }
  449. renderEmptyOnDemandAlert(
  450. organization: Organization,
  451. timeseriesData: Series[] = [],
  452. loading?: boolean
  453. ) {
  454. if (
  455. loading ||
  456. !this.props.isOnDemandAlert ||
  457. !shouldShowOnDemandMetricAlertUI(organization) ||
  458. !isEmptySeries(timeseriesData[0])
  459. ) {
  460. return null;
  461. }
  462. return (
  463. <OnDemandMetricAlert
  464. dismissable
  465. message={t(
  466. 'This alert lacks historical data due to filters for which we don’t routinely extract metrics.'
  467. )}
  468. />
  469. );
  470. }
  471. renderEmpty(placeholderText = '') {
  472. return (
  473. <ChartPanel>
  474. <PanelBody withPadding>
  475. <TriggerChartPlaceholder>{placeholderText}</TriggerChartPlaceholder>
  476. </PanelBody>
  477. </ChartPanel>
  478. );
  479. }
  480. render() {
  481. const {
  482. api,
  483. rule,
  484. organization,
  485. timePeriod,
  486. project,
  487. interval,
  488. query,
  489. location,
  490. isOnDemandAlert,
  491. selectedIncident,
  492. } = this.props;
  493. const {aggregate, timeWindow, environment, dataset} = rule;
  494. // Fix for 7 days * 1m interval being over the max number of results from events api
  495. // 10k events is the current max
  496. if (
  497. timePeriod.usingPeriod &&
  498. timePeriod.period === TimePeriod.SEVEN_DAYS &&
  499. interval === '1m'
  500. ) {
  501. timePeriod.start = getUtcDateString(
  502. // -5 minutes provides a small cushion for rounding up minutes. This might be able to be smaller
  503. moment(moment.utc(timePeriod.end).subtract(10000 - 5, 'minutes'))
  504. );
  505. }
  506. // If the chart duration isn't as long as the rollup duration the events-stats
  507. // endpoint will return an invalid timeseriesData dataset
  508. const viableStartDate = getUtcDateString(
  509. moment.min(
  510. moment.utc(timePeriod.start),
  511. moment.utc(timePeriod.end).subtract(timeWindow, 'minutes')
  512. )
  513. );
  514. const viableEndDate = getUtcDateString(
  515. moment.utc(timePeriod.end).add(timeWindow, 'minutes')
  516. );
  517. let activationFilter = '';
  518. if (
  519. rule.monitorType === MonitorType.ACTIVATED &&
  520. selectedIncident &&
  521. selectedIncident.activation
  522. ) {
  523. const {activation} = selectedIncident;
  524. const {activator, conditionType} = activation;
  525. switch (conditionType) {
  526. case String(ActivationConditionType.RELEASE_CREATION):
  527. activationFilter = ` AND (release:${activator})`;
  528. break;
  529. case String(ActivationConditionType.DEPLOY_CREATION):
  530. activationFilter = ` AND (deploy:${activator})`;
  531. break;
  532. default:
  533. break;
  534. }
  535. }
  536. const queryExtras: Record<string, string> = {
  537. ...getMetricDatasetQueryExtras({
  538. organization,
  539. location,
  540. dataset,
  541. newAlertOrQuery: false,
  542. useOnDemandMetrics: isOnDemandAlert,
  543. }),
  544. ...getForceMetricsLayerQueryExtras(organization, dataset),
  545. };
  546. if (shouldUseErrorsDataset(dataset, query)) {
  547. queryExtras.dataset = 'errors';
  548. }
  549. return isCrashFreeAlert(dataset) ? (
  550. <SessionsRequest
  551. api={api}
  552. organization={organization}
  553. project={project.id ? [Number(project.id)] : []}
  554. environment={environment ? [environment] : undefined}
  555. start={viableStartDate}
  556. end={viableEndDate}
  557. query={query + activationFilter}
  558. interval={interval}
  559. field={SESSION_AGGREGATE_TO_FIELD[aggregate]}
  560. groupBy={['session.status']}
  561. >
  562. {({loading, response}) =>
  563. this.renderChart(
  564. loading,
  565. transformSessionResponseToSeries(response, rule),
  566. MINUTES_THRESHOLD_TO_DISPLAY_SECONDS
  567. )
  568. }
  569. </SessionsRequest>
  570. ) : (
  571. <EventsRequest
  572. api={api}
  573. organization={organization}
  574. query={query + activationFilter}
  575. environment={environment ? [environment] : undefined}
  576. project={project.id ? [Number(project.id)] : []}
  577. interval={interval}
  578. comparisonDelta={rule.comparisonDelta ? rule.comparisonDelta * 60 : undefined}
  579. start={viableStartDate}
  580. end={viableEndDate}
  581. yAxis={aggregate}
  582. includePrevious={false}
  583. currentSeriesNames={[aggregate]}
  584. partial={false}
  585. queryExtras={queryExtras}
  586. referrer="api.alerts.alert-rule-chart"
  587. useOnDemandMetrics
  588. >
  589. {({loading, timeseriesData, comparisonTimeseriesData}) => (
  590. <Fragment>
  591. {this.renderEmptyOnDemandAlert(organization, timeseriesData, loading)}
  592. {this.renderChart(
  593. loading,
  594. timeseriesData,
  595. undefined,
  596. comparisonTimeseriesData
  597. )}
  598. </Fragment>
  599. )}
  600. </EventsRequest>
  601. );
  602. }
  603. }
  604. export default withSentryRouter(MetricChart);
  605. const ChartPanel = styled(Panel)`
  606. margin-top: ${space(2)};
  607. `;
  608. const ChartHeader = styled('div')`
  609. margin-bottom: ${space(3)};
  610. `;
  611. const StyledChartControls = styled(ChartControls)`
  612. display: flex;
  613. justify-content: space-between;
  614. flex-wrap: wrap;
  615. `;
  616. const StyledInlineContainer = styled(InlineContainer)`
  617. grid-auto-flow: column;
  618. grid-column-gap: ${space(1)};
  619. `;
  620. const StyledCircleIndicator = styled(CircleIndicator)`
  621. background: ${p => p.theme.formText};
  622. height: ${space(1)};
  623. margin-right: ${space(0.5)};
  624. `;
  625. const ChartFilters = styled('div')`
  626. font-size: ${p => p.theme.fontSizeSmall};
  627. font-family: ${p => p.theme.text.family};
  628. color: ${p => p.theme.textColor};
  629. display: inline-grid;
  630. grid-template-columns: max-content max-content auto;
  631. align-items: center;
  632. `;
  633. const Filters = styled('span')`
  634. margin-right: ${space(1)};
  635. `;
  636. const QueryFilters = styled('span')`
  637. min-width: 0px;
  638. ${p => p.theme.overflowEllipsis}
  639. `;
  640. const StyledSectionValue = styled(SectionValue)`
  641. display: grid;
  642. grid-template-columns: repeat(4, auto);
  643. gap: ${space(1.5)};
  644. margin: 0 0 0 ${space(1.5)};
  645. `;
  646. const ValueItem = styled('div')`
  647. display: grid;
  648. grid-template-columns: repeat(2, auto);
  649. gap: ${space(0.5)};
  650. align-items: center;
  651. font-variant-numeric: tabular-nums;
  652. text-underline-offset: ${space(4)};
  653. `;
  654. /* Override padding to make chart appear centered */
  655. const StyledPanelBody = styled(PanelBody)`
  656. padding-right: 6px;
  657. `;
  658. const TriggerChartPlaceholder = styled(Placeholder)`
  659. height: 200px;
  660. text-align: center;
  661. padding: ${space(3)};
  662. `;
  663. const StyledTooltip = styled(Tooltip)`
  664. text-underline-offset: ${space(0.5)} !important;
  665. `;