123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- 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<MRI> {
- const metricNameMap = new Map<string, MRI[]>();
- 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<MRI>();
- 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<MRI> {
- return useMemo(() => {
- return getMetricsWithDuplicateNames(metrics);
- }, [metrics]);
- }
- const SEARCH_OPTIONS: Fuse.IFuseOptions<any> = {
- 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<MRI>) => {
- 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<ComboBoxOption<MRI>>(metric => {
- const isDuplicateWithDifferentUnit = metricsWithDuplicateNames.has(metric.mri);
- const trailingItems: React.ReactNode[] = [];
- if (isDuplicateWithDifferentUnit) {
- trailingItems.push(<IconWarning key="warning" size="xs" color="yellow400" />);
- }
- if (parseMRI(metric.mri)?.useCase === 'custom' && !mriMode) {
- trailingItems.push(
- <CustomMetricInfoText key="text">{t('Custom')}</CustomMetricInfoText>
- );
- }
- return {
- label: mriMode
- ? metric.mri
- : middleEllipsis(formatMRI(metric.mri) ?? '', 46, /\.|-|_/),
- value: metric.mri,
- details:
- metric.projectIds.length > 0 ? (
- <MetricListItemDetails
- metric={metric}
- selectedProjects={selectedProjects}
- onTagClick={onTagClick}
- isDuplicateWithDifferentUnit={isDuplicateWithDifferentUnit}
- />
- ) : null,
- showDetailsInOverlay: true,
- trailingItems,
- };
- }),
- [displayedMetrics, metricsWithDuplicateNames, mriMode, onTagClick, selectedProjects]
- );
- return (
- <MetricComboBox
- aria-label={t('Metric')}
- filterOption={handleFilterOption}
- growingInput
- isLoading={isLoading}
- loadingMessage={t('Loading metrics...')}
- menuSize="sm"
- menuWidth="450px"
- onChange={handleMRIChange}
- onInputChange={setInputValue}
- onOpenChange={onOpenMenu}
- options={mriOptions}
- placeholder={t('Select a metric')}
- size="md"
- sizeLimit={100}
- value={value}
- />
- );
- });
- const CustomMetricInfoText = styled('span')`
- color: ${p => p.theme.subText};
- `;
- const MetricComboBox = styled(ComboBox<MRI>)`
- min-width: 200px;
- max-width: min(500px, 100%);
- `;
|