index.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {getInterval} from 'sentry/components/charts/utils';
  4. import {CompactSelect} from 'sentry/components/compactSelect';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  7. import {IconClock, IconGraph} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {dedupeArray} from 'sentry/utils/dedupeArray';
  11. import {
  12. aggregateOutputType,
  13. formatParsedFunction,
  14. parseFunction,
  15. } from 'sentry/utils/discover/fields';
  16. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  17. import usePageFilters from 'sentry/utils/usePageFilters';
  18. import {formatVersion} from 'sentry/utils/versions/formatVersion';
  19. import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
  20. import {useDataset} from 'sentry/views/explore/hooks/useDataset';
  21. import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
  22. import Chart, {
  23. ChartType,
  24. useSynchronizeCharts,
  25. } from 'sentry/views/insights/common/components/chart';
  26. import ChartPanel from 'sentry/views/insights/common/components/chartPanel';
  27. import {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
  28. import {CHART_HEIGHT} from 'sentry/views/insights/database/settings';
  29. import {useGroupBys} from '../hooks/useGroupBys';
  30. import {useResultMode} from '../hooks/useResultsMode';
  31. import {useSorts} from '../hooks/useSorts';
  32. import {TOP_EVENTS_LIMIT, useTopEvents} from '../hooks/useTopEvents';
  33. import {formatSort} from '../tables/aggregatesTable';
  34. interface ExploreChartsProps {
  35. query: string;
  36. }
  37. const exploreChartTypeOptions = [
  38. {
  39. value: ChartType.LINE,
  40. label: t('Line'),
  41. },
  42. {
  43. value: ChartType.AREA,
  44. label: t('Area'),
  45. },
  46. {
  47. value: ChartType.BAR,
  48. label: t('Bar'),
  49. },
  50. ];
  51. export const EXPLORE_CHART_GROUP = 'explore-charts_group';
  52. // TODO: Update to support aggregate mode and multiple queries / visualizations
  53. export function ExploreCharts({query}: ExploreChartsProps) {
  54. const pageFilters = usePageFilters();
  55. const [dataset] = useDataset();
  56. const [visualizes, setVisualizes] = useVisualizes();
  57. const [interval, setInterval, intervalOptions] = useChartInterval();
  58. const {groupBys} = useGroupBys();
  59. const [resultMode] = useResultMode();
  60. const topEvents = useTopEvents();
  61. const fields: string[] = useMemo(() => {
  62. if (resultMode === 'samples') {
  63. return [];
  64. }
  65. return [...groupBys, ...visualizes.flatMap(visualize => visualize.yAxes)].filter(
  66. Boolean
  67. );
  68. }, [resultMode, groupBys, visualizes]);
  69. const [sorts] = useSorts({fields});
  70. const orderby: string | string[] | undefined = useMemo(() => {
  71. if (!sorts.length) {
  72. return undefined;
  73. }
  74. return sorts.map(formatSort);
  75. }, [sorts]);
  76. const yAxes = useMemo(() => {
  77. const deduped = dedupeArray(visualizes.flatMap(visualize => visualize.yAxes));
  78. deduped.sort();
  79. return deduped;
  80. }, [visualizes]);
  81. const timeSeriesResult = useSortedTimeSeries(
  82. {
  83. search: new MutableSearch(query ?? ''),
  84. yAxis: yAxes,
  85. interval: interval ?? getInterval(pageFilters.selection.datetime, 'metrics'),
  86. fields,
  87. orderby,
  88. topEvents,
  89. },
  90. 'api.explorer.stats',
  91. dataset
  92. );
  93. const getSeries = useCallback(
  94. (dedupedYAxes: string[]) => {
  95. return dedupedYAxes.flatMap(yAxis => {
  96. const series = timeSeriesResult.data[yAxis];
  97. return series !== undefined ? series : [];
  98. });
  99. },
  100. [timeSeriesResult]
  101. );
  102. const handleChartTypeChange = useCallback(
  103. (chartType: ChartType, index: number) => {
  104. const newVisualizes = visualizes.slice();
  105. newVisualizes[index] = {...newVisualizes[index], chartType};
  106. setVisualizes(newVisualizes);
  107. },
  108. [visualizes, setVisualizes]
  109. );
  110. useSynchronizeCharts(
  111. visualizes.length,
  112. !timeSeriesResult.isPending,
  113. EXPLORE_CHART_GROUP
  114. );
  115. return (
  116. <Fragment>
  117. {visualizes.map((visualize, index) => {
  118. const dedupedYAxes = dedupeArray(visualize.yAxes);
  119. const formattedYAxes = dedupedYAxes
  120. .map(yaxis => {
  121. const func = parseFunction(yaxis);
  122. return func ? formatParsedFunction(func) : undefined;
  123. })
  124. .filter(Boolean);
  125. const {chartType} = visualize;
  126. const chartIcon =
  127. chartType === ChartType.LINE
  128. ? 'line'
  129. : chartType === ChartType.AREA
  130. ? 'area'
  131. : 'bar';
  132. return (
  133. <ChartContainer key={index}>
  134. <ChartPanel>
  135. <ChartHeader>
  136. <ChartTitle>{formattedYAxes.join(',')}</ChartTitle>
  137. <ChartSettingsContainer>
  138. <Tooltip
  139. title={t('Type of chart displayed in this visualization (ex. line)')}
  140. >
  141. <CompactSelect
  142. triggerLabel=""
  143. triggerProps={{
  144. icon: <IconGraph type={chartIcon} />,
  145. borderless: true,
  146. showChevron: false,
  147. size: 'sm',
  148. }}
  149. value={chartType}
  150. menuTitle="Type"
  151. options={exploreChartTypeOptions}
  152. onChange={option => handleChartTypeChange(option.value, index)}
  153. />
  154. </Tooltip>
  155. <Tooltip
  156. title={t('Time interval displayed in this visualization (ex. 5m)')}
  157. >
  158. <CompactSelect
  159. triggerLabel=""
  160. value={interval}
  161. onChange={({value}) => setInterval(value)}
  162. triggerProps={{
  163. icon: <IconClock />,
  164. borderless: true,
  165. showChevron: false,
  166. size: 'sm',
  167. }}
  168. menuTitle="Interval"
  169. options={intervalOptions}
  170. />
  171. </Tooltip>
  172. </ChartSettingsContainer>
  173. </ChartHeader>
  174. <Chart
  175. height={CHART_HEIGHT}
  176. grid={{
  177. left: '0',
  178. right: '0',
  179. top: '8px',
  180. bottom: '0',
  181. }}
  182. legendFormatter={value => formatVersion(value)}
  183. data={getSeries(dedupedYAxes)}
  184. error={timeSeriesResult.error}
  185. loading={timeSeriesResult.isPending}
  186. chartGroup={EXPLORE_CHART_GROUP}
  187. // TODO Abdullah: Make chart colors dynamic, with changing topN events count and overlay count.
  188. chartColors={CHART_PALETTE[TOP_EVENTS_LIMIT - 1]}
  189. type={chartType}
  190. // for now, use the first y axis unit
  191. aggregateOutputFormat={aggregateOutputType(dedupedYAxes[0])}
  192. />
  193. </ChartPanel>
  194. </ChartContainer>
  195. );
  196. })}
  197. </Fragment>
  198. );
  199. }
  200. const ChartContainer = styled('div')`
  201. display: grid;
  202. gap: 0;
  203. grid-template-columns: 1fr;
  204. margin-bottom: ${space(2)};
  205. `;
  206. const ChartHeader = styled('div')`
  207. display: flex;
  208. align-items: flex-start;
  209. justify-content: space-between;
  210. `;
  211. const ChartTitle = styled('div')`
  212. ${p => p.theme.text.cardTitle}
  213. `;
  214. const ChartSettingsContainer = styled('div')`
  215. display: flex;
  216. `;