resultsChart.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {Component, Fragment} from 'react';
  2. import {InjectedRouter} from 'react-router';
  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 {BarChart} from 'sentry/components/charts/barChart';
  8. import EventsChart from 'sentry/components/charts/eventsChart';
  9. import {getInterval, getPreviousSeriesName} from 'sentry/components/charts/utils';
  10. import {WorldMapChart} from 'sentry/components/charts/worldMapChart';
  11. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  12. import {Panel} from 'sentry/components/panels';
  13. import Placeholder from 'sentry/components/placeholder';
  14. import {t} from 'sentry/locale';
  15. import {Organization} from 'sentry/types';
  16. import {valueIsEqual} from 'sentry/utils';
  17. import {getUtcToLocalDateObject} from 'sentry/utils/dates';
  18. import EventView from 'sentry/utils/discover/eventView';
  19. import {isEquation, stripEquationPrefix} from 'sentry/utils/discover/fields';
  20. import {
  21. DisplayModes,
  22. MULTI_Y_AXIS_SUPPORTED_DISPLAY_MODES,
  23. TOP_EVENT_MODES,
  24. TOP_N,
  25. } from 'sentry/utils/discover/types';
  26. import getDynamicText from 'sentry/utils/getDynamicText';
  27. import {decodeScalar} from 'sentry/utils/queryString';
  28. import withApi from 'sentry/utils/withApi';
  29. import ChartFooter from './chartFooter';
  30. type ResultsChartProps = {
  31. api: Client;
  32. confirmedQuery: boolean;
  33. eventView: EventView;
  34. location: Location;
  35. organization: Organization;
  36. router: InjectedRouter;
  37. yAxisValue: string[];
  38. };
  39. class ResultsChart extends Component<ResultsChartProps> {
  40. shouldComponentUpdate(nextProps: ResultsChartProps) {
  41. const {eventView, ...restProps} = this.props;
  42. const {eventView: nextEventView, ...restNextProps} = nextProps;
  43. if (!eventView.isEqualTo(nextEventView)) {
  44. return true;
  45. }
  46. return !isEqual(restProps, restNextProps);
  47. }
  48. render() {
  49. const {api, eventView, location, organization, router, confirmedQuery, yAxisValue} =
  50. this.props;
  51. const hasPerformanceChartInterpolation = organization.features.includes(
  52. 'performance-chart-interpolation'
  53. );
  54. const globalSelection = eventView.getPageFilters();
  55. const start = globalSelection.datetime.start
  56. ? getUtcToLocalDateObject(globalSelection.datetime.start)
  57. : null;
  58. const end = globalSelection.datetime.end
  59. ? getUtcToLocalDateObject(globalSelection.datetime.end)
  60. : null;
  61. const {utc} = normalizeDateTimeParams(location.query);
  62. const apiPayload = eventView.getEventsAPIPayload(location);
  63. const display = eventView.getDisplayMode();
  64. const isTopEvents =
  65. display === DisplayModes.TOP5 || display === DisplayModes.DAILYTOP5;
  66. const isPeriod = display === DisplayModes.DEFAULT || display === DisplayModes.TOP5;
  67. const isDaily = display === DisplayModes.DAILYTOP5 || display === DisplayModes.DAILY;
  68. const isPrevious = display === DisplayModes.PREVIOUS;
  69. const referrer = `api.discover.${display}-chart`;
  70. const topEvents = eventView.topEvents ? parseInt(eventView.topEvents, 10) : TOP_N;
  71. const chartComponent =
  72. display === DisplayModes.WORLDMAP
  73. ? WorldMapChart
  74. : display === DisplayModes.BAR
  75. ? BarChart
  76. : undefined;
  77. const interval =
  78. display === DisplayModes.BAR
  79. ? getInterval(
  80. {
  81. start,
  82. end,
  83. period: globalSelection.datetime.period,
  84. utc: utc === 'true',
  85. },
  86. 'low'
  87. )
  88. : eventView.interval;
  89. const seriesLabels = yAxisValue.map(stripEquationPrefix);
  90. const disableableSeries = [
  91. ...seriesLabels,
  92. ...seriesLabels.map(getPreviousSeriesName),
  93. ];
  94. return (
  95. <Fragment>
  96. {getDynamicText({
  97. value: (
  98. <EventsChart
  99. api={api}
  100. router={router}
  101. query={apiPayload.query}
  102. organization={organization}
  103. showLegend
  104. yAxis={yAxisValue}
  105. projects={globalSelection.projects}
  106. environments={globalSelection.environments}
  107. start={start}
  108. end={end}
  109. period={globalSelection.datetime.period}
  110. disablePrevious={!isPrevious}
  111. disableReleases={!isPeriod}
  112. field={isTopEvents ? apiPayload.field : undefined}
  113. interval={interval}
  114. showDaily={isDaily}
  115. topEvents={isTopEvents ? topEvents : undefined}
  116. orderby={isTopEvents ? decodeScalar(apiPayload.sort) : undefined}
  117. utc={utc === 'true'}
  118. confirmedQuery={confirmedQuery}
  119. withoutZerofill={hasPerformanceChartInterpolation}
  120. chartComponent={chartComponent}
  121. referrer={referrer}
  122. fromDiscover
  123. disableableSeries={disableableSeries}
  124. />
  125. ),
  126. fixed: <Placeholder height="200px" testId="skeleton-ui" />,
  127. })}
  128. </Fragment>
  129. );
  130. }
  131. }
  132. type ContainerProps = {
  133. api: Client;
  134. confirmedQuery: boolean;
  135. eventView: EventView;
  136. location: Location;
  137. onAxisChange: (value: string[]) => void;
  138. onDisplayChange: (value: string) => void;
  139. onTopEventsChange: (value: string) => void;
  140. organization: Organization;
  141. router: InjectedRouter;
  142. // chart footer props
  143. total: number | null;
  144. yAxis: string[];
  145. };
  146. class ResultsChartContainer extends Component<ContainerProps> {
  147. state = {
  148. yAxisOptions: this.getYAxisOptions(this.props.eventView),
  149. };
  150. componentWillReceiveProps(nextProps) {
  151. const yAxisOptions = this.getYAxisOptions(this.props.eventView);
  152. const nextYAxisOptions = this.getYAxisOptions(nextProps.eventView);
  153. if (!valueIsEqual(yAxisOptions, nextYAxisOptions, true)) {
  154. this.setState({yAxisOptions: nextYAxisOptions});
  155. }
  156. }
  157. shouldComponentUpdate(nextProps: ContainerProps) {
  158. const {eventView, ...restProps} = this.props;
  159. const {eventView: nextEventView, ...restNextProps} = nextProps;
  160. if (
  161. !eventView.isEqualTo(nextEventView) ||
  162. this.props.confirmedQuery !== nextProps.confirmedQuery
  163. ) {
  164. return true;
  165. }
  166. return !isEqual(restProps, restNextProps);
  167. }
  168. getYAxisOptions(eventView) {
  169. const yAxisOptions = eventView.getYAxisOptions();
  170. // Equations on World Map isn't supported on the events-geo endpoint
  171. // Disabling equations as an option to prevent erroring out
  172. if (eventView.getDisplayMode() === DisplayModes.WORLDMAP) {
  173. return yAxisOptions.filter(({value}) => !isEquation(value));
  174. }
  175. return yAxisOptions;
  176. }
  177. render() {
  178. const {
  179. api,
  180. eventView,
  181. location,
  182. router,
  183. total,
  184. onAxisChange,
  185. onDisplayChange,
  186. onTopEventsChange,
  187. organization,
  188. confirmedQuery,
  189. yAxis,
  190. } = this.props;
  191. const {yAxisOptions} = this.state;
  192. const hasQueryFeature = organization.features.includes('discover-query');
  193. const displayOptions = eventView
  194. .getDisplayOptions()
  195. .filter(opt => {
  196. // top5 modes are only available with larger packages in saas.
  197. // We remove instead of disable here as showing tooltips in dropdown
  198. // menus is clunky.
  199. if (TOP_EVENT_MODES.includes(opt.value) && !hasQueryFeature) {
  200. return false;
  201. }
  202. return true;
  203. })
  204. .map(opt => {
  205. // Can only use default display or total daily with multi y axis
  206. if (TOP_EVENT_MODES.includes(opt.value)) {
  207. opt.label = DisplayModes.TOP5 === opt.value ? 'Top Period' : 'Top Daily';
  208. }
  209. if (
  210. yAxis.length > 1 &&
  211. !MULTI_Y_AXIS_SUPPORTED_DISPLAY_MODES.includes(opt.value as DisplayModes)
  212. ) {
  213. return {
  214. ...opt,
  215. disabled: true,
  216. tooltip: t(
  217. 'Change the Y-Axis dropdown to display only 1 function to use this view.'
  218. ),
  219. };
  220. }
  221. return opt;
  222. });
  223. return (
  224. <StyledPanel>
  225. {(yAxis.length > 0 && (
  226. <ResultsChart
  227. api={api}
  228. eventView={eventView}
  229. location={location}
  230. organization={organization}
  231. router={router}
  232. confirmedQuery={confirmedQuery}
  233. yAxisValue={yAxis}
  234. />
  235. )) || <NoChartContainer>{t('No Y-Axis selected.')}</NoChartContainer>}
  236. <ChartFooter
  237. organization={organization}
  238. total={total}
  239. yAxisValue={yAxis}
  240. yAxisOptions={yAxisOptions}
  241. onAxisChange={onAxisChange}
  242. displayOptions={displayOptions}
  243. displayMode={eventView.getDisplayMode()}
  244. onDisplayChange={onDisplayChange}
  245. onTopEventsChange={onTopEventsChange}
  246. topEvents={eventView.topEvents ?? TOP_N.toString()}
  247. />
  248. </StyledPanel>
  249. );
  250. }
  251. }
  252. export default withApi(ResultsChartContainer);
  253. const StyledPanel = styled(Panel)`
  254. @media (min-width: ${p => p.theme.breakpoints.large}) {
  255. margin: 0;
  256. }
  257. `;
  258. const NoChartContainer = styled('div')<{height?: string}>`
  259. display: flex;
  260. flex-direction: column;
  261. justify-content: center;
  262. align-items: center;
  263. flex: 1;
  264. flex-shrink: 0;
  265. overflow: hidden;
  266. height: ${p => p.height || '200px'};
  267. position: relative;
  268. border-color: transparent;
  269. margin-bottom: 0;
  270. color: ${p => p.theme.gray300};
  271. font-size: ${p => p.theme.fontSizeExtraLarge};
  272. `;