sidebarCharts.tsx 9.0 KB

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