scratchpad.tsx 7.3 KB

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