scratchpad.tsx 7.9 KB

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