index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {CompactSelect} from 'sentry/components/compactSelect';
  4. import {Tooltip} from 'sentry/components/tooltip';
  5. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  6. import {IconClock, IconGraph} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import type {Confidence, NewQuery} from 'sentry/types/organization';
  10. import {dedupeArray} from 'sentry/utils/dedupeArray';
  11. import EventView from 'sentry/utils/discover/eventView';
  12. import {
  13. aggregateOutputType,
  14. parseFunction,
  15. prettifyParsedFunction,
  16. } from 'sentry/utils/discover/fields';
  17. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  18. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import usePageFilters from 'sentry/utils/usePageFilters';
  20. import usePrevious from 'sentry/utils/usePrevious';
  21. import {ConfidenceFooter} from 'sentry/views/explore/charts/confidenceFooter';
  22. import ChartContextMenu from 'sentry/views/explore/components/chartContextMenu';
  23. import {
  24. useExploreDataset,
  25. useExploreVisualizes,
  26. useSetExploreVisualizes,
  27. } from 'sentry/views/explore/contexts/pageParamsContext';
  28. import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
  29. import {TOP_EVENTS_LIMIT} from 'sentry/views/explore/hooks/useTopEvents';
  30. import Chart, {
  31. ChartType,
  32. useSynchronizeCharts,
  33. } from 'sentry/views/insights/common/components/chart';
  34. import ChartPanel from 'sentry/views/insights/common/components/chartPanel';
  35. import type {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
  36. import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
  37. import {CHART_HEIGHT} from 'sentry/views/insights/database/settings';
  38. interface ExploreChartsProps {
  39. canUsePreviousResults: boolean;
  40. confidences: Confidence[];
  41. query: string;
  42. timeseriesResult: ReturnType<typeof useSortedTimeSeries>;
  43. }
  44. const exploreChartTypeOptions = [
  45. {
  46. value: ChartType.LINE,
  47. label: t('Line'),
  48. },
  49. {
  50. value: ChartType.AREA,
  51. label: t('Area'),
  52. },
  53. {
  54. value: ChartType.BAR,
  55. label: t('Bar'),
  56. },
  57. ];
  58. export const EXPLORE_CHART_GROUP = 'explore-charts_group';
  59. export function ExploreCharts({
  60. canUsePreviousResults,
  61. confidences,
  62. query,
  63. timeseriesResult,
  64. }: ExploreChartsProps) {
  65. const dataset = useExploreDataset();
  66. const visualizes = useExploreVisualizes();
  67. const setVisualizes = useSetExploreVisualizes();
  68. const [interval, setInterval, intervalOptions] = useChartInterval();
  69. const extrapolationMetaResults = useExtrapolationMeta({
  70. dataset,
  71. query,
  72. });
  73. const previousTimeseriesResult = usePrevious(timeseriesResult);
  74. const getSeries = useCallback(
  75. (dedupedYAxes: string[], formattedYAxes: Array<string | undefined>) => {
  76. const shouldUsePreviousResults =
  77. timeseriesResult.isPending &&
  78. canUsePreviousResults &&
  79. dedupedYAxes.every(yAxis => previousTimeseriesResult.data.hasOwnProperty(yAxis));
  80. const data = dedupedYAxes.flatMap((yAxis, i) => {
  81. const series = shouldUsePreviousResults
  82. ? previousTimeseriesResult.data[yAxis]
  83. : timeseriesResult.data[yAxis];
  84. return (series ?? []).map(s => {
  85. // We replace the series name with the formatted series name here
  86. // when possible as it's cleaner to read.
  87. //
  88. // We can't do this in top N mode as the series name uses the row
  89. // values instead of the aggregate function.
  90. if (s.seriesName === yAxis) {
  91. return {
  92. ...s,
  93. seriesName: formattedYAxes[i] ?? yAxis,
  94. };
  95. }
  96. return s;
  97. });
  98. });
  99. return {
  100. data,
  101. error: shouldUsePreviousResults
  102. ? previousTimeseriesResult.error
  103. : timeseriesResult.error,
  104. loading: shouldUsePreviousResults
  105. ? previousTimeseriesResult.isPending
  106. : timeseriesResult.isPending,
  107. };
  108. },
  109. [canUsePreviousResults, timeseriesResult, previousTimeseriesResult]
  110. );
  111. const chartInfos = useMemo(() => {
  112. return visualizes.map((visualize, index) => {
  113. const dedupedYAxes = dedupeArray(visualize.yAxes);
  114. const formattedYAxes = dedupedYAxes.map(yaxis => {
  115. const func = parseFunction(yaxis);
  116. return func ? prettifyParsedFunction(func) : undefined;
  117. });
  118. const chartIcon =
  119. visualize.chartType === ChartType.LINE
  120. ? 'line'
  121. : visualize.chartType === ChartType.AREA
  122. ? 'area'
  123. : 'bar';
  124. const {data, error, loading} = getSeries(dedupedYAxes, formattedYAxes);
  125. const outputTypes = new Set(
  126. formattedYAxes.filter(Boolean).map(aggregateOutputType)
  127. );
  128. return {
  129. chartIcon: <IconGraph type={chartIcon} />,
  130. chartType: visualize.chartType,
  131. label: visualize.label,
  132. yAxes: visualize.yAxes,
  133. formattedYAxes,
  134. data,
  135. error,
  136. loading,
  137. outputTypes,
  138. confidence: confidences[index],
  139. };
  140. });
  141. }, [confidences, getSeries, visualizes]);
  142. const handleChartTypeChange = useCallback(
  143. (chartType: ChartType, index: number) => {
  144. const newVisualizes = visualizes.slice();
  145. newVisualizes[index] = {...newVisualizes[index]!, chartType};
  146. setVisualizes(newVisualizes);
  147. },
  148. [visualizes, setVisualizes]
  149. );
  150. useSynchronizeCharts(
  151. visualizes.length,
  152. !timeseriesResult.isPending,
  153. EXPLORE_CHART_GROUP
  154. );
  155. const shouldRenderLabel = visualizes.length > 1;
  156. return (
  157. <Fragment>
  158. {chartInfos.map((chartInfo, index) => {
  159. return (
  160. <ChartContainer key={index}>
  161. <ChartPanel>
  162. <ChartHeader>
  163. {shouldRenderLabel && <ChartLabel>{chartInfo.label}</ChartLabel>}
  164. <ChartTitle>
  165. {chartInfo.formattedYAxes.filter(Boolean).join(', ')}
  166. </ChartTitle>
  167. <Tooltip
  168. title={t('Type of chart displayed in this visualization (ex. line)')}
  169. >
  170. <CompactSelect
  171. triggerProps={{
  172. icon: chartInfo.chartIcon,
  173. borderless: true,
  174. showChevron: false,
  175. size: 'sm',
  176. }}
  177. value={chartInfo.chartType}
  178. menuTitle="Type"
  179. options={exploreChartTypeOptions}
  180. onChange={option => handleChartTypeChange(option.value, index)}
  181. />
  182. </Tooltip>
  183. <Tooltip
  184. title={t('Time interval displayed in this visualization (ex. 5m)')}
  185. >
  186. <CompactSelect
  187. value={interval}
  188. onChange={({value}) => setInterval(value)}
  189. triggerProps={{
  190. icon: <IconClock />,
  191. borderless: true,
  192. showChevron: false,
  193. size: 'sm',
  194. }}
  195. menuTitle="Interval"
  196. options={intervalOptions}
  197. />
  198. </Tooltip>
  199. <ChartContextMenu
  200. visualizeYAxes={chartInfo.yAxes}
  201. query={query}
  202. interval={interval}
  203. visualizeIndex={index}
  204. />
  205. </ChartHeader>
  206. <Chart
  207. height={CHART_HEIGHT}
  208. grid={{
  209. left: '0',
  210. right: '0',
  211. top: '32px', // make room to fit the legend above the chart
  212. bottom: '0',
  213. }}
  214. legendOptions={{
  215. itemGap: 24,
  216. top: '4px',
  217. }}
  218. data={chartInfo.data}
  219. error={chartInfo.error}
  220. loading={chartInfo.loading}
  221. chartGroup={EXPLORE_CHART_GROUP}
  222. // TODO Abdullah: Make chart colors dynamic, with changing topN events count and overlay count.
  223. chartColors={CHART_PALETTE[TOP_EVENTS_LIMIT - 1]}
  224. type={chartInfo.chartType}
  225. aggregateOutputFormat={
  226. chartInfo.outputTypes.size === 1
  227. ? chartInfo.outputTypes.keys().next().value
  228. : undefined
  229. }
  230. showLegend
  231. />
  232. {dataset === DiscoverDatasets.SPANS_EAP_RPC && (
  233. <ChartFooter>
  234. <ConfidenceFooter
  235. sampleCount={extrapolationMetaResults.data?.[0]?.['count_sample()']}
  236. confidence={chartInfo.confidence}
  237. />
  238. </ChartFooter>
  239. )}
  240. </ChartPanel>
  241. </ChartContainer>
  242. );
  243. })}
  244. </Fragment>
  245. );
  246. }
  247. export function useExtrapolationMeta({
  248. dataset,
  249. query,
  250. }: {
  251. dataset: DiscoverDatasets;
  252. query: string;
  253. }) {
  254. const {selection} = usePageFilters();
  255. const extrapolationMetaEventView = useMemo(() => {
  256. const search = new MutableSearch(query);
  257. // Filtering out all spans with op like 'ui.interaction*' which aren't
  258. // embedded under transactions. The trace view does not support rendering
  259. // such spans yet.
  260. search.addFilterValues('!transaction.span_id', ['00']);
  261. const discoverQuery: NewQuery = {
  262. id: undefined,
  263. name: 'Explore - Extrapolation Meta',
  264. fields: ['count_sample()', 'min(sampling_rate)'],
  265. query: search.formatString(),
  266. version: 2,
  267. dataset,
  268. };
  269. return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
  270. }, [dataset, query, selection]);
  271. return useSpansQuery({
  272. eventView: extrapolationMetaEventView,
  273. initialData: [],
  274. referrer: 'api.explore.spans-extrapolation-meta',
  275. enabled: dataset === DiscoverDatasets.SPANS_EAP_RPC,
  276. });
  277. }
  278. const ChartContainer = styled('div')`
  279. display: grid;
  280. gap: 0;
  281. grid-template-columns: 1fr;
  282. margin-bottom: ${space(2)};
  283. `;
  284. const ChartHeader = styled('div')`
  285. display: flex;
  286. justify-content: space-between;
  287. `;
  288. const ChartTitle = styled('div')`
  289. ${p => p.theme.text.cardTitle}
  290. line-height: 32px;
  291. flex: 1;
  292. `;
  293. const ChartLabel = styled('div')`
  294. background-color: ${p => p.theme.purple100};
  295. border-radius: ${p => p.theme.borderRadius};
  296. text-align: center;
  297. min-width: 32px;
  298. color: ${p => p.theme.purple400};
  299. white-space: nowrap;
  300. font-weight: ${p => p.theme.fontWeightBold};
  301. align-content: center;
  302. margin-right: ${space(1)};
  303. `;
  304. const ChartFooter = styled('div')`
  305. display: inline-block;
  306. margin-top: ${space(1.5)};
  307. margin-bottom: 0;
  308. `;