scratchpad.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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} 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 {getQuerySymbol} from 'sentry/views/ddm/querySymbol';
  15. import {useGetCachedChartPalette} from 'sentry/views/ddm/utils/metricsChartPalette';
  16. import type {Sample} from './widget';
  17. import {MetricWidget} from './widget';
  18. function widgetToQuery(
  19. widget: MetricWidgetQueryParams,
  20. queryLookup: Map<string, any>,
  21. isQueryOnly = false
  22. ): MetricsQueryApiQueryParams {
  23. return widget.type === MetricQueryType.FORMULA
  24. ? {
  25. name: getQuerySymbol(widget.id),
  26. // TODO(aknaus): Properly parse formulas to format identifiers
  27. // This solution is limited to single character identifiers
  28. formula: widget.formula
  29. .split('')
  30. .map(char => (queryLookup.has(char) ? `$${char}` : char))
  31. .join(''),
  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 getFormulasQueryDependencies = useCallback(
  96. (formula: string): MetricsQueryApiQueryParams[] => {
  97. const children = formula
  98. .split('')
  99. .map(char => queriesLookup.get(char))
  100. .filter((w): w is Exclude<typeof w, undefined> => !!w);
  101. const dependencies: MetricsQueryApiQueryParams[] = [];
  102. // ATM we recursively iterate over child formulas to find all dependencies
  103. // TODO(aknaus): clarify API support for this
  104. // TODO(aknaus): Memoize this
  105. children.forEach(child => {
  106. if (child.type === MetricQueryType.FORMULA) {
  107. dependencies.push(widgetToQuery(child, queriesLookup, true));
  108. dependencies.push(...getFormulasQueryDependencies(child.formula));
  109. } else {
  110. dependencies.push(widgetToQuery(child, queriesLookup, true));
  111. }
  112. });
  113. return dependencies;
  114. },
  115. [queriesLookup]
  116. );
  117. return (
  118. <Wrapper>
  119. {isMultiChartMode ? (
  120. widgets.map((widget, index) => (
  121. <MetricWidget
  122. queryId={widget.id}
  123. key={index}
  124. index={index}
  125. getChartPalette={getChartPalette}
  126. onSelect={setSelectedWidgetIndex}
  127. displayType={widget.displayType}
  128. focusedSeries={widget.focusedSeries}
  129. tableSort={widget.sort}
  130. queries={[
  131. widgetToQuery(widget, queriesLookup),
  132. ...(widget.type === MetricQueryType.FORMULA
  133. ? // TODO(aknaus): Properly parse formulas to extract identifiers
  134. // This solution is limited to single character identifiers
  135. getFormulasQueryDependencies(widget.formula)
  136. : []),
  137. ]}
  138. isSelected={selectedWidgetIndex === index}
  139. hasSiblings={widgets.length > 1}
  140. onChange={handleChange}
  141. filters={selection}
  142. focusArea={focusArea}
  143. showQuerySymbols={showQuerySymbols}
  144. onSampleClick={handleSampleClick}
  145. chartHeight={200}
  146. highlightedSampleId={
  147. selectedWidgetIndex === index ? highlightedSampleId : undefined
  148. }
  149. context="ddm"
  150. />
  151. ))
  152. ) : (
  153. <MetricWidget
  154. index={0}
  155. getChartPalette={getChartPalette}
  156. onSelect={setSelectedWidgetIndex}
  157. displayType={firstWidget.displayType}
  158. focusedSeries={firstWidget.focusedSeries}
  159. tableSort={firstWidget.sort}
  160. queries={widgets.map(w => widgetToQuery(w, queriesLookup))}
  161. isSelected
  162. hasSiblings={false}
  163. onChange={handleChange}
  164. filters={selection}
  165. focusArea={focusArea}
  166. showQuerySymbols={false}
  167. onSampleClick={handleSampleClick}
  168. chartHeight={200}
  169. highlightedSampleId={highlightedSampleId}
  170. context="ddm"
  171. />
  172. )}
  173. </Wrapper>
  174. );
  175. }
  176. const StyledMetricDashboard = styled('div')`
  177. display: grid;
  178. grid-template-columns: repeat(3, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  179. gap: ${space(2)};
  180. @media (max-width: ${props => props.theme.breakpoints.xxlarge}) {
  181. grid-template-columns: repeat(2, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  182. }
  183. @media (max-width: ${props => props.theme.breakpoints.xlarge}) {
  184. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  185. }
  186. grid-auto-rows: auto;
  187. `;
  188. const StyledSingleWidgetWrapper = styled('div')`
  189. display: grid;
  190. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  191. `;