index.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import Feature from 'sentry/components/acl/feature';
  4. import {getInterval} from 'sentry/components/charts/utils';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  9. import {IconClock, IconGraph, IconSubscribed} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {dedupeArray} from 'sentry/utils/dedupeArray';
  13. import {
  14. aggregateOutputType,
  15. formatParsedFunction,
  16. parseFunction,
  17. } from 'sentry/utils/discover/fields';
  18. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import usePageFilters from 'sentry/utils/usePageFilters';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import {formatVersion} from 'sentry/utils/versions/formatVersion';
  23. import {Dataset} from 'sentry/views/alerts/rules/metric/types';
  24. import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
  25. import {useDataset} from 'sentry/views/explore/hooks/useDataset';
  26. import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
  27. import Chart, {
  28. ChartType,
  29. useSynchronizeCharts,
  30. } from 'sentry/views/insights/common/components/chart';
  31. import ChartPanel from 'sentry/views/insights/common/components/chartPanel';
  32. import {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
  33. import {getAlertsUrl} from 'sentry/views/insights/common/utils/getAlertsUrl';
  34. import {CHART_HEIGHT} from 'sentry/views/insights/database/settings';
  35. import {useGroupBys} from '../hooks/useGroupBys';
  36. import {useResultMode} from '../hooks/useResultsMode';
  37. import {useSorts} from '../hooks/useSorts';
  38. import {TOP_EVENTS_LIMIT, useTopEvents} from '../hooks/useTopEvents';
  39. import {formatSort} from '../tables/aggregatesTable';
  40. interface ExploreChartsProps {
  41. query: string;
  42. }
  43. const exploreChartTypeOptions = [
  44. {
  45. value: ChartType.LINE,
  46. label: t('Line'),
  47. },
  48. {
  49. value: ChartType.AREA,
  50. label: t('Area'),
  51. },
  52. {
  53. value: ChartType.BAR,
  54. label: t('Bar'),
  55. },
  56. ];
  57. export const EXPLORE_CHART_GROUP = 'explore-charts_group';
  58. // TODO: Update to support aggregate mode and multiple queries / visualizations
  59. export function ExploreCharts({query}: ExploreChartsProps) {
  60. const pageFilters = usePageFilters();
  61. const organization = useOrganization();
  62. const {projects} = useProjects();
  63. const [dataset] = useDataset();
  64. const [visualizes, setVisualizes] = useVisualizes();
  65. const [interval, setInterval, intervalOptions] = useChartInterval();
  66. const {groupBys} = useGroupBys();
  67. const [resultMode] = useResultMode();
  68. const topEvents = useTopEvents();
  69. const fields: string[] = useMemo(() => {
  70. if (resultMode === 'samples') {
  71. return [];
  72. }
  73. return [...groupBys, ...visualizes.flatMap(visualize => visualize.yAxes)].filter(
  74. Boolean
  75. );
  76. }, [resultMode, groupBys, visualizes]);
  77. const [sorts] = useSorts({fields});
  78. const orderby: string | string[] | undefined = useMemo(() => {
  79. if (!sorts.length) {
  80. return undefined;
  81. }
  82. return sorts.map(formatSort);
  83. }, [sorts]);
  84. const yAxes = useMemo(() => {
  85. const deduped = dedupeArray(visualizes.flatMap(visualize => visualize.yAxes));
  86. deduped.sort();
  87. return deduped;
  88. }, [visualizes]);
  89. const timeSeriesResult = useSortedTimeSeries(
  90. {
  91. search: new MutableSearch(query ?? ''),
  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. const getSeries = useCallback(
  102. (dedupedYAxes: string[]) => {
  103. return dedupedYAxes.flatMap(yAxis => {
  104. const series = timeSeriesResult.data[yAxis];
  105. return series !== undefined ? series : [];
  106. });
  107. },
  108. [timeSeriesResult]
  109. );
  110. const handleChartTypeChange = useCallback(
  111. (chartType: ChartType, index: number) => {
  112. const newVisualizes = visualizes.slice();
  113. newVisualizes[index] = {...newVisualizes[index], chartType};
  114. setVisualizes(newVisualizes);
  115. },
  116. [visualizes, setVisualizes]
  117. );
  118. useSynchronizeCharts(
  119. visualizes.length,
  120. !timeSeriesResult.isPending,
  121. EXPLORE_CHART_GROUP
  122. );
  123. return (
  124. <Fragment>
  125. {visualizes.map((visualize, index) => {
  126. const dedupedYAxes = dedupeArray(visualize.yAxes);
  127. const formattedYAxes = dedupedYAxes
  128. .map(yaxis => {
  129. const func = parseFunction(yaxis);
  130. return func ? formatParsedFunction(func) : undefined;
  131. })
  132. .filter(Boolean);
  133. const {chartType, yAxes: visualizeYAxes} = visualize;
  134. const chartIcon =
  135. chartType === ChartType.LINE
  136. ? 'line'
  137. : chartType === ChartType.AREA
  138. ? 'area'
  139. : 'bar';
  140. const project =
  141. projects.length === 1
  142. ? projects[0]
  143. : projects.find(p => p.id === `${pageFilters.selection.projects[0]}`);
  144. const singleProject =
  145. (pageFilters.selection.projects.length === 1 || projects.length === 1) &&
  146. project;
  147. const alertsUrls = singleProject
  148. ? visualizeYAxes.map(yAxis => ({
  149. key: yAxis,
  150. label: yAxis,
  151. to: getAlertsUrl({
  152. project,
  153. query,
  154. pageFilters: pageFilters.selection,
  155. aggregate: yAxis,
  156. orgSlug: organization.slug,
  157. dataset: Dataset.EVENTS_ANALYTICS_PLATFORM,
  158. interval,
  159. }),
  160. }))
  161. : undefined;
  162. return (
  163. <ChartContainer key={index}>
  164. <ChartPanel>
  165. <ChartHeader>
  166. <ChartTitle>{formattedYAxes.join(',')}</ChartTitle>
  167. <ChartSettingsContainer>
  168. <Tooltip
  169. title={t('Type of chart displayed in this visualization (ex. line)')}
  170. >
  171. <CompactSelect
  172. triggerLabel=""
  173. triggerProps={{
  174. icon: <IconGraph type={chartIcon} />,
  175. borderless: true,
  176. showChevron: false,
  177. size: 'sm',
  178. }}
  179. value={chartType}
  180. menuTitle="Type"
  181. options={exploreChartTypeOptions}
  182. onChange={option => handleChartTypeChange(option.value, index)}
  183. />
  184. </Tooltip>
  185. <Tooltip
  186. title={t('Time interval displayed in this visualization (ex. 5m)')}
  187. >
  188. <CompactSelect
  189. triggerLabel=""
  190. value={interval}
  191. onChange={({value}) => setInterval(value)}
  192. triggerProps={{
  193. icon: <IconClock />,
  194. borderless: true,
  195. showChevron: false,
  196. size: 'sm',
  197. }}
  198. menuTitle="Interval"
  199. options={intervalOptions}
  200. />
  201. </Tooltip>
  202. <Feature features="organizations:alerts-eap">
  203. <Tooltip
  204. title={
  205. singleProject
  206. ? t('Create an alert for this chart')
  207. : t(
  208. 'Cannot create an alert when multiple projects are selected'
  209. )
  210. }
  211. >
  212. <DropdownMenu
  213. triggerProps={{
  214. 'aria-label': t('Create Alert'),
  215. size: 'sm',
  216. borderless: true,
  217. showChevron: false,
  218. icon: <IconSubscribed />,
  219. }}
  220. position="bottom-end"
  221. items={alertsUrls ?? []}
  222. menuTitle={t('Create an alert for')}
  223. isDisabled={!alertsUrls || alertsUrls.length === 0}
  224. />
  225. </Tooltip>
  226. </Feature>
  227. </ChartSettingsContainer>
  228. </ChartHeader>
  229. <Chart
  230. height={CHART_HEIGHT}
  231. grid={{
  232. left: '0',
  233. right: '0',
  234. top: '8px',
  235. bottom: '0',
  236. }}
  237. legendFormatter={value => formatVersion(value)}
  238. data={getSeries(dedupedYAxes)}
  239. error={timeSeriesResult.error}
  240. loading={timeSeriesResult.isPending}
  241. chartGroup={EXPLORE_CHART_GROUP}
  242. // TODO Abdullah: Make chart colors dynamic, with changing topN events count and overlay count.
  243. chartColors={CHART_PALETTE[TOP_EVENTS_LIMIT - 1]}
  244. type={chartType}
  245. // for now, use the first y axis unit
  246. aggregateOutputFormat={aggregateOutputType(dedupedYAxes[0])}
  247. />
  248. </ChartPanel>
  249. </ChartContainer>
  250. );
  251. })}
  252. </Fragment>
  253. );
  254. }
  255. const ChartContainer = styled('div')`
  256. display: grid;
  257. gap: 0;
  258. grid-template-columns: 1fr;
  259. margin-bottom: ${space(2)};
  260. `;
  261. const ChartHeader = styled('div')`
  262. display: flex;
  263. align-items: flex-start;
  264. justify-content: space-between;
  265. `;
  266. const ChartTitle = styled('div')`
  267. ${p => p.theme.text.cardTitle}
  268. `;
  269. const ChartSettingsContainer = styled('div')`
  270. display: flex;
  271. `;