miniGraph.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import {Component} from 'react';
  2. import {Theme, withTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import isEqual from 'lodash/isEqual';
  6. import {Client} from 'sentry/api';
  7. import {AreaChart, AreaChartProps} from 'sentry/components/charts/areaChart';
  8. import {BarChart, BarChartProps} from 'sentry/components/charts/barChart';
  9. import EventsGeoRequest from 'sentry/components/charts/eventsGeoRequest';
  10. import EventsRequest from 'sentry/components/charts/eventsRequest';
  11. import {LineChart} from 'sentry/components/charts/lineChart';
  12. import {getInterval, processTableResults} from 'sentry/components/charts/utils';
  13. import {WorldMapChart} from 'sentry/components/charts/worldMapChart';
  14. import LoadingContainer from 'sentry/components/loading/loadingContainer';
  15. import LoadingIndicator from 'sentry/components/loadingIndicator';
  16. import {IconWarning} from 'sentry/icons';
  17. import {Organization} from 'sentry/types';
  18. import {Series} from 'sentry/types/echarts';
  19. import {getUtcToLocalDateObject} from 'sentry/utils/dates';
  20. import {axisLabelFormatter} from 'sentry/utils/discover/charts';
  21. import EventView from 'sentry/utils/discover/eventView';
  22. import {aggregateOutputType, PlotType} from 'sentry/utils/discover/fields';
  23. import {DisplayModes, TOP_N} from 'sentry/utils/discover/types';
  24. import {decodeScalar} from 'sentry/utils/queryString';
  25. import withApi from 'sentry/utils/withApi';
  26. type Props = {
  27. api: Client;
  28. eventView: EventView;
  29. location: Location;
  30. organization: Organization;
  31. theme: Theme;
  32. referrer?: string;
  33. yAxis?: string[];
  34. };
  35. class MiniGraph extends Component<Props> {
  36. shouldComponentUpdate(nextProps) {
  37. // We pay for the cost of the deep comparison here since it is cheaper
  38. // than the cost for rendering the graph, which can take ~200ms to ~300ms to
  39. // render.
  40. return !isEqual(this.getRefreshProps(this.props), this.getRefreshProps(nextProps));
  41. }
  42. getRefreshProps(props: Props) {
  43. // get props that are relevant to the API payload for the graph
  44. const {organization, location, eventView, yAxis} = props;
  45. const apiPayload = eventView.getEventsAPIPayload(location);
  46. const query = apiPayload.query;
  47. const start = apiPayload.start ? getUtcToLocalDateObject(apiPayload.start) : null;
  48. const end = apiPayload.end ? getUtcToLocalDateObject(apiPayload.end) : null;
  49. const period: string | undefined = apiPayload.statsPeriod as any;
  50. const display = eventView.getDisplayMode();
  51. const isTopEvents =
  52. display === DisplayModes.TOP5 || display === DisplayModes.DAILYTOP5;
  53. const isDaily = display === DisplayModes.DAILYTOP5 || display === DisplayModes.DAILY;
  54. const field = isTopEvents ? apiPayload.field : undefined;
  55. const topEvents = isTopEvents ? TOP_N : undefined;
  56. const orderby = isTopEvents ? decodeScalar(apiPayload.sort) : undefined;
  57. const intervalFidelity = display === 'bar' ? 'low' : 'high';
  58. const interval = isDaily
  59. ? '1d'
  60. : eventView.interval
  61. ? eventView.interval
  62. : getInterval({start, end, period}, intervalFidelity);
  63. return {
  64. organization,
  65. apiPayload,
  66. query,
  67. start,
  68. end,
  69. period,
  70. interval,
  71. project: eventView.project,
  72. environment: eventView.environment,
  73. yAxis: yAxis ?? eventView.getYAxis(),
  74. field,
  75. topEvents,
  76. orderby,
  77. showDaily: isDaily,
  78. expired: eventView.expired,
  79. name: eventView.name,
  80. display,
  81. };
  82. }
  83. getChartType({
  84. showDaily,
  85. }: {
  86. showDaily: boolean;
  87. timeseriesData: Series[];
  88. yAxis: string;
  89. }): PlotType {
  90. if (showDaily) {
  91. return 'bar';
  92. }
  93. return 'area';
  94. }
  95. getChartComponent(
  96. chartType: PlotType
  97. ): React.ComponentType<BarChartProps> | React.ComponentType<AreaChartProps> {
  98. switch (chartType) {
  99. case 'bar':
  100. return BarChart;
  101. case 'line':
  102. return LineChart;
  103. case 'area':
  104. return AreaChart;
  105. default:
  106. throw new Error(`Unknown multi plot type for ${chartType}`);
  107. }
  108. }
  109. render() {
  110. const {theme, api, referrer} = this.props;
  111. const {
  112. query,
  113. start,
  114. end,
  115. period,
  116. interval,
  117. organization,
  118. project,
  119. environment,
  120. yAxis,
  121. field,
  122. topEvents,
  123. orderby,
  124. showDaily,
  125. expired,
  126. name,
  127. display,
  128. } = this.getRefreshProps(this.props);
  129. if (display === DisplayModes.WORLDMAP) {
  130. return (
  131. <EventsGeoRequest
  132. api={api}
  133. organization={organization}
  134. yAxis={yAxis}
  135. query={query}
  136. orderby={orderby}
  137. projects={project as number[]}
  138. period={period}
  139. start={start}
  140. end={end}
  141. environments={environment as string[]}
  142. referrer={referrer}
  143. >
  144. {({errored, loading, tableData}) => {
  145. if (errored) {
  146. return (
  147. <StyledGraphContainer>
  148. <IconWarning color="gray300" size="md" />
  149. </StyledGraphContainer>
  150. );
  151. }
  152. if (loading) {
  153. return (
  154. <StyledGraphContainer>
  155. <LoadingIndicator mini />
  156. </StyledGraphContainer>
  157. );
  158. }
  159. const {data, title} = processTableResults(tableData);
  160. const chartOptions = {
  161. height: 100,
  162. series: [
  163. {
  164. seriesName: title,
  165. data,
  166. },
  167. ],
  168. fromDiscoverQueryList: true,
  169. };
  170. return <WorldMapChart {...chartOptions} />;
  171. }}
  172. </EventsGeoRequest>
  173. );
  174. }
  175. return (
  176. <EventsRequest
  177. organization={organization}
  178. api={api}
  179. query={query}
  180. start={start}
  181. end={end}
  182. period={period}
  183. interval={interval}
  184. project={project as number[]}
  185. environment={environment as string[]}
  186. includePrevious={false}
  187. yAxis={yAxis}
  188. field={field}
  189. topEvents={topEvents}
  190. orderby={orderby}
  191. expired={expired}
  192. name={name}
  193. referrer={referrer}
  194. hideError
  195. partial
  196. >
  197. {({loading, timeseriesData, results, errored, errorMessage}) => {
  198. if (errored) {
  199. return (
  200. <StyledGraphContainer>
  201. <IconWarning color="gray300" size="md" />
  202. <StyledErrorMessage>{errorMessage}</StyledErrorMessage>
  203. </StyledGraphContainer>
  204. );
  205. }
  206. if (loading) {
  207. return (
  208. <StyledGraphContainer>
  209. <LoadingIndicator mini />
  210. </StyledGraphContainer>
  211. );
  212. }
  213. const allSeries = timeseriesData ?? results ?? [];
  214. const chartType =
  215. display === 'bar'
  216. ? display
  217. : this.getChartType({
  218. showDaily,
  219. yAxis: Array.isArray(yAxis) ? yAxis[0] : yAxis,
  220. timeseriesData: allSeries,
  221. });
  222. const data = allSeries.map(series => ({
  223. ...series,
  224. lineStyle: {
  225. opacity: chartType === 'line' ? 1 : 0,
  226. },
  227. }));
  228. const hasOther = topEvents && topEvents + 1 === allSeries.length;
  229. const chartColors = allSeries.length
  230. ? [...theme.charts.getColorPalette(allSeries.length - 2 - (hasOther ? 1 : 0))]
  231. : undefined;
  232. if (chartColors && chartColors.length && hasOther) {
  233. chartColors.push(theme.chartOther);
  234. }
  235. const chartOptions = {
  236. colors: chartColors,
  237. height: 150,
  238. series: [...data],
  239. xAxis: {
  240. show: false,
  241. axisPointer: {
  242. show: false,
  243. },
  244. },
  245. yAxis: {
  246. show: true,
  247. axisLine: {
  248. show: false,
  249. },
  250. axisLabel: {
  251. color: theme.chartLabel,
  252. fontFamily: theme.text.family,
  253. fontSize: 12,
  254. formatter: (value: number) =>
  255. axisLabelFormatter(
  256. value,
  257. aggregateOutputType(Array.isArray(yAxis) ? yAxis[0] : yAxis),
  258. true
  259. ),
  260. inside: true,
  261. showMinLabel: false,
  262. showMaxLabel: false,
  263. },
  264. splitNumber: 3,
  265. splitLine: {
  266. show: false,
  267. },
  268. zlevel: theme.zIndex.header,
  269. },
  270. tooltip: {
  271. show: false,
  272. },
  273. toolBox: {
  274. show: false,
  275. },
  276. grid: {
  277. left: 0,
  278. top: 0,
  279. right: 0,
  280. bottom: 0,
  281. containLabel: false,
  282. },
  283. stacked:
  284. (typeof topEvents === 'number' && topEvents > 0) ||
  285. (Array.isArray(yAxis) && yAxis.length > 1),
  286. };
  287. const ChartComponent = this.getChartComponent(chartType);
  288. return <ChartComponent {...chartOptions} />;
  289. }}
  290. </EventsRequest>
  291. );
  292. }
  293. }
  294. const StyledGraphContainer = styled(props => (
  295. <LoadingContainer {...props} maskBackgroundColor="transparent" />
  296. ))`
  297. height: 150px;
  298. display: flex;
  299. justify-content: center;
  300. align-items: center;
  301. `;
  302. const StyledErrorMessage = styled('div')`
  303. color: ${p => p.theme.gray300};
  304. margin-left: 4px;
  305. `;
  306. export default withApi(withTheme(MiniGraph));