import {Fragment, useCallback, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {CompactSelect} from 'sentry/components/compactSelect'; import SearchBar, {SearchBarProps} from 'sentry/components/events/searchBar'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import Tag from 'sentry/components/tag'; import {IconLightning, IconReleases} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {MRI, SavedSearchType, TagCollection} from 'sentry/types'; import { defaultMetricDisplayType, getReadableMetricType, isAllowedOp, MetricDisplayType, MetricsQuery, MetricWidgetQueryParams, } from 'sentry/utils/metrics'; import {formatMRI, getUseCaseFromMRI} from 'sentry/utils/metrics/mri'; import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta'; import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags'; import useApi from 'sentry/utils/useApi'; import useKeyPress from 'sentry/utils/useKeyPress'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; type QueryBuilderProps = { displayType: MetricDisplayType; // TODO(ddm): move display type out of the query builder metricsQuery: Pick; onChange: (data: Partial) => void; projects: number[]; powerUserMode?: boolean; }; export function QueryBuilder({ metricsQuery, projects, displayType, powerUserMode, onChange, }: QueryBuilderProps) { const {data: meta, isLoading: isMetaLoading} = useMetricsMeta(projects); const mriModeKeyPressed = useKeyPress('`', undefined, true); const [mriMode, setMriMode] = useState(powerUserMode); // power user mode that shows raw MRI instead of metrics names useEffect(() => { if (mriModeKeyPressed && !powerUserMode) { setMriMode(!mriMode); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mriModeKeyPressed, powerUserMode]); const {data: tags = []} = useMetricsTags(metricsQuery.mri, projects); const metaArr = useMemo(() => { if (mriMode) { return Object.values(meta); } return Object.values(meta).filter( metric => metric.mri.includes(':custom/') || metric.mri === metricsQuery.mri ); }, [meta, metricsQuery.mri, mriMode]); // Reset the query data if the selected metric is no longer available useEffect(() => { if ( metricsQuery.mri && !isMetaLoading && !metaArr.find(metric => metric.mri === metricsQuery.mri) ) { onChange({mri: '' as MRI, op: '', groupBy: []}); } }, [isMetaLoading, metaArr, metricsQuery.mri, onChange]); if (!meta) { return null; } return ( ({ label: mriMode ? metric.mri : formatMRI(metric.mri), value: metric.mri, trailingItems: mriMode ? undefined : ( {getReadableMetricType(metric.type)} {metric.unit} ), }))} value={metricsQuery.mri} onChange={option => { const availableOps = meta[option.value]?.operations.filter(isAllowedOp); // @ts-expect-error .op is an operation const selectedOp = availableOps.includes(metricsQuery.op ?? '') ? metricsQuery.op : availableOps[0]; onChange({ mri: option.value, op: selectedOp, groupBy: undefined, focusedSeries: undefined, displayType: getWidgetDisplayType(option.value, selectedOp), }); }} /> ({ label: op, value: op, })) ?? [] } disabled={!metricsQuery.mri} value={metricsQuery.op} onChange={option => onChange({ op: option.value, }) } /> ({ label: tag.key, value: tag.key, trailingItems: ( {tag.key === 'release' && } {tag.key === 'transaction' && } ), }))} disabled={!metricsQuery.mri} value={metricsQuery.groupBy} onChange={options => onChange({ groupBy: options.map(o => o.value), focusedSeries: undefined, }) } /> { onChange({displayType: value}); }} /> id.toString())} mri={metricsQuery.mri} disabled={!metricsQuery.mri} onChange={query => onChange({query})} query={metricsQuery.query} /> ); } interface MetricSearchBarProps extends Omit, 'tags' | 'projectIds'> { onChange: (value: string) => void; projectIds: string[]; disabled?: boolean; mri?: MRI; query?: string; } const EMPTY_ARRAY = []; export function MetricSearchBar({ mri, disabled, onChange, query, projectIds, ...props }: MetricSearchBarProps) { const org = useOrganization(); const api = useApi(); const {selection} = usePageFilters(); const projectIdNumbers = useMemo( () => projectIds.map(id => parseInt(id, 10)), [projectIds] ); const {data: tags = EMPTY_ARRAY} = useMetricsTags(mri, projectIdNumbers); const supportedTags: TagCollection = useMemo( () => tags.reduce((acc, tag) => ({...acc, [tag.key]: tag}), {}), [tags] ); // TODO(ddm): try to use useApiQuery here const getTagValues = useCallback( async tag => { const useCase = getUseCaseFromMRI(mri); const tagsValues = await api.requestPromise( `/organizations/${org.slug}/metrics/tags/${tag.key}/`, { query: { metric: mri, useCase, project: selection.projects, }, } ); return tagsValues.filter(tv => tv.value !== '').map(tv => tv.value); }, [api, mri, org.slug, selection.projects] ); const handleChange = useCallback( (value: string, {validSearch} = {validSearch: true}) => { if (validSearch) { onChange(value); } }, [onChange] ); return ( ); } function getWidgetDisplayType( mri: MetricsQuery['mri'], op: MetricsQuery['op'] ): MetricDisplayType { if (mri?.startsWith('c') || op === 'count') { return MetricDisplayType.BAR; } return MetricDisplayType.LINE; } const QueryBuilderWrapper = styled('div')` display: flex; flex-grow: 1; flex-direction: column; `; const QueryBuilderRow = styled('div')` padding: ${space(1)}; padding-bottom: 0; `; const WideSearchBar = styled(SearchBar)` width: 100%; opacity: ${p => (p.disabled ? '0.6' : '1')}; `; const WrapPageFilterBar = styled(PageFilterBar)` max-width: max-content; height: auto; flex-wrap: wrap; `;