index.tsx 11 KB

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