sidebarCharts.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import * as React from 'react';
  2. import * as ReactRouter from 'react-router';
  3. import {withTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Location} from 'history';
  6. import {Client} from 'app/api';
  7. import ChartZoom from 'app/components/charts/chartZoom';
  8. import ErrorPanel from 'app/components/charts/errorPanel';
  9. import EventsRequest from 'app/components/charts/eventsRequest';
  10. import LineChart from 'app/components/charts/lineChart';
  11. import {SectionHeading} from 'app/components/charts/styles';
  12. import TransitionChart from 'app/components/charts/transitionChart';
  13. import TransparentLoadingMask from 'app/components/charts/transparentLoadingMask';
  14. import {getInterval} from 'app/components/charts/utils';
  15. import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
  16. import Placeholder from 'app/components/placeholder';
  17. import QuestionTooltip from 'app/components/questionTooltip';
  18. import {IconWarning} from 'app/icons';
  19. import {t, tct} from 'app/locale';
  20. import {LightWeightOrganization} from 'app/types';
  21. import {getUtcToLocalDateObject} from 'app/utils/dates';
  22. import {tooltipFormatter} from 'app/utils/discover/charts';
  23. import EventView from 'app/utils/discover/eventView';
  24. import {
  25. formatAbbreviatedNumber,
  26. formatFloat,
  27. formatPercentage,
  28. } from 'app/utils/formatters';
  29. import {Theme} from 'app/utils/theme';
  30. import withApi from 'app/utils/withApi';
  31. import {getTermHelp, PERFORMANCE_TERM} from 'app/views/performance/data';
  32. type Props = ReactRouter.WithRouterProps & {
  33. theme: Theme;
  34. api: Client;
  35. organization: LightWeightOrganization;
  36. location: Location;
  37. eventView: EventView;
  38. isLoading: boolean;
  39. error: string | null;
  40. totals: Record<string, number> | null;
  41. };
  42. function SidebarCharts({
  43. theme,
  44. api,
  45. location,
  46. eventView,
  47. organization,
  48. router,
  49. isLoading,
  50. error,
  51. totals,
  52. }: Props) {
  53. const statsPeriod = eventView.statsPeriod;
  54. const start = eventView.start ? getUtcToLocalDateObject(eventView.start) : undefined;
  55. const end = eventView.end ? getUtcToLocalDateObject(eventView.end) : undefined;
  56. const {utc} = getParams(location.query);
  57. const colors = theme.charts.getColorPalette(3);
  58. const axisLineConfig = {
  59. scale: true,
  60. axisLine: {
  61. show: false,
  62. },
  63. axisTick: {
  64. show: false,
  65. },
  66. splitLine: {
  67. show: false,
  68. },
  69. };
  70. const chartOptions = {
  71. height: 480,
  72. grid: [
  73. {
  74. top: '60px',
  75. left: '10px',
  76. right: '10px',
  77. height: '100px',
  78. },
  79. {
  80. top: '220px',
  81. left: '10px',
  82. right: '10px',
  83. height: '100px',
  84. },
  85. {
  86. top: '380px',
  87. left: '10px',
  88. right: '10px',
  89. height: '120px',
  90. },
  91. ],
  92. axisPointer: {
  93. // Link each x-axis together.
  94. link: [{xAxisIndex: [0, 1, 2]}],
  95. },
  96. xAxes: Array.from(new Array(3)).map((_i, index) => ({
  97. gridIndex: index,
  98. type: 'time' as const,
  99. show: false,
  100. })),
  101. yAxes: [
  102. {
  103. // apdex
  104. gridIndex: 0,
  105. interval: 0.2,
  106. axisLabel: {
  107. formatter: (value: number) => formatFloat(value, 1),
  108. color: theme.chartLabel,
  109. },
  110. ...axisLineConfig,
  111. },
  112. {
  113. // failure rate
  114. gridIndex: 1,
  115. splitNumber: 4,
  116. interval: 0.5,
  117. max: 1.0,
  118. axisLabel: {
  119. formatter: (value: number) => formatPercentage(value, 0),
  120. color: theme.chartLabel,
  121. },
  122. ...axisLineConfig,
  123. },
  124. {
  125. // throughput
  126. gridIndex: 2,
  127. splitNumber: 4,
  128. axisLabel: {
  129. formatter: formatAbbreviatedNumber,
  130. color: theme.chartLabel,
  131. },
  132. ...axisLineConfig,
  133. },
  134. ],
  135. utc: utc === 'true',
  136. isGroupedByDate: true,
  137. showTimeInTooltip: true,
  138. colors: [colors[0], colors[1], colors[2]] as string[],
  139. tooltip: {
  140. trigger: 'axis' as const,
  141. truncate: 80,
  142. valueFormatter: tooltipFormatter,
  143. nameFormatter(value: string) {
  144. return value === 'epm()' ? 'tpm()' : value;
  145. },
  146. },
  147. };
  148. const datetimeSelection = {
  149. start: start || null,
  150. end: end || null,
  151. period: statsPeriod,
  152. };
  153. const project = eventView.project;
  154. const environment = eventView.environment;
  155. const threshold = organization.apdexThreshold;
  156. let apdexKey: string;
  157. let apdexPerformanceTerm: PERFORMANCE_TERM;
  158. if (organization.features.includes('project-transaction-threshold')) {
  159. apdexKey = 'apdex_new';
  160. apdexPerformanceTerm = PERFORMANCE_TERM.APDEX_NEW;
  161. } else {
  162. apdexKey = `apdex_${threshold}`;
  163. apdexPerformanceTerm = PERFORMANCE_TERM.APDEX;
  164. }
  165. return (
  166. <RelativeBox>
  167. <ChartLabel top="0px">
  168. <ChartTitle>
  169. {t('Apdex')}
  170. <QuestionTooltip
  171. position="top"
  172. title={getTermHelp(organization, apdexPerformanceTerm)}
  173. size="sm"
  174. />
  175. </ChartTitle>
  176. <ChartSummaryValue
  177. isLoading={isLoading}
  178. error={error}
  179. value={totals ? formatFloat(totals[apdexKey], 4) : null}
  180. />
  181. </ChartLabel>
  182. <ChartLabel top="160px">
  183. <ChartTitle>
  184. {t('Failure Rate')}
  185. <QuestionTooltip
  186. position="top"
  187. title={getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE)}
  188. size="sm"
  189. />
  190. </ChartTitle>
  191. <ChartSummaryValue
  192. isLoading={isLoading}
  193. error={error}
  194. value={totals ? formatPercentage(totals.failure_rate) : null}
  195. />
  196. </ChartLabel>
  197. <ChartLabel top="320px">
  198. <ChartTitle>
  199. {t('TPM')}
  200. <QuestionTooltip
  201. position="top"
  202. title={getTermHelp(organization, PERFORMANCE_TERM.TPM)}
  203. size="sm"
  204. />
  205. </ChartTitle>
  206. <ChartSummaryValue
  207. isLoading={isLoading}
  208. error={error}
  209. value={totals ? tct('[tpm] tpm', {tpm: formatFloat(totals.tpm, 4)}) : null}
  210. />
  211. </ChartLabel>
  212. <ChartZoom
  213. router={router}
  214. period={statsPeriod}
  215. start={start}
  216. end={end}
  217. utc={utc === 'true'}
  218. xAxisIndex={[0, 1, 2]}
  219. >
  220. {zoomRenderProps => (
  221. <EventsRequest
  222. api={api}
  223. organization={organization}
  224. period={statsPeriod}
  225. project={project}
  226. environment={environment}
  227. start={start}
  228. end={end}
  229. interval={getInterval(datetimeSelection)}
  230. showLoading={false}
  231. query={eventView.query}
  232. includePrevious={false}
  233. yAxis={[`apdex(${organization.apdexThreshold})`, 'failure_rate()', 'epm()']}
  234. partial
  235. >
  236. {({results, errored, loading, reloading}) => {
  237. if (errored) {
  238. return (
  239. <ErrorPanel height="580px">
  240. <IconWarning color="gray300" size="lg" />
  241. </ErrorPanel>
  242. );
  243. }
  244. const series = results
  245. ? results.map((values, i: number) => ({
  246. ...values,
  247. yAxisIndex: i,
  248. xAxisIndex: i,
  249. }))
  250. : [];
  251. return (
  252. <TransitionChart loading={loading} reloading={reloading} height="580px">
  253. <TransparentLoadingMask visible={reloading} />
  254. <LineChart {...zoomRenderProps} {...chartOptions} series={series} />
  255. </TransitionChart>
  256. );
  257. }}
  258. </EventsRequest>
  259. )}
  260. </ChartZoom>
  261. </RelativeBox>
  262. );
  263. }
  264. type ChartValueProps = {
  265. isLoading: boolean;
  266. error: string | null;
  267. value: React.ReactNode;
  268. };
  269. function ChartSummaryValue({error, isLoading, value}: ChartValueProps) {
  270. if (error) {
  271. return <div>{'\u2014'}</div>;
  272. } else if (isLoading) {
  273. return <Placeholder height="24px" />;
  274. } else {
  275. return <ChartValue>{value}</ChartValue>;
  276. }
  277. }
  278. const RelativeBox = styled('div')`
  279. position: relative;
  280. `;
  281. const ChartTitle = styled(SectionHeading)`
  282. margin: 0;
  283. `;
  284. const ChartLabel = styled('div')<{top: string}>`
  285. position: absolute;
  286. top: ${p => p.top};
  287. z-index: 1;
  288. `;
  289. const ChartValue = styled('div')`
  290. font-size: ${p => p.theme.fontSizeExtraLarge};
  291. `;
  292. export default withApi(withTheme(ReactRouter.withRouter(SidebarCharts)));