import {Fragment, memo, useCallback, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import type {SelectOption} from 'sentry/components/compactSelect'; import {CompactSelect} from 'sentry/components/compactSelect'; import Tag from 'sentry/components/tag'; import {IconLightning, IconReleases} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {MetricMeta, MetricsOperation, MRI} from 'sentry/types'; import {trackAnalytics} from 'sentry/utils/analytics'; import { getDefaultMetricDisplayType, isAllowedOp, isCustomMetric, isMeasurement, isSpanMetric, isTransactionDuration, } from 'sentry/utils/metrics'; import {getReadableMetricType} from 'sentry/utils/metrics/formatters'; import {formatMRI} from 'sentry/utils/metrics/mri'; import type { MetricDisplayType, MetricsQuerySubject, MetricWidgetQueryParams, } from 'sentry/utils/metrics/types'; import {useBreakpoints} from 'sentry/utils/metrics/useBreakpoints'; import {useIncrementQueryMetric} from 'sentry/utils/metrics/useIncrementQueryMetric'; import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta'; import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags'; import {middleEllipsis} from 'sentry/utils/middleEllipsis'; import useKeyPress from 'sentry/utils/useKeyPress'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {MetricSearchBar} from 'sentry/views/ddm/metricSearchBar'; type QueryBuilderProps = { displayType: MetricDisplayType; isEdit: boolean; metricsQuery: MetricsQuerySubject; onChange: (data: Partial) => void; projects: number[]; fixedWidth?: boolean; powerUserMode?: boolean; }; const isShownByDefault = (metric: MetricMeta) => isCustomMetric(metric) || isTransactionDuration(metric) || isMeasurement(metric) || isSpanMetric(metric); function getOpsForMRI(mri: MRI, meta: MetricMeta[]) { return meta.find(metric => metric.mri === mri)?.operations.filter(isAllowedOp) ?? []; } export const QueryBuilder = memo(function QueryBuilder({ metricsQuery, projects, displayType, powerUserMode, onChange, }: QueryBuilderProps) { const organization = useOrganization(); const pageFilters = usePageFilters(); const {data: meta} = useMetricsMeta(pageFilters.selection); const mriModeKeyPressed = useKeyPress('`', undefined, true); const [mriMode, setMriMode] = useState(powerUserMode); // power user mode that shows raw MRI instead of metrics names const breakpoints = useBreakpoints(); useEffect(() => { if (mriModeKeyPressed && !powerUserMode) { setMriMode(!mriMode); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mriModeKeyPressed, powerUserMode]); const {data: tags = [], isLoading: tagsIsLoading} = useMetricsTags(metricsQuery.mri, { projects, }); const displayedMetrics = useMemo(() => { if (mriMode) { return meta; } const isSelected = (metric: MetricMeta) => metric.mri === metricsQuery.mri; return meta .filter(metric => isShownByDefault(metric) || isSelected(metric)) .sort(metric => (isSelected(metric) ? -1 : 1)); }, [meta, metricsQuery.mri, mriMode]); const selectedMeta = useMemo(() => { return meta.find(metric => metric.mri === metricsQuery.mri); }, [meta, metricsQuery.mri]); const incrementQueryMetric = useIncrementQueryMetric({ displayType, op: metricsQuery.op, groupBy: metricsQuery.groupBy, query: metricsQuery.query, mri: metricsQuery.mri, }); const handleMRIChange = useCallback( ({value}) => { const availableOps = getOpsForMRI(value, meta); const selectedOp = availableOps.includes( (metricsQuery.op ?? '') as MetricsOperation ) ? metricsQuery.op : availableOps?.[0]; const queryChanges = { mri: value, op: selectedOp, groupBy: undefined, displayType: getDefaultMetricDisplayType(value, selectedOp), }; trackAnalytics('ddm.widget.metric', {organization}); incrementQueryMetric('ddm.widget.metric', queryChanges); onChange({ ...queryChanges, focusedSeries: undefined, }); }, [incrementQueryMetric, meta, metricsQuery.op, onChange, organization] ); const handleOpChange = useCallback( ({value}) => { trackAnalytics('ddm.widget.operation', {organization}); incrementQueryMetric('ddm.widget.operation', {op: value}); onChange({ op: value, }); }, [incrementQueryMetric, onChange, organization] ); const handleGroupByChange = useCallback( (options: SelectOption[]) => { trackAnalytics('ddm.widget.group', {organization}); incrementQueryMetric('ddm.widget.group', { groupBy: options.map(o => o.value), }); onChange({ groupBy: options.map(o => o.value), focusedSeries: undefined, }); }, [incrementQueryMetric, onChange, organization] ); const handleQueryChange = useCallback( (query: string) => { trackAnalytics('ddm.widget.filter', {organization}); incrementQueryMetric('ddm.widget.filter', {query}); onChange({query}); }, [incrementQueryMetric, onChange, organization] ); const mriOptions = useMemo( () => displayedMetrics.map>(metric => ({ label: mriMode ? metric.mri : formatMRI(metric.mri), // enable search by mri, name, unit (millisecond), type (c:), and readable type (counter) textValue: `${metric.mri}${getReadableMetricType(metric.type)}`, value: metric.mri, trailingItems: mriMode ? undefined : ( {getReadableMetricType(metric.type)} {metric.unit} ), })), [displayedMetrics, mriMode] ); return ( ({ label: op, value: op, })) ?? [] } disabled={!metricsQuery.mri} value={metricsQuery.op} onChange={handleOpChange} /> ({ label: tag.key, value: tag.key, trailingItems: ( {tag.key === 'release' && } {tag.key === 'transaction' && } ), }))} disabled={!metricsQuery.mri || tagsIsLoading} value={metricsQuery.groupBy} onChange={handleGroupByChange} /> ); }); const QueryBuilderWrapper = styled('div')` display: flex; flex-grow: 1; gap: ${space(1)}; flex-wrap: wrap; `; const FlexBlock = styled('div')` display: flex; gap: ${space(1)}; flex-wrap: wrap; `; const MetricSelect = styled(CompactSelect)` min-width: 200px; & > button { width: 100%; } `; const OpSelect = styled(CompactSelect)` width: 120px; & > button { width: 100%; } `; const SearchBarWrapper = styled('div')` flex: 1; min-width: 200px; `;