scratchpad.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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 {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  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 {useLocation} from 'sentry/utils/useLocation';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. import useRouter from 'sentry/utils/useRouter';
  20. import {METRIC_CHART_GROUP, MIN_WIDGET_WIDTH} from 'sentry/views/metrics/constants';
  21. import {useMetricsContext} from 'sentry/views/metrics/context';
  22. import {useGetCachedChartPalette} from 'sentry/views/metrics/utils/metricsChartPalette';
  23. import {useFormulaDependencies} from 'sentry/views/metrics/utils/useFormulaDependencies';
  24. import {widgetToQuery} from 'sentry/views/metrics/utils/widgetToQuery';
  25. import {TraceViewSources} from '../performance/newTraceDetails/traceMetadataHeader';
  26. import {MetricWidget} from './widget';
  27. export function MetricScratchpad() {
  28. const {
  29. setSelectedWidgetIndex,
  30. selectedWidgetIndex,
  31. widgets,
  32. updateWidget,
  33. showQuerySymbols,
  34. highlightedSampleId,
  35. focusArea,
  36. isMultiChartMode,
  37. metricsSamples,
  38. } = useMetricsContext();
  39. const {selection} = usePageFilters();
  40. const location = useLocation();
  41. const router = useRouter();
  42. const organization = useOrganization();
  43. const getChartPalette = useGetCachedChartPalette();
  44. // Make sure all charts are connected to the same group whenever the widgets definition changes
  45. useLayoutEffect(() => {
  46. echarts.connect(METRIC_CHART_GROUP);
  47. }, [widgets]);
  48. const handleChange = useCallback(
  49. (index: number, widget: Partial<MetricsWidget>) => {
  50. updateWidget(index, widget);
  51. },
  52. [updateWidget]
  53. );
  54. const handleSampleClick = useCallback(
  55. (sample: MetricsSamplesResults<Field>['data'][number]) => {
  56. if (!sample['transaction.id']) {
  57. addErrorMessage(t('No matching transaction found'));
  58. return;
  59. }
  60. const isTransaction = sample.id === sample['segment.id'];
  61. const dataRow: {
  62. id: string;
  63. project: string;
  64. trace: string;
  65. timestamp?: number;
  66. } = {
  67. id: sample['transaction.id'],
  68. project: sample.project,
  69. trace: sample.trace,
  70. };
  71. if (sample.timestamp) {
  72. const timestamp = new Date(sample.timestamp).getTime();
  73. if (!isNaN(timestamp)) {
  74. dataRow.timestamp = timestamp / 1000;
  75. }
  76. }
  77. router.push(
  78. generateLinkToEventInTraceView({
  79. traceSlug: dataRow.trace,
  80. projectSlug: dataRow.project,
  81. eventId: dataRow.id,
  82. timestamp: dataRow.timestamp ?? '',
  83. location: {
  84. ...location,
  85. query: {...location.query, referrer: 'metrics', openPanel: 'open'},
  86. },
  87. organization,
  88. transactionName: isTransaction ? sample.transaction : undefined,
  89. spanId: isTransaction ? sample.id : undefined,
  90. source: TraceViewSources.METRICS,
  91. })
  92. );
  93. },
  94. [router, organization, location]
  95. );
  96. const firstWidget = widgets[0];
  97. const Wrapper =
  98. !isMultiChartMode || widgets.length === 1
  99. ? StyledSingleWidgetWrapper
  100. : StyledMetricDashboard;
  101. const formulaDependencies = useFormulaDependencies();
  102. const filteredWidgets = useMemo(() => {
  103. return widgets.filter(
  104. w =>
  105. w.type !== MetricExpressionType.EQUATION ||
  106. formulaDependencies[w.id]?.isError === false
  107. );
  108. }, [formulaDependencies, widgets]);
  109. return (
  110. <Wrapper>
  111. {isMultiChartMode ? (
  112. filteredWidgets.map((widget, index) =>
  113. widget.isHidden ? null : (
  114. <MultiChartWidgetQueries
  115. formulaDependencies={formulaDependencies}
  116. widget={widget}
  117. key={`${widget.type}_${widget.id}`}
  118. >
  119. {queries => (
  120. <MetricWidget
  121. queryId={widget.id}
  122. index={index}
  123. getChartPalette={getChartPalette}
  124. onSelect={setSelectedWidgetIndex}
  125. displayType={widget.displayType}
  126. focusedSeries={widget.focusedSeries}
  127. tableSort={widget.sort}
  128. queries={queries}
  129. isSelected={selectedWidgetIndex === index}
  130. hasSiblings={widgets.length > 1}
  131. onChange={handleChange}
  132. filters={selection}
  133. focusAreaProps={focusArea}
  134. showQuerySymbols={showQuerySymbols}
  135. onSampleClick={handleSampleClick}
  136. chartHeight={200}
  137. highlightedSampleId={
  138. selectedWidgetIndex === index ? highlightedSampleId : undefined
  139. }
  140. metricsSamples={metricsSamples}
  141. overlays={widget.overlays}
  142. />
  143. )}
  144. </MultiChartWidgetQueries>
  145. )
  146. )
  147. ) : (
  148. <MetricWidget
  149. index={0}
  150. getChartPalette={getChartPalette}
  151. onSelect={setSelectedWidgetIndex}
  152. displayType={firstWidget.displayType}
  153. focusedSeries={firstWidget.focusedSeries}
  154. tableSort={firstWidget.sort}
  155. queries={filteredWidgets
  156. .filter(w => !(w.type === MetricExpressionType.EQUATION && w.isHidden))
  157. .map(w => widgetToQuery(w))}
  158. isSelected
  159. hasSiblings={false}
  160. onChange={handleChange}
  161. filters={selection}
  162. focusAreaProps={focusArea}
  163. showQuerySymbols={false}
  164. onSampleClick={handleSampleClick}
  165. chartHeight={200}
  166. highlightedSampleId={highlightedSampleId}
  167. metricsSamples={metricsSamples}
  168. overlays={firstWidget.overlays}
  169. />
  170. )}
  171. </Wrapper>
  172. );
  173. }
  174. function MultiChartWidgetQueries({
  175. widget,
  176. formulaDependencies,
  177. children,
  178. }: {
  179. children: (queries: MetricsQueryApiQueryParams[]) => JSX.Element;
  180. formulaDependencies: ReturnType<typeof useFormulaDependencies>;
  181. widget: MetricsWidget;
  182. }) {
  183. const queries = useMemo(() => {
  184. return [
  185. widgetToQuery(widget),
  186. ...(isMetricsEquationWidget(widget)
  187. ? formulaDependencies[widget.id]?.dependencies?.map(dependency =>
  188. widgetToQuery(dependency, true)
  189. )
  190. : []),
  191. ];
  192. }, [widget, formulaDependencies]);
  193. return children(queries);
  194. }
  195. const StyledMetricDashboard = styled('div')`
  196. display: grid;
  197. grid-template-columns: repeat(3, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  198. gap: ${space(2)};
  199. @media (max-width: ${props => props.theme.breakpoints.xxlarge}) {
  200. grid-template-columns: repeat(2, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  201. }
  202. @media (max-width: ${props => props.theme.breakpoints.xlarge}) {
  203. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  204. }
  205. grid-auto-rows: auto;
  206. `;
  207. const StyledSingleWidgetWrapper = styled('div')`
  208. display: grid;
  209. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  210. `;