scratchpad.tsx 8.1 KB

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