eventsChart.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import * as React from 'react';
  2. import {InjectedRouter} from 'react-router/lib/Router';
  3. import {withTheme} from '@emotion/react';
  4. import {EChartOption} from 'echarts/lib/echarts';
  5. import {Query} from 'history';
  6. import isEqual from 'lodash/isEqual';
  7. import {Client} from 'app/api';
  8. import AreaChart from 'app/components/charts/areaChart';
  9. import BarChart from 'app/components/charts/barChart';
  10. import ChartZoom, {ZoomRenderProps} from 'app/components/charts/chartZoom';
  11. import ErrorPanel from 'app/components/charts/errorPanel';
  12. import LineChart from 'app/components/charts/lineChart';
  13. import ReleaseSeries from 'app/components/charts/releaseSeries';
  14. import TransitionChart from 'app/components/charts/transitionChart';
  15. import TransparentLoadingMask from 'app/components/charts/transparentLoadingMask';
  16. import {getInterval, RELEASE_LINES_THRESHOLD} from 'app/components/charts/utils';
  17. import {IconWarning} from 'app/icons';
  18. import {t} from 'app/locale';
  19. import {DateString, OrganizationSummary} from 'app/types';
  20. import {Series} from 'app/types/echarts';
  21. import {defined} from 'app/utils';
  22. import {axisLabelFormatter, tooltipFormatter} from 'app/utils/discover/charts';
  23. import {aggregateMultiPlotType, getEquation, isEquation} from 'app/utils/discover/fields';
  24. import {Theme} from 'app/utils/theme';
  25. import EventsRequest from './eventsRequest';
  26. type ChartProps = {
  27. theme: Theme;
  28. loading: boolean;
  29. reloading: boolean;
  30. zoomRenderProps: ZoomRenderProps;
  31. timeseriesData: Series[];
  32. showLegend?: boolean;
  33. legendOptions?: EChartOption.Legend;
  34. chartOptions?: EChartOption;
  35. currentSeriesName?: string;
  36. releaseSeries?: Series[];
  37. previousTimeseriesData?: Series | null;
  38. previousSeriesName?: string;
  39. /**
  40. * A callback to allow for post-processing of the series data.
  41. * Can be used to rename series or even insert a new series.
  42. */
  43. seriesTransformer?: (series: Series[]) => Series[];
  44. showDaily?: boolean;
  45. interval?: string;
  46. yAxis: string;
  47. stacked: boolean;
  48. colors?: string[];
  49. /**
  50. * By default, only the release series is disableable. This adds
  51. * a list of series names that are also disableable.
  52. */
  53. disableableSeries?: string[];
  54. chartComponent?:
  55. | React.ComponentType<BarChart['props']>
  56. | React.ComponentType<AreaChart['props']>
  57. | React.ComponentType<LineChart['props']>;
  58. height?: number;
  59. };
  60. type State = {
  61. seriesSelection: Record<string, boolean>;
  62. forceUpdate: boolean;
  63. };
  64. class Chart extends React.Component<ChartProps, State> {
  65. state: State = {
  66. seriesSelection: {},
  67. forceUpdate: false,
  68. };
  69. shouldComponentUpdate(nextProps: ChartProps, nextState: State) {
  70. if (nextState.forceUpdate) {
  71. return true;
  72. }
  73. if (!isEqual(this.state.seriesSelection, nextState.seriesSelection)) {
  74. return true;
  75. }
  76. if (nextProps.reloading || !nextProps.timeseriesData) {
  77. return false;
  78. }
  79. if (
  80. isEqual(this.props.timeseriesData, nextProps.timeseriesData) &&
  81. isEqual(this.props.releaseSeries, nextProps.releaseSeries) &&
  82. isEqual(this.props.previousTimeseriesData, nextProps.previousTimeseriesData)
  83. ) {
  84. return false;
  85. }
  86. return true;
  87. }
  88. getChartComponent():
  89. | React.ComponentType<BarChart['props']>
  90. | React.ComponentType<AreaChart['props']>
  91. | React.ComponentType<LineChart['props']> {
  92. const {showDaily, timeseriesData, yAxis, chartComponent} = this.props;
  93. if (defined(chartComponent)) {
  94. return chartComponent;
  95. }
  96. if (showDaily) {
  97. return BarChart;
  98. }
  99. if (timeseriesData.length > 1) {
  100. switch (aggregateMultiPlotType(yAxis)) {
  101. case 'line':
  102. return LineChart;
  103. case 'area':
  104. return AreaChart;
  105. default:
  106. throw new Error(`Unknown multi plot type for ${yAxis}`);
  107. }
  108. }
  109. return AreaChart;
  110. }
  111. handleLegendSelectChanged = legendChange => {
  112. const {disableableSeries = []} = this.props;
  113. const {selected} = legendChange;
  114. const seriesSelection = Object.keys(selected).reduce((state, key) => {
  115. // we only want them to be able to disable the Releases series,
  116. // and not any of the other possible series here
  117. const disableable = key === 'Releases' || disableableSeries.includes(key);
  118. state[key] = disableable ? selected[key] : true;
  119. return state;
  120. }, {});
  121. // we have to force an update here otherwise ECharts will
  122. // update its internal state and disable the series
  123. this.setState({seriesSelection, forceUpdate: true}, () =>
  124. this.setState({forceUpdate: false})
  125. );
  126. };
  127. render() {
  128. const {
  129. theme,
  130. loading: _loading,
  131. reloading: _reloading,
  132. yAxis,
  133. releaseSeries,
  134. zoomRenderProps,
  135. timeseriesData,
  136. previousTimeseriesData,
  137. showLegend,
  138. legendOptions,
  139. chartOptions: chartOptionsProp,
  140. currentSeriesName,
  141. previousSeriesName,
  142. seriesTransformer,
  143. colors,
  144. height,
  145. ...props
  146. } = this.props;
  147. const {seriesSelection} = this.state;
  148. const data = [currentSeriesName ?? t('Current'), previousSeriesName ?? t('Previous')];
  149. const releasesLegend = t('Releases');
  150. if (Array.isArray(releaseSeries)) {
  151. data.push(releasesLegend);
  152. }
  153. // Temporary fix to improve performance on pages with a high number of releases.
  154. const releases = releaseSeries && releaseSeries[0];
  155. const hideReleasesByDefault =
  156. Array.isArray(releaseSeries) &&
  157. (releases as any)?.markLine?.data &&
  158. (releases as any).markLine.data.length >= RELEASE_LINES_THRESHOLD;
  159. const selected = !Array.isArray(releaseSeries)
  160. ? seriesSelection
  161. : Object.keys(seriesSelection).length === 0 && hideReleasesByDefault
  162. ? {[releasesLegend]: false}
  163. : seriesSelection;
  164. const legend = showLegend
  165. ? {
  166. right: 16,
  167. top: 12,
  168. data,
  169. selected,
  170. ...(legendOptions ?? {}),
  171. }
  172. : undefined;
  173. let series = Array.isArray(releaseSeries)
  174. ? [...timeseriesData, ...releaseSeries]
  175. : timeseriesData;
  176. if (seriesTransformer) {
  177. series = seriesTransformer(series);
  178. }
  179. const chartOptions = {
  180. colors: timeseriesData.length
  181. ? colors?.slice(0, series.length) ?? [
  182. ...theme.charts.getColorPalette(timeseriesData.length - 2),
  183. ]
  184. : undefined,
  185. grid: {
  186. left: '24px',
  187. right: '24px',
  188. top: '32px',
  189. bottom: '12px',
  190. },
  191. seriesOptions: {
  192. showSymbol: false,
  193. },
  194. tooltip: {
  195. trigger: 'axis' as const,
  196. truncate: 80,
  197. valueFormatter: (value: number) => tooltipFormatter(value, yAxis),
  198. },
  199. yAxis: {
  200. axisLabel: {
  201. color: theme.chartLabel,
  202. formatter: (value: number) => axisLabelFormatter(value, yAxis),
  203. },
  204. },
  205. ...(chartOptionsProp ?? {}),
  206. };
  207. const Component = this.getChartComponent();
  208. return (
  209. <Component
  210. {...props}
  211. {...zoomRenderProps}
  212. {...chartOptions}
  213. legend={legend}
  214. onLegendSelectChanged={this.handleLegendSelectChanged}
  215. series={series}
  216. previousPeriod={previousTimeseriesData ? [previousTimeseriesData] : undefined}
  217. height={height}
  218. />
  219. );
  220. }
  221. }
  222. const ThemedChart = withTheme(Chart);
  223. export type EventsChartProps = {
  224. api: Client;
  225. router: InjectedRouter;
  226. organization: OrganizationSummary;
  227. /**
  228. * Project ids
  229. */
  230. projects: number[];
  231. /**
  232. * Environment condition.
  233. */
  234. environments: string[];
  235. /**
  236. * The discover query string to find events with.
  237. */
  238. query: string;
  239. /**
  240. * The aggregate/metric to plot.
  241. */
  242. yAxis: string;
  243. /**
  244. * Relative datetime expression. eg. 14d
  245. */
  246. period?: string;
  247. /**
  248. * Absolute start date.
  249. */
  250. start: DateString;
  251. /**
  252. * Absolute end date.
  253. */
  254. end: DateString;
  255. /**
  256. * Should datetimes be formatted in UTC?
  257. */
  258. utc?: boolean | null;
  259. /**
  260. * Don't show the previous period's data. Will automatically disable
  261. * when start/end are used.
  262. */
  263. disablePrevious?: boolean;
  264. /**
  265. * Don't show the release marklines.
  266. */
  267. disableReleases?: boolean;
  268. /**
  269. * A list of release names to visually emphasize. Can only be used when `disableReleases` is false.
  270. */
  271. emphasizeReleases?: string[];
  272. /**
  273. * Fetch n top events as dictated by the field and orderby props.
  274. */
  275. topEvents?: number;
  276. /**
  277. * The fields that act as grouping conditions when generating a topEvents chart.
  278. */
  279. field?: string[];
  280. /**
  281. * The interval resolution for a chart e.g. 1m, 5m, 1d
  282. */
  283. interval?: string;
  284. /**
  285. * Order condition when showing topEvents
  286. */
  287. orderby?: string;
  288. /**
  289. * Override the interval calculation and show daily results.
  290. */
  291. showDaily?: boolean;
  292. confirmedQuery?: boolean;
  293. /**
  294. * Override the default color palette.
  295. */
  296. colors?: string[];
  297. /**
  298. * Markup for optional chart header
  299. */
  300. chartHeader?: React.ReactNode;
  301. releaseQueryExtra?: Query;
  302. preserveReleaseQueryParams?: boolean;
  303. /**
  304. * Chart zoom will change 'pageStart' instead of 'start'
  305. */
  306. usePageZoom?: boolean;
  307. } & Pick<
  308. ChartProps,
  309. | 'currentSeriesName'
  310. | 'previousSeriesName'
  311. | 'seriesTransformer'
  312. | 'showLegend'
  313. | 'disableableSeries'
  314. | 'legendOptions'
  315. | 'chartOptions'
  316. | 'chartComponent'
  317. | 'height'
  318. >;
  319. type ChartDataProps = {
  320. zoomRenderProps: ZoomRenderProps;
  321. errored: boolean;
  322. loading: boolean;
  323. reloading: boolean;
  324. results?: Series[];
  325. timeseriesData?: Series[];
  326. previousTimeseriesData?: Series | null;
  327. releaseSeries?: Series[];
  328. };
  329. class EventsChart extends React.Component<EventsChartProps> {
  330. render() {
  331. const {
  332. api,
  333. period,
  334. utc,
  335. query,
  336. router,
  337. start,
  338. end,
  339. projects,
  340. environments,
  341. showLegend,
  342. yAxis,
  343. disablePrevious,
  344. disableReleases,
  345. emphasizeReleases,
  346. currentSeriesName: currentName,
  347. previousSeriesName: previousName,
  348. seriesTransformer,
  349. field,
  350. interval,
  351. showDaily,
  352. topEvents,
  353. orderby,
  354. confirmedQuery,
  355. colors,
  356. chartHeader,
  357. legendOptions,
  358. chartOptions,
  359. preserveReleaseQueryParams,
  360. releaseQueryExtra,
  361. disableableSeries,
  362. chartComponent,
  363. usePageZoom,
  364. height,
  365. ...props
  366. } = this.props;
  367. // Include previous only on relative dates (defaults to relative if no start and end)
  368. const includePrevious = !disablePrevious && !start && !end;
  369. let yAxisLabel = yAxis && isEquation(yAxis) ? getEquation(yAxis) : yAxis;
  370. if (yAxisLabel && yAxisLabel.length > 60) {
  371. yAxisLabel = yAxisLabel.substr(0, 60) + '...';
  372. }
  373. const previousSeriesName =
  374. previousName ?? (yAxisLabel ? t('previous %s', yAxisLabel) : undefined);
  375. const currentSeriesName = currentName ?? yAxisLabel;
  376. const intervalVal = showDaily ? '1d' : interval || getInterval(this.props, true);
  377. let chartImplementation = ({
  378. zoomRenderProps,
  379. releaseSeries,
  380. errored,
  381. loading,
  382. reloading,
  383. results,
  384. timeseriesData,
  385. previousTimeseriesData,
  386. }: ChartDataProps) => {
  387. if (errored) {
  388. return (
  389. <ErrorPanel>
  390. <IconWarning color="gray300" size="lg" />
  391. </ErrorPanel>
  392. );
  393. }
  394. const seriesData = results ? results : timeseriesData;
  395. return (
  396. <TransitionChart
  397. loading={loading}
  398. reloading={reloading}
  399. height={height ? `${height}px` : undefined}
  400. >
  401. <TransparentLoadingMask visible={reloading} />
  402. {React.isValidElement(chartHeader) && chartHeader}
  403. <ThemedChart
  404. zoomRenderProps={zoomRenderProps}
  405. loading={loading}
  406. reloading={reloading}
  407. showLegend={showLegend}
  408. releaseSeries={releaseSeries || []}
  409. timeseriesData={seriesData ?? []}
  410. previousTimeseriesData={previousTimeseriesData}
  411. currentSeriesName={currentSeriesName}
  412. previousSeriesName={previousSeriesName}
  413. seriesTransformer={seriesTransformer}
  414. stacked={typeof topEvents === 'number' && topEvents > 0}
  415. yAxis={yAxis}
  416. showDaily={showDaily}
  417. colors={colors}
  418. legendOptions={legendOptions}
  419. chartOptions={chartOptions}
  420. disableableSeries={disableableSeries}
  421. chartComponent={chartComponent}
  422. height={height}
  423. />
  424. </TransitionChart>
  425. );
  426. };
  427. if (!disableReleases) {
  428. const previousChart = chartImplementation;
  429. chartImplementation = chartProps => (
  430. <ReleaseSeries
  431. utc={utc}
  432. period={period}
  433. start={start}
  434. end={end}
  435. projects={projects}
  436. environments={environments}
  437. emphasizeReleases={emphasizeReleases}
  438. preserveQueryParams={preserveReleaseQueryParams}
  439. queryExtra={releaseQueryExtra}
  440. >
  441. {({releaseSeries}) => previousChart({...chartProps, releaseSeries})}
  442. </ReleaseSeries>
  443. );
  444. }
  445. return (
  446. <ChartZoom
  447. router={router}
  448. period={period}
  449. start={start}
  450. end={end}
  451. utc={utc}
  452. usePageDate={usePageZoom}
  453. {...props}
  454. >
  455. {zoomRenderProps => (
  456. <EventsRequest
  457. {...props}
  458. api={api}
  459. period={period}
  460. project={projects}
  461. environment={environments}
  462. start={start}
  463. end={end}
  464. interval={intervalVal}
  465. query={query}
  466. includePrevious={includePrevious}
  467. currentSeriesName={currentSeriesName}
  468. previousSeriesName={previousSeriesName}
  469. yAxis={yAxis}
  470. field={field}
  471. orderby={orderby}
  472. topEvents={topEvents}
  473. confirmedQuery={confirmedQuery}
  474. partial
  475. >
  476. {eventData =>
  477. chartImplementation({
  478. ...eventData,
  479. zoomRenderProps,
  480. })
  481. }
  482. </EventsRequest>
  483. )}
  484. </ChartZoom>
  485. );
  486. }
  487. }
  488. export default EventsChart;