import {Fragment, useMemo, useState} from 'react'; import {Link} from 'react-router'; import styled from '@emotion/styled'; import Tag from 'sentry/components/badge/tag'; import {PanelTable} from 'sentry/components/panels/panelTable'; import SearchBar from 'sentry/components/searchBar'; import {TabList, TabPanels, Tabs} from 'sentry/components/tabs'; import {Tooltip} from 'sentry/components/tooltip'; import {IconArrow} from 'sentry/icons'; import {IconWarning} from 'sentry/icons/iconWarning'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {MetricMeta} from 'sentry/types/metrics'; import type {Project} from 'sentry/types/project'; import {hasCustomMetricsExtractionRules} from 'sentry/utils/metrics/features'; import {getReadableMetricType} from 'sentry/utils/metrics/formatters'; import {formatMRI, isExtractedCustomMetric} from 'sentry/utils/metrics/mri'; import {useBlockMetric} from 'sentry/utils/metrics/useBlockMetric'; import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume'; import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta'; import {middleEllipsis} from 'sentry/utils/string/middleEllipsis'; import useOrganization from 'sentry/utils/useOrganization'; import {useAccess} from 'sentry/views/settings/projectMetrics/access'; import {BlockButton} from 'sentry/views/settings/projectMetrics/blockButton'; import {useSearchQueryParam} from 'sentry/views/settings/projectMetrics/utils/useSearchQueryParam'; type Props = { project: Project; }; enum BlockingStatusTab { ACTIVE = 'active', DISABLED = 'disabled', } type MetricWithCardinality = MetricMeta & {cardinality: number}; export function CustomMetricsTable({project}: Props) { const organization = useOrganization(); const [selectedTab, setSelectedTab] = useState(BlockingStatusTab.ACTIVE); const [query, setQuery] = useSearchQueryParam('metricsQuery'); const metricsMeta = useMetricsMeta( {projects: [parseInt(project.id, 10)]}, ['custom'], false ); const metricsCardinality = useCardinalityLimitedMetricVolume({ projects: [parseInt(project.id, 10)], }); const isLoading = metricsMeta.isLoading || metricsCardinality.isLoading; const sortedMeta = useMemo(() => { if (!metricsMeta.data) { return []; } // Do not show internal extracted metrics in this table const filteredMeta = metricsMeta.data.filter(meta => !isExtractedCustomMetric(meta)); if (!metricsCardinality.data) { return filteredMeta.map(meta => ({...meta, cardinality: 0})); } return filteredMeta .map(({mri, ...rest}) => { return { mri, cardinality: metricsCardinality.data[mri] ?? 0, ...rest, }; }) .sort((a, b) => { // First sort by cardinality (descending) if (b.cardinality !== a.cardinality) { return b.cardinality - a.cardinality; } // If cardinality is the same, sort by name (ascending) return a.mri.localeCompare(b.mri); }) as MetricWithCardinality[]; }, [metricsCardinality.data, metricsMeta.data]); const metrics = sortedMeta.filter( ({mri, type, unit}) => mri.includes(query) || getReadableMetricType(type).includes(query) || unit.includes(query) ); // If we have custom metrics extraction rules, // we only show the custom metrics table if the project has custom metrics if (hasCustomMetricsExtractionRules(organization) && metricsMeta.data.length === 0) { return null; } return ( <h6>{t('Custom Metrics')}</h6> {hasCustomMetricsExtractionRules(organization) && ( <Tag type="warning">{t('deprecated')}</Tag> )} {t('Active')} {t('Disabled')} !blockingStatus[0]?.isBlocked )} isLoading={isLoading} query={query} project={project} /> blockingStatus[0]?.isBlocked)} isLoading={isLoading} query={query} project={project} /> ); } interface MetricsTableProps { isLoading: boolean; metrics: MetricWithCardinality[]; project: Project; query: string; } function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) { const blockMetricMutation = useBlockMetric(project); const {hasAccess} = useAccess({access: ['project:write'], project}); return ( {t('Metric')} , {t('Type')} , {t('Unit')} , {t('Actions')} , ]} emptyMessage={ query ? t('No metrics match the query.') : t('There are no custom metrics to display.') } isEmpty={metrics.length === 0} isLoading={isLoading} > {metrics.map(({mri, type, unit, cardinality, blockingStatus}) => { const isBlocked = blockingStatus[0]?.isBlocked; const isCardinalityLimited = cardinality > 0; return ( {isCardinalityLimited && ( )} {middleEllipsis(formatMRI(mri), 65, /\.|-|_/)} {getReadableMetricType(type)} {unit} { blockMetricMutation.mutate({ mri, operationType: isBlocked ? 'unblockMetric' : 'blockMetric', }); }} /> ); })} ); } const SearchWrapper = styled('div')` display: flex; justify-content: space-between; align-items: flex-start; gap: ${space(1)}; margin-top: ${space(4)}; margin-bottom: ${space(0)}; & > h6 { margin: 0; } `; const MetricsPanelTable = styled(PanelTable)` margin-top: ${space(2)}; grid-template-columns: 1fr repeat(3, min-content); `; const Cell = styled('div')<{right?: boolean}>` display: flex; align-items: center; align-self: stretch; gap: ${space(0.5)}; justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')}; `; const StyledIconWarning = styled(IconWarning)` margin-top: ${space(0.5)}; &:hover { cursor: pointer; } `; const Title = styled('div')` display: flex; align-items: center; flex-wrap: wrap; gap: ${space(0.5)}; margin-bottom: ${space(3)}; & > h6 { margin: 0; } `;