scratchpad.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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 useOrganization from 'sentry/utils/useOrganization';
  19. import usePageFilters from 'sentry/utils/usePageFilters';
  20. import useRouter from 'sentry/utils/useRouter';
  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/traceMetadataHeader';
  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 router = useRouter();
  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. const isTransaction = sample.id === sample['segment.id'];
  63. const dataRow: {
  64. id: string;
  65. project: string;
  66. trace: string;
  67. timestamp?: number;
  68. } = {
  69. id: sample['transaction.id'],
  70. project: sample.project,
  71. trace: sample.trace,
  72. };
  73. if (sample.timestamp) {
  74. const timestamp = new Date(sample.timestamp).getTime();
  75. if (!isNaN(timestamp)) {
  76. dataRow.timestamp = timestamp / 1000;
  77. }
  78. }
  79. router.push(
  80. generateLinkToEventInTraceView({
  81. traceSlug: dataRow.trace,
  82. projectSlug: dataRow.project,
  83. eventId: dataRow.id,
  84. timestamp: dataRow.timestamp ?? '',
  85. location: {
  86. ...location,
  87. query: {...location.query, referrer: 'metrics', openPanel: 'open'},
  88. },
  89. organization,
  90. transactionName: isTransaction ? sample.transaction : undefined,
  91. spanId: isTransaction ? sample.id : undefined,
  92. source: TraceViewSources.METRICS,
  93. })
  94. );
  95. },
  96. [router, organization, location]
  97. );
  98. const firstWidget = widgets[0];
  99. const Wrapper =
  100. !isMultiChartMode || widgets.length === 1
  101. ? StyledSingleWidgetWrapper
  102. : StyledMetricDashboard;
  103. const formulaDependencies = useFormulaDependencies();
  104. const filteredWidgets = useMemo(() => {
  105. return widgets.filter(
  106. w =>
  107. w.type !== MetricExpressionType.EQUATION ||
  108. formulaDependencies[w.id]?.isError === false
  109. );
  110. }, [formulaDependencies, widgets]);
  111. return (
  112. <Wrapper>
  113. {isMultiChartMode ? (
  114. filteredWidgets.map((widget, index) =>
  115. widget.isHidden ? null : (
  116. <MultiChartWidgetQueries
  117. formulaDependencies={formulaDependencies}
  118. widget={widget}
  119. key={`${widget.type}_${widget.id}`}
  120. >
  121. {queries => (
  122. <MetricWidget
  123. queryId={widget.id}
  124. index={index}
  125. getChartPalette={getChartPalette}
  126. onSelect={setSelectedWidgetIndex}
  127. displayType={widget.displayType}
  128. focusedSeries={widget.focusedSeries}
  129. tableSort={widget.sort}
  130. queries={queries}
  131. isSelected={selectedWidgetIndex === index}
  132. hasSiblings={widgets.length > 1}
  133. onChange={handleChange}
  134. filters={selection}
  135. focusAreaProps={focusArea}
  136. showQuerySymbols={showQuerySymbols}
  137. onSampleClick={handleSampleClick}
  138. chartHeight={200}
  139. highlightedSampleId={
  140. selectedWidgetIndex === index ? highlightedSampleId : undefined
  141. }
  142. metricsSamples={metricsSamples}
  143. overlays={widget.overlays}
  144. />
  145. )}
  146. </MultiChartWidgetQueries>
  147. )
  148. )
  149. ) : (
  150. <MetricWidget
  151. index={0}
  152. getChartPalette={getChartPalette}
  153. onSelect={setSelectedWidgetIndex}
  154. displayType={firstWidget.displayType}
  155. focusedSeries={firstWidget.focusedSeries}
  156. tableSort={firstWidget.sort}
  157. queries={filteredWidgets
  158. .filter(w => !(w.type === MetricExpressionType.EQUATION && w.isHidden))
  159. .map(w => widgetToQuery({widget: w, metricsNewInputs}))}
  160. isSelected
  161. hasSiblings={false}
  162. onChange={handleChange}
  163. filters={selection}
  164. focusAreaProps={focusArea}
  165. showQuerySymbols={false}
  166. onSampleClick={handleSampleClick}
  167. chartHeight={200}
  168. highlightedSampleId={highlightedSampleId}
  169. metricsSamples={metricsSamples}
  170. overlays={firstWidget.overlays}
  171. />
  172. )}
  173. </Wrapper>
  174. );
  175. }
  176. function MultiChartWidgetQueries({
  177. widget,
  178. formulaDependencies,
  179. children,
  180. }: {
  181. children: (queries: MetricsQueryApiQueryParams[]) => JSX.Element;
  182. formulaDependencies: ReturnType<typeof useFormulaDependencies>;
  183. widget: MetricsWidget;
  184. }) {
  185. const organization = useOrganization();
  186. const metricsNewInputs = hasMetricsNewInputs(organization);
  187. const queries = useMemo(() => {
  188. return [
  189. widgetToQuery({widget, metricsNewInputs}),
  190. ...(isMetricsEquationWidget(widget)
  191. ? formulaDependencies[widget.id]?.dependencies?.map(dependency =>
  192. widgetToQuery({widget: dependency, isQueryOnly: true, metricsNewInputs})
  193. )
  194. : []),
  195. ];
  196. }, [widget, formulaDependencies, metricsNewInputs]);
  197. return children(queries);
  198. }
  199. const StyledMetricDashboard = styled('div')`
  200. display: grid;
  201. grid-template-columns: repeat(3, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  202. gap: ${space(2)};
  203. @media (max-width: ${props => props.theme.breakpoints.xxlarge}) {
  204. grid-template-columns: repeat(2, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  205. }
  206. @media (max-width: ${props => props.theme.breakpoints.xlarge}) {
  207. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  208. }
  209. grid-auto-rows: auto;
  210. `;
  211. const StyledSingleWidgetWrapper = styled('div')`
  212. display: grid;
  213. grid-template-columns: repeat(1, minmax(${MIN_WIDGET_WIDTH}px, 1fr));
  214. `;