import {memo, useCallback, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {ComboBox} from 'sentry/components/comboBox'; import type {ComboBoxOption} from 'sentry/components/comboBox/types'; import {IconWarning} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {MetricMeta, MRI} from 'sentry/types/metrics'; import {type Fuse, useFuzzySearch} from 'sentry/utils/fuzzySearch'; import { isCustomMetric, isSpanDuration, isSpanMeasurement, isTransactionDuration, isTransactionMeasurement, } from 'sentry/utils/metrics'; import {getReadableMetricType} from 'sentry/utils/metrics/formatters'; import {formatMRI, parseMRI} from 'sentry/utils/metrics/mri'; import {middleEllipsis} from 'sentry/utils/string/middleEllipsis'; import useKeyPress from 'sentry/utils/useKeyPress'; import useProjects from 'sentry/utils/useProjects'; import {MetricListItemDetails} from 'sentry/views/metrics/metricListItemDetails'; type MRISelectProps = { isLoading: boolean; metricsMeta: MetricMeta[]; onChange: (mri: MRI) => void; onOpenMenu: (isOpen: boolean) => void; onTagClick: (mri: MRI, tag: string) => void; projects: number[]; value: MRI; }; const isVisibleTransactionMetric = (metric: MetricMeta) => isTransactionDuration(metric) || isTransactionMeasurement(metric); const isVisibleSpanMetric = (metric: MetricMeta) => isSpanDuration(metric) || isSpanMeasurement(metric); const isShownByDefault = (metric: MetricMeta) => isCustomMetric(metric) || isVisibleTransactionMetric(metric) || isVisibleSpanMetric(metric); function useMriMode() { const [mriMode, setMriMode] = useState(false); const mriModeKeyPressed = useKeyPress('`', undefined, true); useEffect(() => { if (mriModeKeyPressed) { setMriMode(value => !value); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mriModeKeyPressed]); return mriMode; } /** * Returns a set of MRIs that have duplicate names but different units */ export function getMetricsWithDuplicateNames(metrics: MetricMeta[]): Set { const metricNameMap = new Map(); const duplicateNames: string[] = []; for (const metric of metrics) { const metricName = parseMRI(metric.mri)?.name; if (!metricName) { continue; } if (metricNameMap.has(metricName)) { const mapEntry = metricNameMap.get(metricName); mapEntry?.push(metric.mri); duplicateNames.push(metricName); } else { metricNameMap.set(metricName, [metric.mri]); } } const duplicateMetrics = new Set(); for (const name of duplicateNames) { const duplicates = metricNameMap.get(name); if (!duplicates) { continue; } duplicates.forEach(duplicate => duplicateMetrics.add(duplicate)); } return duplicateMetrics; } /** * Returns a set of MRIs that have duplicate names but different units */ function useMetricsWithDuplicateNames(metrics: MetricMeta[]): Set { return useMemo(() => { return getMetricsWithDuplicateNames(metrics); }, [metrics]); } const SEARCH_OPTIONS: Fuse.IFuseOptions = { keys: ['searchText'], threshold: 0.2, ignoreLocation: true, includeScore: false, includeMatches: false, }; function useFilteredMRIs( metricsMeta: MetricMeta[], inputValue: string, mriMode: boolean ) { const searchEntries = useMemo(() => { return metricsMeta.map(metric => { return { value: metric.mri, searchText: mriMode ? // enable search by mri, name, unit (millisecond), type (c:), and readable type (counter) `${getReadableMetricType(metric.type)}${metric.mri}` : // enable search in the full formatted string formatMRI(metric.mri), }; }); }, [metricsMeta, mriMode]); const search = useFuzzySearch(searchEntries, SEARCH_OPTIONS); return useMemo(() => { if (!search || !inputValue) { return new Set(metricsMeta.map(metric => metric.mri)); } const results = search.search(inputValue); return new Set(results.map(result => result.item.value)); }, [inputValue, metricsMeta, search]); } export const MRISelect = memo(function MRISelect({ projects: projectIds, onChange, onTagClick, onOpenMenu, metricsMeta, isLoading, value, }: MRISelectProps) { const {projects} = useProjects(); const mriMode = useMriMode(); const [inputValue, setInputValue] = useState(''); const metricsWithDuplicateNames = useMetricsWithDuplicateNames(metricsMeta); const filteredMRIs = useFilteredMRIs(metricsMeta, inputValue, mriMode); const handleFilterOption = useCallback( (option: ComboBoxOption) => { return filteredMRIs.has(option.value); }, [filteredMRIs] ); const selectedProjects = useMemo( () => projects.filter(project => projectIds[0] === -1 ? true : projectIds.length === 0 ? project.isMember : projectIds.includes(parseInt(project.id, 10)) ), [projectIds, projects] ); const displayedMetrics = useMemo(() => { const isSelected = (metric: MetricMeta) => metric.mri === value; const result = metricsMeta .filter(metric => isShownByDefault(metric) || isSelected(metric)) .sort(metric => (isSelected(metric) ? -1 : 1)); // Add the selected metric to the top of the list if it's not already there if (result[0]?.mri !== value) { const parsedMri = parseMRI(value)!; return [ { mri: value, type: parsedMri.type, unit: parsedMri.unit, operations: [], projectIds: [], blockingStatus: [], } satisfies MetricMeta, ...result, ]; } return result; }, [metricsMeta, value]); const handleMRIChange = useCallback( option => { onChange(option.value); }, [onChange] ); const mriOptions = useMemo( () => displayedMetrics.map>(metric => { const isDuplicateWithDifferentUnit = metricsWithDuplicateNames.has(metric.mri); const trailingItems: React.ReactNode[] = []; if (isDuplicateWithDifferentUnit) { trailingItems.push(); } if (parseMRI(metric.mri)?.useCase === 'custom' && !mriMode) { trailingItems.push( {t('Custom')} ); } return { label: mriMode ? metric.mri : middleEllipsis(formatMRI(metric.mri) ?? '', 46, /\.|-|_/), value: metric.mri, details: metric.projectIds.length > 0 ? ( ) : null, showDetailsInOverlay: true, trailingItems, }; }), [displayedMetrics, metricsWithDuplicateNames, mriMode, onTagClick, selectedProjects] ); return ( ); }); const CustomMetricInfoText = styled('span')` color: ${p => p.theme.subText}; `; const MetricComboBox = styled(ComboBox)` min-width: 200px; max-width: min(500px, 100%); `;