widgetDetails.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import Feature from 'sentry/components/acl/feature';
  4. import {Button} from 'sentry/components/button';
  5. import HookOrDefault from 'sentry/components/hookOrDefault';
  6. import {
  7. type Field,
  8. MetricSamplesTable,
  9. SearchableMetricSamplesTable,
  10. } from 'sentry/components/metrics/metricSamplesTable';
  11. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {PageFilters} from 'sentry/types/core';
  15. import type {MetricAggregation, MRI} from 'sentry/types/metrics';
  16. import {defined} from 'sentry/utils';
  17. import {isVirtualMetric} from 'sentry/utils/metrics';
  18. import type {FocusedMetricsSeries, MetricsWidget} from 'sentry/utils/metrics/types';
  19. import {isMetricsEquationWidget} from 'sentry/utils/metrics/types';
  20. import type {MetricsSamplesResults} from 'sentry/utils/metrics/useMetricsSamples';
  21. import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import usePageFilters from 'sentry/utils/usePageFilters';
  24. import type {FocusAreaProps} from 'sentry/views/metrics/context';
  25. import {useMetricsContext} from 'sentry/views/metrics/context';
  26. import {extendQueryWithGroupBys} from 'sentry/views/metrics/utils';
  27. import {generateTracesRouteWithQuery} from 'sentry/views/traces/utils';
  28. export function WidgetDetails() {
  29. const {
  30. selectedWidgetIndex,
  31. widgets,
  32. focusArea,
  33. setHighlightedSampleId,
  34. setMetricsSamples,
  35. hasPerformanceMetrics,
  36. } = useMetricsContext();
  37. const selectedWidget = widgets[selectedWidgetIndex] as MetricsWidget | undefined;
  38. const handleSampleRowHover = useCallback(
  39. (sampleId?: string) => {
  40. setHighlightedSampleId(sampleId);
  41. },
  42. [setHighlightedSampleId]
  43. );
  44. if (!selectedWidget || isMetricsEquationWidget(selectedWidget)) {
  45. return <MetricDetails onRowHover={handleSampleRowHover} focusArea={focusArea} />;
  46. }
  47. const {mri, aggregation, query, condition, focusedSeries} = selectedWidget;
  48. return (
  49. <MetricDetails
  50. mri={mri}
  51. aggregation={aggregation}
  52. condition={condition}
  53. query={query}
  54. focusedSeries={focusedSeries}
  55. onRowHover={handleSampleRowHover}
  56. setMetricsSamples={setMetricsSamples}
  57. focusArea={focusArea}
  58. hasPerformanceMetrics={hasPerformanceMetrics}
  59. />
  60. );
  61. }
  62. interface MetricDetailsProps {
  63. aggregation?: MetricAggregation;
  64. condition?: number;
  65. focusArea?: FocusAreaProps;
  66. focusedSeries?: FocusedMetricsSeries[];
  67. hasPerformanceMetrics?: boolean;
  68. mri?: MRI;
  69. onRowHover?: (sampleId?: string) => void;
  70. query?: string;
  71. setMetricsSamples?: React.Dispatch<
  72. React.SetStateAction<MetricsSamplesResults<Field>['data'] | undefined>
  73. >;
  74. }
  75. export function MetricDetails({
  76. mri,
  77. aggregation,
  78. condition,
  79. query,
  80. focusedSeries,
  81. onRowHover,
  82. focusArea,
  83. setMetricsSamples,
  84. hasPerformanceMetrics,
  85. }: MetricDetailsProps) {
  86. const {selection} = usePageFilters();
  87. const organization = useOrganization();
  88. const {getCondition} = useVirtualMetricsContext();
  89. const queryWithFocusedSeries = useMemo(
  90. () =>
  91. focusedSeries &&
  92. extendQueryWithGroupBys(
  93. query || '',
  94. focusedSeries.map(s => s.groupBy)
  95. ),
  96. [focusedSeries, query]
  97. );
  98. const selectionRange = focusArea?.selection?.range;
  99. const tracesTarget = useMemo(() => {
  100. const selectionDatetime =
  101. defined(selectionRange) && defined(selectionRange) && defined(selectionRange)
  102. ? ({
  103. start: selectionRange.start,
  104. end: selectionRange.end,
  105. } as PageFilters['datetime'])
  106. : undefined;
  107. if (mri && isVirtualMetric({mri})) {
  108. const conditionQuery = getCondition(mri, condition || -1)?.value || '';
  109. return generateTracesRouteWithQuery({
  110. orgSlug: organization.slug,
  111. query: {
  112. project: selection.projects as unknown as string[],
  113. environment: selection.environments,
  114. ...normalizeDateTimeParams(selectionDatetime ?? selection.datetime),
  115. query: `${conditionQuery.trim()} ${queryWithFocusedSeries?.trim()}`,
  116. },
  117. });
  118. }
  119. if (aggregation && mri) {
  120. return generateTracesRouteWithQuery({
  121. orgSlug: organization.slug,
  122. metric: {
  123. max: selectionRange?.max,
  124. min: selectionRange?.min,
  125. op: aggregation,
  126. query: queryWithFocusedSeries,
  127. mri: mri,
  128. },
  129. query: {
  130. project: selection.projects as unknown as string[],
  131. environment: selection.environments,
  132. ...normalizeDateTimeParams(selectionDatetime ?? selection.datetime),
  133. },
  134. });
  135. }
  136. return '';
  137. }, [
  138. aggregation,
  139. mri,
  140. organization.slug,
  141. queryWithFocusedSeries,
  142. selection,
  143. selectionRange,
  144. condition,
  145. getCondition,
  146. ]);
  147. return (
  148. <TrayWrapper>
  149. <TabsAndAction>
  150. <Heading>{t('Span Samples')}</Heading>
  151. <Feature
  152. features={[
  153. 'performance-trace-explorer-with-metrics',
  154. 'performance-trace-explorer',
  155. ]}
  156. requireAll
  157. >
  158. <OpenInTracesButton to={tracesTarget} size="sm">
  159. {t('Open in Traces')}
  160. </OpenInTracesButton>
  161. </Feature>
  162. </TabsAndAction>
  163. <ContentWrapper>
  164. <MetricSampleTableWrapper organization={organization}>
  165. {organization.features.includes('metrics-samples-list-search') ? (
  166. <SearchableMetricSamplesTable
  167. focusArea={selectionRange}
  168. mri={mri}
  169. onRowHover={onRowHover}
  170. aggregation={aggregation}
  171. condition={condition}
  172. query={queryWithFocusedSeries}
  173. setMetricsSamples={setMetricsSamples}
  174. hasPerformance={hasPerformanceMetrics}
  175. />
  176. ) : (
  177. <MetricSamplesTable
  178. focusArea={selectionRange}
  179. mri={mri}
  180. onRowHover={onRowHover}
  181. aggregation={aggregation}
  182. condition={condition}
  183. query={queryWithFocusedSeries}
  184. setMetricsSamples={setMetricsSamples}
  185. hasPerformance={hasPerformanceMetrics}
  186. />
  187. )}
  188. </MetricSampleTableWrapper>
  189. </ContentWrapper>
  190. </TrayWrapper>
  191. );
  192. }
  193. const MetricSampleTableWrapper = HookOrDefault({
  194. hookName: 'component:ddm-metrics-samples-list',
  195. defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
  196. });
  197. const Heading = styled('h6')`
  198. margin-bottom: ${space(0.5)};
  199. `;
  200. const TrayWrapper = styled('div')`
  201. padding-top: ${space(4)};
  202. display: grid;
  203. grid-template-rows: auto auto 1fr;
  204. `;
  205. const ContentWrapper = styled('div')`
  206. position: relative;
  207. padding-top: ${space(1)};
  208. `;
  209. const OpenInTracesButton = styled(Button)``;
  210. const TabsAndAction = styled('div')`
  211. display: grid;
  212. grid-template-columns: 1fr auto;
  213. gap: ${space(4)};
  214. align-items: end;
  215. `;