vitalChartMetrics.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import {browserHistory} from 'react-router';
  2. import {useTheme} from '@emotion/react';
  3. import moment from 'moment';
  4. import ChartZoom from 'sentry/components/charts/chartZoom';
  5. import ErrorPanel from 'sentry/components/charts/errorPanel';
  6. import {LineChart} from 'sentry/components/charts/lineChart';
  7. import ReleaseSeries from 'sentry/components/charts/releaseSeries';
  8. import {ChartContainer, HeaderTitleLegend} from 'sentry/components/charts/styles';
  9. import TransitionChart from 'sentry/components/charts/transitionChart';
  10. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  11. import Panel from 'sentry/components/panels/panel';
  12. import QuestionTooltip from 'sentry/components/questionTooltip';
  13. import {IconWarning} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import type {DateString, MetricsApiResponse} from 'sentry/types';
  16. import type {Series} from 'sentry/types/echarts';
  17. import type {WebVital} from 'sentry/utils/fields';
  18. import getDynamicText from 'sentry/utils/getDynamicText';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useRouter from 'sentry/utils/useRouter';
  21. import {replaceSeriesName, transformEventStatsSmoothed} from '../trends/utils';
  22. import type {ViewProps} from '../types';
  23. import {getMaxOfSeries, getVitalChartDefinitions, getVitalChartTitle} from './utils';
  24. type Props = Omit<ViewProps, 'query' | 'start' | 'end'> & {
  25. end: DateString | null;
  26. errored: boolean;
  27. field: string;
  28. loading: boolean;
  29. reloading: boolean;
  30. response: MetricsApiResponse | null;
  31. start: DateString | null;
  32. vital: WebVital;
  33. };
  34. function VitalChartMetrics({
  35. reloading,
  36. loading,
  37. response,
  38. errored,
  39. statsPeriod,
  40. start,
  41. end,
  42. project,
  43. environment,
  44. field,
  45. vital,
  46. }: Props) {
  47. const location = useLocation();
  48. const router = useRouter();
  49. const theme = useTheme();
  50. const {utc, legend, vitalPoor, markLines, chartOptions} = getVitalChartDefinitions({
  51. theme,
  52. location,
  53. vital,
  54. yAxis: field,
  55. });
  56. function handleLegendSelectChanged(legendChange: {
  57. name: string;
  58. selected: Record<string, boolean>;
  59. type: string;
  60. }) {
  61. const {selected} = legendChange;
  62. const unselected = Object.keys(selected).filter(key => !selected[key]);
  63. const to = {
  64. ...location,
  65. query: {
  66. ...location.query,
  67. unselectedSeries: unselected,
  68. },
  69. };
  70. browserHistory.push(to);
  71. }
  72. return (
  73. <Panel>
  74. <ChartContainer>
  75. <HeaderTitleLegend>
  76. {getVitalChartTitle(vital)}
  77. <QuestionTooltip
  78. size="sm"
  79. position="top"
  80. title={t('The durations shown should fall under the vital threshold.')}
  81. />
  82. </HeaderTitleLegend>
  83. <ChartZoom router={router} period={statsPeriod} start={start} end={end} utc={utc}>
  84. {zoomRenderProps => {
  85. if (errored) {
  86. return (
  87. <ErrorPanel>
  88. <IconWarning color="gray500" size="lg" />
  89. </ErrorPanel>
  90. );
  91. }
  92. const data = response?.groups.map(group => ({
  93. seriesName: field,
  94. data: response.intervals.map((intervalValue, intervalIndex) => ({
  95. name: moment(intervalValue).valueOf(),
  96. value: group.series ? group.series[field][intervalIndex] : 0,
  97. })),
  98. })) as Series[] | undefined;
  99. const colors = (data && theme.charts.getColorPalette(data.length - 2)) || [];
  100. const {smoothedResults} = transformEventStatsSmoothed(data);
  101. const smoothedSeries = smoothedResults
  102. ? smoothedResults.map(({seriesName, ...rest}, i: number) => {
  103. return {
  104. seriesName: replaceSeriesName(seriesName) || 'p75',
  105. ...rest,
  106. color: colors[i],
  107. lineStyle: {
  108. opacity: 1,
  109. width: 2,
  110. },
  111. };
  112. })
  113. : [];
  114. const seriesMax = getMaxOfSeries(smoothedSeries);
  115. const yAxisMax = Math.max(seriesMax, vitalPoor);
  116. chartOptions.yAxis!.max = yAxisMax * 1.1;
  117. return (
  118. <ReleaseSeries
  119. start={start}
  120. end={end}
  121. period={statsPeriod}
  122. utc={utc}
  123. projects={project}
  124. environments={environment}
  125. >
  126. {({releaseSeries}) => (
  127. <TransitionChart loading={loading} reloading={reloading}>
  128. <TransparentLoadingMask visible={reloading} />
  129. {getDynamicText({
  130. value: (
  131. <LineChart
  132. {...zoomRenderProps}
  133. {...chartOptions}
  134. legend={legend}
  135. onLegendSelectChanged={handleLegendSelectChanged}
  136. series={[...markLines, ...releaseSeries, ...smoothedSeries]}
  137. />
  138. ),
  139. fixed: 'Web Vitals Chart',
  140. })}
  141. </TransitionChart>
  142. )}
  143. </ReleaseSeries>
  144. );
  145. }}
  146. </ChartZoom>
  147. </ChartContainer>
  148. </Panel>
  149. );
  150. }
  151. export default VitalChartMetrics;