scratchpad.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {useCallback, useLayoutEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as echarts from 'echarts/core';
  4. import {space} from 'sentry/styles/space';
  5. import {getMetricsCorrelationSpanUrl, unescapeMetricsFormula} from 'sentry/utils/metrics';
  6. import {MetricQueryType, type MetricWidgetQueryParams} from 'sentry/utils/metrics/types';
  7. import type {MetricsQueryApiQueryParams} from 'sentry/utils/metrics/useMetricsQuery';
  8. import useOrganization from 'sentry/utils/useOrganization';
  9. import usePageFilters from 'sentry/utils/usePageFilters';
  10. import useProjects from 'sentry/utils/useProjects';
  11. import useRouter from 'sentry/utils/useRouter';
  12. import {DDM_CHART_GROUP, MIN_WIDGET_WIDTH} from 'sentry/views/ddm/constants';
  13. import {useDDMContext} from 'sentry/views/ddm/context';
  14. import {parseFormula} from 'sentry/views/ddm/formulaParser/parser';
  15. import {type TokenList, TokenType} from 'sentry/views/ddm/formulaParser/types';
  16. import {getQuerySymbol} from 'sentry/views/ddm/querySymbol';
  17. import {useGetCachedChartPalette} from 'sentry/views/ddm/utils/metricsChartPalette';
  18. import type {Sample} from './widget';
  19. import {MetricWidget} from './widget';
  20. interface WidgetDependencies {
  21. dependencies: MetricsQueryApiQueryParams[];
  22. isError: boolean;
  23. }
  24. function widgetToQuery(
  25. widget: MetricWidgetQueryParams,
  26. isQueryOnly = false
  27. ): MetricsQueryApiQueryParams {
  28. return widget.type === MetricQueryType.FORMULA
  29. ? {
  30. name: getQuerySymbol(widget.id),
  31. formula: widget.formula,
  32. }
  33. : {
  34. name: getQuerySymbol(widget.id),
  35. mri: widget.mri,
  36. op: widget.op,
  37. groupBy: widget.groupBy,
  38. query: widget.query,
  39. isQueryOnly: isQueryOnly,
  40. };
  41. }
  42. export function MetricScratchpad() {
  43. const {
  44. setSelectedWidgetIndex,
  45. selectedWidgetIndex,
  46. widgets,
  47. updateWidget,
  48. showQuerySymbols,
  49. highlightedSampleId,
  50. focusArea,
  51. isMultiChartMode,
  52. } = useDDMContext();
  53. const {selection} = usePageFilters();
  54. const router = useRouter();
  55. const organization = useOrganization();
  56. const {projects} = useProjects();
  57. const getChartPalette = useGetCachedChartPalette();
  58. // Make sure all charts are connected to the same group whenever the widgets definition changes
  59. useLayoutEffect(() => {
  60. echarts.connect(DDM_CHART_GROUP);
  61. }, [widgets]);
  62. const handleChange = useCallback(
  63. (index: number, widget: Partial<MetricWidgetQueryParams>) => {
  64. updateWidget(index, widget);
  65. },
  66. [updateWidget]
  67. );
  68. const handleSampleClick = useCallback(
  69. (sample: Sample) => {
  70. const project = projects.find(p => parseInt(p.id, 10) === sample.projectId);
  71. router.push(
  72. getMetricsCorrelationSpanUrl(
  73. organization,
  74. project?.slug,
  75. sample.spanId,
  76. sample.transactionId,
  77. sample.transactionSpanId
  78. )
  79. );
  80. },
  81. [projects, router, organization]
  82. );
  83. const firstWidget = widgets[0];
  84. const Wrapper =
  85. !isMultiChartMode || widgets.length === 1
  86. ? StyledSingleWidgetWrapper
  87. : StyledMetricDashboard;
  88. const queriesLookup = useMemo(() => {
  89. const lookup = new Map<string, MetricWidgetQueryParams>();
  90. widgets.forEach(widget => {
  91. lookup.set(getQuerySymbol(widget.id), widget);
  92. });
  93. return lookup;
  94. }, [widgets]);
  95. const getFormulaQueryDependencies = useCallback(
  96. (formula: string): WidgetDependencies => {
  97. let tokens: TokenList = [];
  98. try {
  99. tokens = parseFormula(unescapeMetricsFormula(formula));
  100. } catch {
  101. // We should not end up here, but if we do, we should not crash the UI
  102. return {dependencies: [], isError: true};
  103. }
  104. const dependencies: MetricsQueryApiQueryParams[] = [];
  105. let isError: boolean = false;
  106. tokens.forEach(token => {
  107. if (token.type === TokenType.VARIABLE) {
  108. const widget = queriesLookup.get(token.content);
  109. if (widget && widget.type === MetricQueryType.QUERY) {
  110. dependencies.push(widgetToQuery(widget, true));
  111. } else {
  112. isError = true;
  113. }
  114. }
  115. });
  116. return {dependencies, isError};
  117. },
  118. [queriesLookup]
  119. );
  120. const formulaDependencies = useMemo(() => {
  121. return widgets.reduce((acc: Record<number, WidgetDependencies>, widget) => {
  122. if (widget.type === MetricQueryType.FORMULA) {
  123. acc[widget.id] = getFormulaQueryDependencies(widget.formula);
  124. }
  125. return acc;
  126. }, {});
  127. }, [getFormulaQueryDependencies, widgets]);
  128. const filteredWidgets = useMemo(() => {
  129. return widgets.filter(
  130. w =>
  131. w.type !== MetricQueryType.FORMULA || formulaDependencies[w.id]?.isError === false
  132. );
  133. }, [formulaDependencies, widgets]);
  134. return (
  135. <Wrapper>
  136. {isMultiChartMode ? (
  137. filteredWidgets.map((widget, index) => (
  138. <MultiChartWidgetQueries
  139. formulaDependencies={formulaDependencies}
  140. widget={widget}
  141. key={widget.id}
  142. >
  143. {queries => (
  144. <MetricWidget
  145. queryId={widget.id}
  146. index={index}
  147. getChartPalette={getChartPalette}
  148. onSelect={setSelectedWidgetIndex}
  149. displayType={widget.displayType}
  150. focusedSeries={widget.focusedSeries}
  151. tableSort={widget.sort}
  152. queries={queries}
  153. isSelected={selectedWidgetIndex === index}
  154. hasSiblings={widgets.length > 1}
  155. onChange={handleChange}
  156. filters={selection}
  157. focusAreaProps={focusArea}
  158. showQuerySymbols={showQuerySymbols}
  159. onSampleClick={handleSampleClick}
  160. chartHeight={200}
  161. highlightedSampleId={
  162. selectedWidgetIndex === index ? highlightedSampleId : undefined
  163. }
  164. context="ddm"
  165. />
  166. )}
  167. </MultiChartWidgetQueries>
  168. ))
  169. ) : (
  170. <MetricWidget
  171. index={0}
  172. getChartPalette={getChartPalette}
  173. onSelect={setSelectedWidgetIndex}
  174. displayType={firstWidget.displayType}
  175. focusedSeries={firstWidget.focusedSeries}
  176. tableSort={firstWidget.sort}
  177. queries={filteredWidgets.map(w => widgetToQuery(w))}
  178. isSelected
  179. hasSiblings={false}
  180. onChange={handleChange}
  181. filters={selection}
  182. focusAreaProps={focusArea}
  183. showQuerySymbols={false}
  184. onSampleClick={handleSampleClick}
  185. chartHeight={200}
  186. highlightedSampleId={highlightedSampleId}
  187. context="ddm"
  188. />
  189. )}
  190. </Wrapper>
  191. );
  192. }
  193. function MultiChartWidgetQueries({
  194. widget,
  195. formulaDependencies,
  196. children,
  197. }: {
  198. children: (queries: MetricsQueryApiQueryParams[]) => JSX.Element;
  199. formulaDependencies: Record<number, WidgetDependencies>;
  200. widget: MetricWidgetQueryParams;
  201. }) {
  202. const queries = useMemo(() => {
  203. return [
  204. widgetToQuery(widget),
  205. ...(widget.type === MetricQueryType.FORMULA
  206. ? formulaDependencies[widget.id]?.dependencies
  207. : []),
  208. ];
  209. }, [widget, formulaDependencies]);
  210. return children(queries);
  211. }
  212. const StyledMetricDashboard = styled('div')`
  213. display: grid;
  214. grid-template-columns: repeat(3, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  215. gap: ${space(2)};
  216. @media (max-width: ${props => props.theme.breakpoints.xxlarge}) {
  217. grid-template-columns: repeat(2, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  218. }
  219. @media (max-width: ${props => props.theme.breakpoints.xlarge}) {
  220. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  221. }
  222. grid-auto-rows: auto;
  223. `;
  224. const StyledSingleWidgetWrapper = styled('div')`
  225. display: grid;
  226. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  227. `;