widgetDetails.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {Fragment, useCallback, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import omit from 'lodash/omit';
  4. import Feature from 'sentry/components/acl/feature';
  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 {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
  13. import {Tooltip} from 'sentry/components/tooltip';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {MRI} from 'sentry/types/metrics';
  17. import {trackAnalytics} from 'sentry/utils/analytics';
  18. import {isCustomMetric} from 'sentry/utils/metrics';
  19. import type {
  20. FocusedMetricsSeries,
  21. MetricsQueryWidget,
  22. MetricsWidget,
  23. } from 'sentry/utils/metrics/types';
  24. import {MetricExpressionType} from 'sentry/utils/metrics/types';
  25. import type {MetricsSamplesResults} from 'sentry/utils/metrics/useMetricsSamples';
  26. import {useLocation} from 'sentry/utils/useLocation';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import {CodeLocations} from 'sentry/views/metrics/codeLocations';
  29. import type {FocusAreaProps} from 'sentry/views/metrics/context';
  30. import {useMetricsContext} from 'sentry/views/metrics/context';
  31. import {extendQueryWithGroupBys} from 'sentry/views/metrics/utils';
  32. import {generateTracesRouteWithQuery} from 'sentry/views/performance/traces/utils';
  33. enum Tab {
  34. SAMPLES = 'samples',
  35. CODE_LOCATIONS = 'codeLocations',
  36. }
  37. export function WidgetDetails() {
  38. const {
  39. selectedWidgetIndex,
  40. widgets,
  41. focusArea,
  42. setHighlightedSampleId,
  43. setMetricsSamples,
  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. // TODO(aknaus): better fallback
  53. if (selectedWidget?.type === MetricExpressionType.EQUATION) {
  54. <MetricDetails onRowHover={handleSampleRowHover} focusArea={focusArea} />;
  55. }
  56. const {mri, op, query, focusedSeries} = selectedWidget as MetricsQueryWidget;
  57. return (
  58. <MetricDetails
  59. mri={mri}
  60. op={op}
  61. query={query}
  62. focusedSeries={focusedSeries}
  63. onRowHover={handleSampleRowHover}
  64. setMetricsSamples={setMetricsSamples}
  65. focusArea={focusArea}
  66. />
  67. );
  68. }
  69. interface MetricDetailsProps {
  70. focusArea?: FocusAreaProps;
  71. focusedSeries?: FocusedMetricsSeries[];
  72. mri?: MRI;
  73. onRowHover?: (sampleId?: string) => void;
  74. op?: string;
  75. query?: string;
  76. setMetricsSamples?: React.Dispatch<
  77. React.SetStateAction<MetricsSamplesResults<Field>['data'] | undefined>
  78. >;
  79. }
  80. export function MetricDetails({
  81. mri,
  82. op,
  83. query,
  84. focusedSeries,
  85. onRowHover,
  86. focusArea,
  87. setMetricsSamples,
  88. }: MetricDetailsProps) {
  89. const location = useLocation();
  90. const organization = useOrganization();
  91. const [selectedTab, setSelectedTab] = useState(Tab.SAMPLES);
  92. const isCodeLocationsDisabled = mri && !isCustomMetric({mri});
  93. if (isCodeLocationsDisabled && selectedTab === Tab.CODE_LOCATIONS) {
  94. setSelectedTab(Tab.SAMPLES);
  95. }
  96. const queryWithFocusedSeries = useMemo(
  97. () =>
  98. focusedSeries &&
  99. extendQueryWithGroupBys(
  100. query || '',
  101. focusedSeries.map(s => s.groupBy)
  102. ),
  103. [focusedSeries, query]
  104. );
  105. const handleTabChange = useCallback(
  106. (tab: Tab) => {
  107. if (tab === Tab.CODE_LOCATIONS) {
  108. trackAnalytics('ddm.code-locations', {
  109. organization,
  110. });
  111. }
  112. setSelectedTab(tab);
  113. },
  114. [organization]
  115. );
  116. const tracesTarget = generateTracesRouteWithQuery({
  117. orgSlug: organization.slug,
  118. metric:
  119. op && mri
  120. ? {
  121. metricsOp: op,
  122. mri,
  123. metricsQuery: queryWithFocusedSeries,
  124. }
  125. : undefined,
  126. query: omit(location.query, ['widgets', 'interval']),
  127. });
  128. return (
  129. <TrayWrapper>
  130. <Tabs value={selectedTab} onChange={handleTabChange}>
  131. <TabList>
  132. <TabList.Item key={Tab.SAMPLES}>{t('Sampled Events')}</TabList.Item>
  133. <TabList.Item
  134. textValue={t('Code Location')}
  135. key={Tab.CODE_LOCATIONS}
  136. disabled={isCodeLocationsDisabled}
  137. >
  138. <Tooltip
  139. title={t(
  140. 'This metric is automatically collected by Sentry. It is not bound to a specific line of your code.'
  141. )}
  142. disabled={!isCodeLocationsDisabled}
  143. >
  144. <span style={{pointerEvents: 'all'}}>{t('Code Location')}</span>
  145. </Tooltip>
  146. </TabList.Item>
  147. </TabList>
  148. <ContentWrapper>
  149. <TabPanels>
  150. <TabPanels.Item key={Tab.SAMPLES}>
  151. <MetricSampleTableWrapper organization={organization}>
  152. {organization.features.includes('metrics-samples-list-search') ? (
  153. <SearchableMetricSamplesTable
  154. focusArea={focusArea?.selection?.range}
  155. mri={mri}
  156. onRowHover={onRowHover}
  157. op={op}
  158. query={queryWithFocusedSeries}
  159. setMetricsSamples={setMetricsSamples}
  160. />
  161. ) : (
  162. <MetricSamplesTable
  163. focusArea={focusArea?.selection?.range}
  164. mri={mri}
  165. onRowHover={onRowHover}
  166. op={op}
  167. query={queryWithFocusedSeries}
  168. setMetricsSamples={setMetricsSamples}
  169. />
  170. )}
  171. </MetricSampleTableWrapper>
  172. <Feature features="performance-trace-explorer-with-metrics">
  173. <OpenInTracesWrapper>
  174. <Button to={tracesTarget}>{t('Open in Traces')}</Button>
  175. </OpenInTracesWrapper>
  176. </Feature>
  177. </TabPanels.Item>
  178. <TabPanels.Item key={Tab.CODE_LOCATIONS}>
  179. <CodeLocations mri={mri} {...focusArea?.selection?.range} />
  180. </TabPanels.Item>
  181. </TabPanels>
  182. </ContentWrapper>
  183. </Tabs>
  184. </TrayWrapper>
  185. );
  186. }
  187. const MetricSampleTableWrapper = HookOrDefault({
  188. hookName: 'component:ddm-metrics-samples-list',
  189. defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
  190. });
  191. const TrayWrapper = styled('div')`
  192. padding-top: ${space(4)};
  193. display: grid;
  194. grid-template-rows: auto auto 1fr;
  195. `;
  196. const ContentWrapper = styled('div')`
  197. position: relative;
  198. padding-top: ${space(2)};
  199. `;
  200. const OpenInTracesWrapper = styled('div')`
  201. display: flex;
  202. justify-content: flex-end;
  203. `;