widgetDetails.tsx 7.7 KB

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