customMetricsTable.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import {Fragment, useMemo, useState} from 'react';
  2. import {Link} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import Tag from 'sentry/components/badge/tag';
  5. import {PanelTable} from 'sentry/components/panels/panelTable';
  6. import SearchBar from 'sentry/components/searchBar';
  7. import {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
  8. import {Tooltip} from 'sentry/components/tooltip';
  9. import {IconArrow} from 'sentry/icons';
  10. import {IconWarning} from 'sentry/icons/iconWarning';
  11. import {t, tct} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {MetricMeta} from 'sentry/types/metrics';
  14. import type {Project} from 'sentry/types/project';
  15. import {DEFAULT_METRICS_CARDINALITY_LIMIT} from 'sentry/utils/metrics/constants';
  16. import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
  17. import {formatMRI} from 'sentry/utils/metrics/mri';
  18. import {useBlockMetric} from 'sentry/utils/metrics/useBlockMetric';
  19. import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
  20. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  21. import {middleEllipsis} from 'sentry/utils/string/middleEllipsis';
  22. import {useAccess} from 'sentry/views/settings/projectMetrics/access';
  23. import {BlockButton} from 'sentry/views/settings/projectMetrics/blockButton';
  24. import {useSearchQueryParam} from 'sentry/views/settings/projectMetrics/utils/useSearchQueryParam';
  25. type Props = {
  26. project: Project;
  27. };
  28. enum BlockingStatusTab {
  29. ACTIVE = 'active',
  30. DISABLED = 'disabled',
  31. }
  32. type MetricWithCardinality = MetricMeta & {cardinality: number};
  33. export function CustomMetricsTable({project}: Props) {
  34. const [selectedTab, setSelectedTab] = useState(BlockingStatusTab.ACTIVE);
  35. const [query, setQuery] = useSearchQueryParam('metricsQuery');
  36. const metricsMeta = useMetricsMeta(
  37. {projects: [parseInt(project.id, 10)]},
  38. ['custom'],
  39. false
  40. );
  41. const metricsCardinality = useMetricsCardinality({
  42. project,
  43. });
  44. const isLoading = metricsMeta.isLoading || metricsCardinality.isLoading;
  45. const sortedMeta = useMemo(() => {
  46. if (!metricsMeta.data) {
  47. return [];
  48. }
  49. if (!metricsCardinality.data) {
  50. return metricsMeta.data.map(meta => ({...meta, cardinality: 0}));
  51. }
  52. return metricsMeta.data
  53. .map(({mri, ...rest}) => {
  54. return {
  55. mri,
  56. cardinality: metricsCardinality.data[mri] ?? 0,
  57. ...rest,
  58. };
  59. })
  60. .sort((a, b) => {
  61. return b.cardinality - a.cardinality;
  62. }) as MetricWithCardinality[];
  63. }, [metricsCardinality.data, metricsMeta.data]);
  64. const metrics = sortedMeta.filter(
  65. ({mri, type, unit}) =>
  66. mri.includes(query) ||
  67. getReadableMetricType(type).includes(query) ||
  68. unit.includes(query)
  69. );
  70. return (
  71. <Fragment>
  72. <SearchWrapper>
  73. <h6>{t('Emitted Metrics')}</h6>
  74. <SearchBar
  75. placeholder={t('Search Metrics')}
  76. onChange={setQuery}
  77. query={query}
  78. size="sm"
  79. />
  80. </SearchWrapper>
  81. <Tabs value={selectedTab} onChange={setSelectedTab}>
  82. <TabList>
  83. <TabList.Item key={BlockingStatusTab.ACTIVE}>{t('Active')}</TabList.Item>
  84. <TabList.Item key={BlockingStatusTab.DISABLED}>{t('Disabled')}</TabList.Item>
  85. </TabList>
  86. <TabPanels>
  87. <TabPanels.Item key={BlockingStatusTab.ACTIVE}>
  88. <MetricsTable
  89. metrics={metrics.filter(
  90. ({blockingStatus}) => !blockingStatus[0]?.isBlocked
  91. )}
  92. isLoading={isLoading}
  93. query={query}
  94. project={project}
  95. />
  96. </TabPanels.Item>
  97. <TabPanels.Item key={BlockingStatusTab.DISABLED}>
  98. <MetricsTable
  99. metrics={metrics.filter(({blockingStatus}) => blockingStatus[0]?.isBlocked)}
  100. isLoading={isLoading}
  101. query={query}
  102. project={project}
  103. />
  104. </TabPanels.Item>
  105. </TabPanels>
  106. </Tabs>
  107. </Fragment>
  108. );
  109. }
  110. interface MetricsTableProps {
  111. isLoading: boolean;
  112. metrics: MetricWithCardinality[];
  113. project: Project;
  114. query: string;
  115. }
  116. function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
  117. const blockMetricMutation = useBlockMetric(project);
  118. const {hasAccess} = useAccess({access: ['project:write'], project});
  119. const cardinalityLimit =
  120. project.relayCustomMetricCardinalityLimit ?? DEFAULT_METRICS_CARDINALITY_LIMIT;
  121. return (
  122. <MetricsPanelTable
  123. headers={[
  124. t('Metric'),
  125. <Cell right key="cardinality">
  126. <IconArrow size="xs" direction="down" />
  127. {t('Cardinality')}
  128. </Cell>,
  129. <Cell right key="type">
  130. {t('Type')}
  131. </Cell>,
  132. <Cell right key="unit">
  133. {t('Unit')}
  134. </Cell>,
  135. <Cell right key="actions">
  136. {t('Actions')}
  137. </Cell>,
  138. ]}
  139. emptyMessage={
  140. query
  141. ? t('No metrics match the query.')
  142. : t('There are no custom metrics to display.')
  143. }
  144. isEmpty={metrics.length === 0}
  145. isLoading={isLoading}
  146. >
  147. {metrics.map(({mri, type, unit, cardinality, blockingStatus}) => {
  148. const isBlocked = blockingStatus[0]?.isBlocked;
  149. const isCardinalityLimited = cardinality >= cardinalityLimit;
  150. return (
  151. <Fragment key={mri}>
  152. <Cell>
  153. <Link
  154. to={`/settings/projects/${project.slug}/metrics/${encodeURIComponent(
  155. mri
  156. )}`}
  157. >
  158. {middleEllipsis(formatMRI(mri), 65, /\.|-|_/)}
  159. </Link>
  160. </Cell>
  161. <Cell right>
  162. {isCardinalityLimited && (
  163. <Tooltip
  164. title={tct(
  165. 'The tag cardinality of this metric exceeded our limit of [cardinalityLimit], which led to the data being dropped',
  166. {cardinalityLimit}
  167. )}
  168. >
  169. <StyledIconWarning size="sm" color="red300" />
  170. </Tooltip>
  171. )}
  172. {cardinality}
  173. </Cell>
  174. <Cell right>
  175. <Tag>{getReadableMetricType(type)}</Tag>
  176. </Cell>
  177. <Cell right>
  178. <Tag>{unit}</Tag>
  179. </Cell>
  180. <Cell right>
  181. <BlockButton
  182. size="xs"
  183. hasAccess={hasAccess}
  184. disabled={blockMetricMutation.isLoading}
  185. isBlocked={isBlocked}
  186. blockTarget="metric"
  187. onConfirm={() => {
  188. blockMetricMutation.mutate({
  189. mri,
  190. operationType: isBlocked ? 'unblockMetric' : 'blockMetric',
  191. });
  192. }}
  193. />
  194. </Cell>
  195. </Fragment>
  196. );
  197. })}
  198. </MetricsPanelTable>
  199. );
  200. }
  201. const SearchWrapper = styled('div')`
  202. display: flex;
  203. justify-content: space-between;
  204. align-items: flex-start;
  205. margin-top: ${space(4)};
  206. margin-bottom: ${space(0)};
  207. & > h6 {
  208. margin: 0;
  209. }
  210. `;
  211. const MetricsPanelTable = styled(PanelTable)`
  212. margin-top: ${space(2)};
  213. grid-template-columns: 1fr repeat(4, min-content);
  214. `;
  215. const Cell = styled('div')<{right?: boolean}>`
  216. display: flex;
  217. align-items: center;
  218. align-self: stretch;
  219. gap: ${space(0.5)};
  220. justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')};
  221. `;
  222. const StyledIconWarning = styled(IconWarning)`
  223. margin-top: ${space(0.5)};
  224. &:hover {
  225. cursor: pointer;
  226. }
  227. `;