scratchpad.tsx 6.3 KB

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