scratchpad.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import {useCallback, useLayoutEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as echarts from 'echarts/core';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import type {Field} from 'sentry/components/metrics/metricSamplesTable';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {getMetricsCorrelationSpanUrl} from 'sentry/utils/metrics';
  9. import {
  10. isMetricsEquationWidget,
  11. MetricExpressionType,
  12. type MetricsWidget,
  13. } from 'sentry/utils/metrics/types';
  14. import type {MetricsQueryApiQueryParams} from 'sentry/utils/metrics/useMetricsQuery';
  15. import type {MetricsSamplesResults} from 'sentry/utils/metrics/useMetricsSamples';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import usePageFilters from 'sentry/utils/usePageFilters';
  18. import useRouter from 'sentry/utils/useRouter';
  19. import {DDM_CHART_GROUP, MIN_WIDGET_WIDTH} from 'sentry/views/metrics/constants';
  20. import {useMetricsContext} from 'sentry/views/metrics/context';
  21. import {useGetCachedChartPalette} from 'sentry/views/metrics/utils/metricsChartPalette';
  22. import {useFormulaDependencies} from 'sentry/views/metrics/utils/useFormulaDependencies';
  23. import {widgetToQuery} from 'sentry/views/metrics/utils/widgetToQuery';
  24. import {MetricWidget} from './widget';
  25. export function MetricScratchpad() {
  26. const {
  27. setSelectedWidgetIndex,
  28. selectedWidgetIndex,
  29. widgets,
  30. updateWidget,
  31. showQuerySymbols,
  32. highlightedSampleId,
  33. focusArea,
  34. isMultiChartMode,
  35. metricsSamples,
  36. } = useMetricsContext();
  37. const {selection} = usePageFilters();
  38. const router = useRouter();
  39. const organization = useOrganization();
  40. const getChartPalette = useGetCachedChartPalette();
  41. // Make sure all charts are connected to the same group whenever the widgets definition changes
  42. useLayoutEffect(() => {
  43. echarts.connect(DDM_CHART_GROUP);
  44. }, [widgets]);
  45. const handleChange = useCallback(
  46. (index: number, widget: Partial<MetricsWidget>) => {
  47. updateWidget(index, widget);
  48. },
  49. [updateWidget]
  50. );
  51. const handleSampleClick = useCallback(
  52. (sample: MetricsSamplesResults<Field>['data'][number]) => {
  53. if (!sample['transaction.id']) {
  54. addErrorMessage(t('No matching transaction found'));
  55. return;
  56. }
  57. router.push(
  58. getMetricsCorrelationSpanUrl(
  59. organization,
  60. sample.project,
  61. sample.id,
  62. sample['transaction.id'],
  63. sample['segment.id']
  64. )
  65. );
  66. },
  67. [router, organization]
  68. );
  69. const firstWidget = widgets[0];
  70. const Wrapper =
  71. !isMultiChartMode || widgets.length === 1
  72. ? StyledSingleWidgetWrapper
  73. : StyledMetricDashboard;
  74. const formulaDependencies = useFormulaDependencies();
  75. const filteredWidgets = useMemo(() => {
  76. return widgets.filter(
  77. w =>
  78. w.type !== MetricExpressionType.EQUATION ||
  79. formulaDependencies[w.id]?.isError === false
  80. );
  81. }, [formulaDependencies, widgets]);
  82. return (
  83. <Wrapper>
  84. {isMultiChartMode ? (
  85. filteredWidgets.map((widget, index) =>
  86. widget.isHidden ? null : (
  87. <MultiChartWidgetQueries
  88. formulaDependencies={formulaDependencies}
  89. widget={widget}
  90. key={`${widget.type}_${widget.id}`}
  91. >
  92. {queries => (
  93. <MetricWidget
  94. queryId={widget.id}
  95. index={index}
  96. getChartPalette={getChartPalette}
  97. onSelect={setSelectedWidgetIndex}
  98. displayType={widget.displayType}
  99. focusedSeries={widget.focusedSeries}
  100. tableSort={widget.sort}
  101. queries={queries}
  102. isSelected={selectedWidgetIndex === index}
  103. hasSiblings={widgets.length > 1}
  104. onChange={handleChange}
  105. filters={selection}
  106. focusAreaProps={focusArea}
  107. showQuerySymbols={showQuerySymbols}
  108. onSampleClick={handleSampleClick}
  109. chartHeight={200}
  110. highlightedSampleId={
  111. selectedWidgetIndex === index ? highlightedSampleId : undefined
  112. }
  113. metricsSamples={metricsSamples}
  114. />
  115. )}
  116. </MultiChartWidgetQueries>
  117. )
  118. )
  119. ) : (
  120. <MetricWidget
  121. index={0}
  122. getChartPalette={getChartPalette}
  123. onSelect={setSelectedWidgetIndex}
  124. displayType={firstWidget.displayType}
  125. focusedSeries={firstWidget.focusedSeries}
  126. tableSort={firstWidget.sort}
  127. queries={filteredWidgets
  128. .filter(w => !(w.type === MetricExpressionType.EQUATION && w.isHidden))
  129. .map(w => widgetToQuery(w))}
  130. isSelected
  131. hasSiblings={false}
  132. onChange={handleChange}
  133. filters={selection}
  134. focusAreaProps={focusArea}
  135. showQuerySymbols={false}
  136. onSampleClick={handleSampleClick}
  137. chartHeight={200}
  138. highlightedSampleId={highlightedSampleId}
  139. metricsSamples={metricsSamples}
  140. />
  141. )}
  142. </Wrapper>
  143. );
  144. }
  145. function MultiChartWidgetQueries({
  146. widget,
  147. formulaDependencies,
  148. children,
  149. }: {
  150. children: (queries: MetricsQueryApiQueryParams[]) => JSX.Element;
  151. formulaDependencies: ReturnType<typeof useFormulaDependencies>;
  152. widget: MetricsWidget;
  153. }) {
  154. const queries = useMemo(() => {
  155. return [
  156. widgetToQuery(widget),
  157. ...(isMetricsEquationWidget(widget)
  158. ? formulaDependencies[widget.id]?.dependencies?.map(dependency =>
  159. widgetToQuery(dependency, true)
  160. )
  161. : []),
  162. ];
  163. }, [widget, formulaDependencies]);
  164. return children(queries);
  165. }
  166. const StyledMetricDashboard = styled('div')`
  167. display: grid;
  168. grid-template-columns: repeat(3, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  169. gap: ${space(2)};
  170. @media (max-width: ${props => props.theme.breakpoints.xxlarge}) {
  171. grid-template-columns: repeat(2, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  172. }
  173. @media (max-width: ${props => props.theme.breakpoints.xlarge}) {
  174. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  175. }
  176. grid-auto-rows: auto;
  177. `;
  178. const StyledSingleWidgetWrapper = styled('div')`
  179. display: grid;
  180. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  181. `;