widgetDetails.tsx 5.7 KB

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