index.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import type {Dispatch, SetStateAction} from 'react';
  2. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  3. import styled from '@emotion/styled';
  4. import {getInterval} from 'sentry/components/charts/utils';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  8. import {IconClock, IconGraph} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {dedupeArray} from 'sentry/utils/dedupeArray';
  12. import {
  13. aggregateOutputType,
  14. parseFunction,
  15. prettifyParsedFunction,
  16. } from 'sentry/utils/discover/fields';
  17. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. import {formatVersion} from 'sentry/utils/versions/formatVersion';
  20. import ChartContextMenu from 'sentry/views/explore/components/chartContextMenu';
  21. import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
  22. import {useDataset} from 'sentry/views/explore/hooks/useDataset';
  23. import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
  24. import Chart, {
  25. ChartType,
  26. useSynchronizeCharts,
  27. } from 'sentry/views/insights/common/components/chart';
  28. import ChartPanel from 'sentry/views/insights/common/components/chartPanel';
  29. import {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
  30. import {CHART_HEIGHT} from 'sentry/views/insights/database/settings';
  31. import {useGroupBys} from '../hooks/useGroupBys';
  32. import {useResultMode} from '../hooks/useResultsMode';
  33. import {useSorts} from '../hooks/useSorts';
  34. import {TOP_EVENTS_LIMIT, useTopEvents} from '../hooks/useTopEvents';
  35. import {formatSort} from '../tables/aggregatesTable';
  36. interface ExploreChartsProps {
  37. query: string;
  38. setError: Dispatch<SetStateAction<string>>;
  39. }
  40. const exploreChartTypeOptions = [
  41. {
  42. value: ChartType.LINE,
  43. label: t('Line'),
  44. },
  45. {
  46. value: ChartType.AREA,
  47. label: t('Area'),
  48. },
  49. {
  50. value: ChartType.BAR,
  51. label: t('Bar'),
  52. },
  53. ];
  54. export const EXPLORE_CHART_GROUP = 'explore-charts_group';
  55. // TODO: Update to support aggregate mode and multiple queries / visualizations
  56. export function ExploreCharts({query, setError}: ExploreChartsProps) {
  57. const pageFilters = usePageFilters();
  58. const [dataset] = useDataset({allowRPC: true});
  59. const [visualizes, setVisualizes] = useVisualizes();
  60. const [interval, setInterval, intervalOptions] = useChartInterval();
  61. const {groupBys} = useGroupBys();
  62. const [resultMode] = useResultMode();
  63. const topEvents = useTopEvents();
  64. const fields: string[] = useMemo(() => {
  65. if (resultMode === 'samples') {
  66. return [];
  67. }
  68. return [...groupBys, ...visualizes.flatMap(visualize => visualize.yAxes)].filter(
  69. Boolean
  70. );
  71. }, [resultMode, groupBys, visualizes]);
  72. const [sorts] = useSorts({fields});
  73. const orderby: string | string[] | undefined = useMemo(() => {
  74. if (!sorts.length) {
  75. return undefined;
  76. }
  77. return sorts.map(formatSort);
  78. }, [sorts]);
  79. const yAxes = useMemo(() => {
  80. const deduped = dedupeArray(visualizes.flatMap(visualize => visualize.yAxes));
  81. deduped.sort();
  82. return deduped;
  83. }, [visualizes]);
  84. const search = new MutableSearch(query);
  85. // Filtering out all spans with op like 'ui.interaction*' which aren't
  86. // embedded under transactions. The trace view does not support rendering
  87. // such spans yet.
  88. search.addFilterValues('!transaction.span_id', ['00']);
  89. const timeSeriesResult = useSortedTimeSeries(
  90. {
  91. search,
  92. yAxis: yAxes,
  93. interval: interval ?? getInterval(pageFilters.selection.datetime, 'metrics'),
  94. fields,
  95. orderby,
  96. topEvents,
  97. },
  98. 'api.explorer.stats',
  99. dataset
  100. );
  101. useEffect(() => {
  102. setError(timeSeriesResult.error?.message ?? '');
  103. }, [setError, timeSeriesResult.error?.message]);
  104. const getSeries = useCallback(
  105. (dedupedYAxes: string[], formattedYAxes: (string | undefined)[]) => {
  106. return dedupedYAxes.flatMap((yAxis, i) => {
  107. const series = timeSeriesResult.data[yAxis] ?? [];
  108. return series.map(s => {
  109. // We replace the series name with the formatted series name here
  110. // when possible as it's cleaner to read.
  111. //
  112. // We can't do this in top N mode as the series name uses the row
  113. // values instead of the aggregate function.
  114. if (s.seriesName === yAxis) {
  115. return {
  116. ...s,
  117. seriesName: formattedYAxes[i] ?? yAxis,
  118. };
  119. }
  120. return s;
  121. });
  122. });
  123. },
  124. [timeSeriesResult]
  125. );
  126. const handleChartTypeChange = useCallback(
  127. (chartType: ChartType, index: number) => {
  128. const newVisualizes = visualizes.slice();
  129. newVisualizes[index] = {...newVisualizes[index], chartType};
  130. setVisualizes(newVisualizes);
  131. },
  132. [visualizes, setVisualizes]
  133. );
  134. useSynchronizeCharts(
  135. visualizes.length,
  136. !timeSeriesResult.isPending,
  137. EXPLORE_CHART_GROUP
  138. );
  139. const shouldRenderLabel = visualizes.length > 1;
  140. return (
  141. <Fragment>
  142. {visualizes.map((visualize, index) => {
  143. const dedupedYAxes = dedupeArray(visualize.yAxes);
  144. const formattedYAxes = dedupedYAxes.map(yaxis => {
  145. const func = parseFunction(yaxis);
  146. return func ? prettifyParsedFunction(func) : undefined;
  147. });
  148. const {chartType, label, yAxes: visualizeYAxes} = visualize;
  149. const chartIcon =
  150. chartType === ChartType.LINE
  151. ? 'line'
  152. : chartType === ChartType.AREA
  153. ? 'area'
  154. : 'bar';
  155. const data = getSeries(dedupedYAxes, formattedYAxes);
  156. const outputTypes = new Set(
  157. formattedYAxes.filter(Boolean).map(aggregateOutputType)
  158. );
  159. return (
  160. <ChartContainer key={index}>
  161. <ChartPanel>
  162. <ChartHeader>
  163. {shouldRenderLabel && <ChartLabel>{label}</ChartLabel>}
  164. <ChartTitle>{formattedYAxes.filter(Boolean).join(', ')}</ChartTitle>
  165. <Tooltip
  166. title={t('Type of chart displayed in this visualization (ex. line)')}
  167. >
  168. <CompactSelect
  169. triggerProps={{
  170. icon: <IconGraph type={chartIcon} />,
  171. borderless: true,
  172. showChevron: false,
  173. size: 'sm',
  174. }}
  175. value={chartType}
  176. menuTitle="Type"
  177. options={exploreChartTypeOptions}
  178. onChange={option => handleChartTypeChange(option.value, index)}
  179. />
  180. </Tooltip>
  181. <Tooltip
  182. title={t('Time interval displayed in this visualization (ex. 5m)')}
  183. >
  184. <CompactSelect
  185. value={interval}
  186. onChange={({value}) => setInterval(value)}
  187. triggerProps={{
  188. icon: <IconClock />,
  189. borderless: true,
  190. showChevron: false,
  191. size: 'sm',
  192. }}
  193. menuTitle="Interval"
  194. options={intervalOptions}
  195. />
  196. </Tooltip>
  197. <ChartContextMenu
  198. visualizeYAxes={visualizeYAxes}
  199. query={query}
  200. interval={interval}
  201. visualizeIndex={index}
  202. />
  203. </ChartHeader>
  204. <Chart
  205. height={CHART_HEIGHT}
  206. grid={{
  207. left: '0',
  208. right: '0',
  209. top: '8px',
  210. bottom: '0',
  211. }}
  212. legendFormatter={value => formatVersion(value)}
  213. data={data}
  214. error={timeSeriesResult.error}
  215. loading={timeSeriesResult.isPending}
  216. chartGroup={EXPLORE_CHART_GROUP}
  217. // TODO Abdullah: Make chart colors dynamic, with changing topN events count and overlay count.
  218. chartColors={CHART_PALETTE[TOP_EVENTS_LIMIT - 1]}
  219. type={chartType}
  220. aggregateOutputFormat={
  221. outputTypes.size === 1 ? outputTypes.keys().next().value : undefined
  222. }
  223. showLegend
  224. />
  225. </ChartPanel>
  226. </ChartContainer>
  227. );
  228. })}
  229. </Fragment>
  230. );
  231. }
  232. const ChartContainer = styled('div')`
  233. display: grid;
  234. gap: 0;
  235. grid-template-columns: 1fr;
  236. margin-bottom: ${space(2)};
  237. `;
  238. const ChartHeader = styled('div')`
  239. display: flex;
  240. justify-content: space-between;
  241. `;
  242. const ChartTitle = styled('div')`
  243. ${p => p.theme.text.cardTitle}
  244. line-height: 32px;
  245. flex: 1;
  246. `;
  247. const ChartLabel = styled('div')`
  248. background-color: ${p => p.theme.purple100};
  249. border-radius: ${p => p.theme.borderRadius};
  250. text-align: center;
  251. min-width: 32px;
  252. color: ${p => p.theme.purple400};
  253. white-space: nowrap;
  254. font-weight: ${p => p.theme.fontWeightBold};
  255. align-content: center;
  256. margin-right: ${space(1)};
  257. `;