widgetDetails.tsx 7.9 KB

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