projectBaseSessionsChart.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import {Component, Fragment} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import {Theme, useTheme} from '@emotion/react';
  4. import type {LegendComponentOption, LineSeriesOption} from 'echarts';
  5. import isEqual from 'lodash/isEqual';
  6. import {Client} from 'sentry/api';
  7. import {BarChart} from 'sentry/components/charts/barChart';
  8. import ChartZoom, {ZoomRenderProps} from 'sentry/components/charts/chartZoom';
  9. import ErrorPanel from 'sentry/components/charts/errorPanel';
  10. import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart';
  11. import ReleaseSeries from 'sentry/components/charts/releaseSeries';
  12. import StackedAreaChart from 'sentry/components/charts/stackedAreaChart';
  13. import {HeaderTitleLegend} from 'sentry/components/charts/styles';
  14. import TransitionChart from 'sentry/components/charts/transitionChart';
  15. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  16. import {RELEASE_LINES_THRESHOLD} from 'sentry/components/charts/utils';
  17. import QuestionTooltip from 'sentry/components/questionTooltip';
  18. import {IconWarning} from 'sentry/icons';
  19. import {t} from 'sentry/locale';
  20. import {Organization, PageFilters} from 'sentry/types';
  21. import {EChartEventHandler, Series} from 'sentry/types/echarts';
  22. import getDynamicText from 'sentry/utils/getDynamicText';
  23. import {MINUTES_THRESHOLD_TO_DISPLAY_SECONDS} from 'sentry/utils/sessions';
  24. import withPageFilters from 'sentry/utils/withPageFilters';
  25. import {displayCrashFreePercent} from 'sentry/views/releases/utils';
  26. import {sessionTerm} from 'sentry/views/releases/utils/sessionTerm';
  27. import {DisplayModes} from '../projectCharts';
  28. import ProjectSessionsAnrRequest from './projectSessionsAnrRequest';
  29. import ProjectSessionsChartRequest from './projectSessionsChartRequest';
  30. type Props = {
  31. api: Client;
  32. displayMode:
  33. | DisplayModes.SESSIONS
  34. | DisplayModes.STABILITY_USERS
  35. | DisplayModes.ANR_RATE
  36. | DisplayModes.FOREGROUND_ANR_RATE
  37. | DisplayModes.STABILITY;
  38. onTotalValuesChange: (value: number | null) => void;
  39. organization: Organization;
  40. router: InjectedRouter;
  41. selection: PageFilters;
  42. title: string;
  43. disablePrevious?: boolean;
  44. help?: string;
  45. query?: string;
  46. };
  47. function ProjectBaseSessionsChart({
  48. title,
  49. organization,
  50. router,
  51. selection,
  52. api,
  53. onTotalValuesChange,
  54. displayMode,
  55. help,
  56. disablePrevious,
  57. query,
  58. }: Props) {
  59. const theme = useTheme();
  60. const {projects, environments, datetime} = selection;
  61. const {start, end, period, utc} = datetime;
  62. const Request = [DisplayModes.ANR_RATE, DisplayModes.FOREGROUND_ANR_RATE].includes(
  63. displayMode
  64. )
  65. ? ProjectSessionsAnrRequest
  66. : ProjectSessionsChartRequest;
  67. return (
  68. <Fragment>
  69. {getDynamicText({
  70. value: (
  71. <ChartZoom router={router} period={period} start={start} end={end} utc={utc}>
  72. {zoomRenderProps => (
  73. <Request
  74. api={api}
  75. selection={selection}
  76. organization={organization}
  77. onTotalValuesChange={onTotalValuesChange}
  78. displayMode={displayMode}
  79. disablePrevious={disablePrevious}
  80. query={query}
  81. >
  82. {({
  83. errored,
  84. loading,
  85. reloading,
  86. timeseriesData,
  87. previousTimeseriesData,
  88. additionalSeries,
  89. }) => (
  90. <ReleaseSeries
  91. utc={utc}
  92. period={period}
  93. start={start}
  94. end={end}
  95. projects={projects}
  96. environments={environments}
  97. query={query}
  98. >
  99. {({releaseSeries}) => {
  100. if (errored) {
  101. return (
  102. <ErrorPanel>
  103. <IconWarning color="gray300" size="lg" />
  104. </ErrorPanel>
  105. );
  106. }
  107. return (
  108. <TransitionChart loading={loading} reloading={reloading}>
  109. <TransparentLoadingMask visible={reloading} />
  110. <HeaderTitleLegend>
  111. {title}
  112. {help && (
  113. <QuestionTooltip size="sm" position="top" title={help} />
  114. )}
  115. </HeaderTitleLegend>
  116. <Chart
  117. theme={theme}
  118. zoomRenderProps={zoomRenderProps}
  119. reloading={reloading}
  120. timeSeries={timeseriesData}
  121. previousTimeSeries={
  122. previousTimeseriesData
  123. ? [previousTimeseriesData]
  124. : undefined
  125. }
  126. releaseSeries={releaseSeries}
  127. displayMode={displayMode}
  128. additionalSeries={additionalSeries}
  129. />
  130. </TransitionChart>
  131. );
  132. }}
  133. </ReleaseSeries>
  134. )}
  135. </Request>
  136. )}
  137. </ChartZoom>
  138. ),
  139. fixed: `${title} Chart`,
  140. })}
  141. </Fragment>
  142. );
  143. }
  144. type ChartProps = {
  145. displayMode:
  146. | DisplayModes.SESSIONS
  147. | DisplayModes.STABILITY
  148. | DisplayModes.STABILITY_USERS
  149. | DisplayModes.ANR_RATE
  150. | DisplayModes.FOREGROUND_ANR_RATE;
  151. releaseSeries: Series[];
  152. reloading: boolean;
  153. theme: Theme;
  154. timeSeries: Series[];
  155. zoomRenderProps: ZoomRenderProps;
  156. additionalSeries?: LineSeriesOption[];
  157. previousTimeSeries?: Series[];
  158. };
  159. type ChartState = {
  160. forceUpdate: boolean;
  161. seriesSelection: Record<string, boolean>;
  162. };
  163. class Chart extends Component<ChartProps, ChartState> {
  164. state: ChartState = {
  165. seriesSelection: {},
  166. forceUpdate: false,
  167. };
  168. shouldComponentUpdate(nextProps: ChartProps, nextState: ChartState) {
  169. if (nextState.forceUpdate) {
  170. return true;
  171. }
  172. if (!isEqual(this.state.seriesSelection, nextState.seriesSelection)) {
  173. return true;
  174. }
  175. if (
  176. nextProps.releaseSeries !== this.props.releaseSeries &&
  177. !nextProps.reloading &&
  178. !this.props.reloading
  179. ) {
  180. return true;
  181. }
  182. if (this.props.reloading && !nextProps.reloading) {
  183. return true;
  184. }
  185. if (nextProps.timeSeries !== this.props.timeSeries) {
  186. return true;
  187. }
  188. return false;
  189. }
  190. // inspired by app/components/charts/eventsChart.tsx@handleLegendSelectChanged
  191. handleLegendSelectChanged: EChartEventHandler<{
  192. name: string;
  193. selected: Record<string, boolean>;
  194. type: 'legendselectchanged';
  195. }> = ({selected}) => {
  196. const seriesSelection = Object.keys(selected).reduce((state, key) => {
  197. state[key] = selected[key];
  198. return state;
  199. }, {});
  200. // we have to force an update here otherwise ECharts will
  201. // update its internal state and disable the series
  202. this.setState({seriesSelection, forceUpdate: true}, () =>
  203. this.setState({forceUpdate: false})
  204. );
  205. };
  206. get isCrashFree() {
  207. const {displayMode} = this.props;
  208. return [DisplayModes.STABILITY, DisplayModes.STABILITY_USERS].includes(displayMode);
  209. }
  210. get isAnr() {
  211. const {displayMode} = this.props;
  212. return [DisplayModes.ANR_RATE, DisplayModes.FOREGROUND_ANR_RATE].includes(
  213. displayMode
  214. );
  215. }
  216. get legend(): LegendComponentOption {
  217. const {theme, timeSeries, previousTimeSeries, releaseSeries, additionalSeries} =
  218. this.props;
  219. const {seriesSelection} = this.state;
  220. const hideReleasesByDefault =
  221. (releaseSeries[0] as any)?.markLine?.data.length >= RELEASE_LINES_THRESHOLD;
  222. const hideHealthyByDefault = timeSeries
  223. .filter(s => sessionTerm.healthy !== s.seriesName)
  224. .some(s => s.data.some(d => d.value > 0));
  225. const selected =
  226. Object.keys(seriesSelection).length === 0 &&
  227. (hideReleasesByDefault || hideHealthyByDefault)
  228. ? {
  229. [t('Releases')]: !hideReleasesByDefault,
  230. [sessionTerm.healthy]: !hideHealthyByDefault,
  231. }
  232. : seriesSelection;
  233. return {
  234. right: 10,
  235. top: 0,
  236. icon: 'circle',
  237. itemHeight: 8,
  238. itemWidth: 8,
  239. itemGap: 12,
  240. align: 'left' as const,
  241. textStyle: {
  242. color: theme.textColor,
  243. verticalAlign: 'top',
  244. fontSize: 11,
  245. fontFamily: theme.text.family,
  246. },
  247. data: [
  248. ...timeSeries.map(s => s.seriesName),
  249. ...(previousTimeSeries ?? []).map(s => s.seriesName),
  250. ...(additionalSeries ?? []).map(s => s.name?.toString() ?? ''),
  251. ...releaseSeries.map(s => s.seriesName),
  252. ],
  253. selected,
  254. };
  255. }
  256. get chartOptions(): Omit<LineChartProps, 'series'> {
  257. return {
  258. grid: {left: '10px', right: '10px', top: '40px', bottom: '0px'},
  259. seriesOptions: {
  260. showSymbol: false,
  261. },
  262. tooltip: {
  263. trigger: 'axis',
  264. truncate: 80,
  265. valueFormatter: (value: number | null) => {
  266. if (value === null) {
  267. return '\u2014';
  268. }
  269. if (this.isCrashFree) {
  270. return displayCrashFreePercent(value, 0, 3);
  271. }
  272. if (this.isAnr) {
  273. return displayCrashFreePercent(value, 0, 3, false);
  274. }
  275. return typeof value === 'number' ? value.toLocaleString() : value;
  276. },
  277. },
  278. yAxis: this.isCrashFree
  279. ? {
  280. axisLabel: {
  281. formatter: (value: number) => displayCrashFreePercent(value),
  282. },
  283. scale: true,
  284. max: 100,
  285. }
  286. : this.isAnr
  287. ? {
  288. axisLabel: {
  289. formatter: (value: number) => displayCrashFreePercent(value, 0, 3, false),
  290. },
  291. scale: true,
  292. }
  293. : {min: 0},
  294. };
  295. }
  296. render() {
  297. const {
  298. zoomRenderProps,
  299. timeSeries,
  300. previousTimeSeries,
  301. releaseSeries,
  302. additionalSeries,
  303. } = this.props;
  304. const ChartComponent = this.isCrashFree
  305. ? LineChart
  306. : this.isAnr
  307. ? BarChart
  308. : StackedAreaChart;
  309. return (
  310. <ChartComponent
  311. {...zoomRenderProps}
  312. {...this.chartOptions}
  313. legend={this.legend}
  314. series={
  315. Array.isArray(releaseSeries) && !this.isAnr
  316. ? [...timeSeries, ...releaseSeries]
  317. : timeSeries
  318. }
  319. additionalSeries={additionalSeries}
  320. previousPeriod={previousTimeSeries}
  321. onLegendSelectChanged={this.handleLegendSelectChanged}
  322. minutesThresholdToDisplaySeconds={MINUTES_THRESHOLD_TO_DISPLAY_SECONDS}
  323. transformSinglePointToBar
  324. />
  325. );
  326. }
  327. }
  328. export default withPageFilters(ProjectBaseSessionsChart);