diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx index ca21492a1a957d..e028933cc9ddef 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx @@ -181,7 +181,7 @@ function getSuggestionDescription(group: SearchGroup | SearchItem) { return undefined; } -function getPredefinedValues({ +export function getPredefinedValues({ fieldDefinition, key, filterValue, diff --git a/static/app/views/dashboards/globalFilter/filterSelector.tsx b/static/app/views/dashboards/globalFilter/filterSelector.tsx index bf0b30df3f2392..c3ff2fd2f4aa90 100644 --- a/static/app/views/dashboards/globalFilter/filterSelector.tsx +++ b/static/app/views/dashboards/globalFilter/filterSelector.tsx @@ -2,10 +2,12 @@ import {useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import isEqual from 'lodash/isEqual'; +import type {SelectOption} from '@sentry/scraps/compactSelect'; import {Flex} from '@sentry/scraps/layout'; import {Button} from 'sentry/components/core/button'; import {HybridFilter} from 'sentry/components/organizations/hybridFilter'; +import {getPredefinedValues} from 'sentry/components/searchQueryBuilder/tokens/filter/valueCombobox'; import {MutableSearch} from 'sentry/components/searchSyntax/mutableSearch'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -15,6 +17,10 @@ import usePageFilters from 'sentry/utils/usePageFilters'; import {type SearchBarData} from 'sentry/views/dashboards/datasetConfig/base'; import {getDatasetLabel} from 'sentry/views/dashboards/globalFilter/addFilter'; import FilterSelectorTrigger from 'sentry/views/dashboards/globalFilter/filterSelectorTrigger'; +import { + getFieldDefinitionForDataset, + getFilterToken, +} from 'sentry/views/dashboards/globalFilter/utils'; import type {GlobalFilter} from 'sentry/views/dashboards/types'; type FilterSelectorProps = { @@ -48,6 +54,35 @@ function FilterSelector({ const {dataset, tag} = globalFilter; const {selection} = usePageFilters(); + // Retrieve full tag definition to check if it has predefined values + const datasetFilterKeys = searchBarData.getFilterKeys(); + const fullTag = datasetFilterKeys[tag.key]; + const fieldDefinition = getFieldDefinitionForDataset(tag, dataset); + + const filterToken = useMemo( + () => getFilterToken(globalFilter, fieldDefinition), + [globalFilter, fieldDefinition] + ); + + // Retrieve predefined values if the tag has any + const predefinedValues = useMemo(() => { + if (!filterToken) { + return null; + } + const filterValue = filterToken.value.text; + return getPredefinedValues({ + key: fullTag, + filterValue, + token: filterToken, + fieldDefinition, + }); + }, [fullTag, filterToken, fieldDefinition]); + + // Only fetch values if the tag has no predefined values + const shouldFetchValues = fullTag + ? !fullTag.predefined && predefinedValues === null + : true; + const baseQueryKey = useMemo( () => ['global-dashboard-filters-tag-values', tag, selection, searchQuery], [tag, selection, searchQuery] @@ -63,33 +98,48 @@ function FilterSelector({ return result ?? []; }, placeholderData: keepPreviousData, - enabled: true, + enabled: shouldFetchValues, staleTime: 5 * 60 * 1000, }); const {data: fetchedFilterValues, isFetching} = queryResult; const options = useMemo(() => { - const optionMap = new Map(); - const addOption = (value: string) => optionMap.set(value, {label: value, value}); + const optionMap = new Map>(); + const fixedOptionMap = new Map>(); + const addOption = (value: string, map: Map>) => + map.set(value, {label: value, value}); - // Filter values fetched using getTagValues - fetchedFilterValues?.forEach(addOption); // Filter values in the global filter - activeFilterValues.forEach(addOption); - // Staged filter values inside the filter selector - stagedFilterValues.forEach(addOption); + activeFilterValues.forEach(value => addOption(value, optionMap)); + + // Predefined values + predefinedValues?.forEach(suggestionSection => { + suggestionSection.suggestions.forEach(suggestion => + addOption(suggestion.value, optionMap) + ); + }); + // Filter values fetched using getTagValues + fetchedFilterValues?.forEach(value => addOption(value, optionMap)); // Allow setting a custom filter value based on search input - if (searchQuery) { - addOption(searchQuery); + if (searchQuery && !optionMap.has(searchQuery)) { + addOption(searchQuery, fixedOptionMap); } - - // Reversing the order allows effectively deduplicating the values - // and avoid losing their original order from the fetched results - // (e.g. without this, all staged values would be grouped at the top of the list) - return Array.from(optionMap.values()).reverse(); - }, [fetchedFilterValues, activeFilterValues, stagedFilterValues, searchQuery]); + // Staged filter values inside the filter selector + stagedFilterValues.forEach(value => { + if (!optionMap.has(value)) { + addOption(value, fixedOptionMap); + } + }); + return [...Array.from(fixedOptionMap.values()), ...Array.from(optionMap.values())]; + }, [ + fetchedFilterValues, + predefinedValues, + activeFilterValues, + stagedFilterValues, + searchQuery, + ]); const handleChange = (opts: string[]) => { if (isEqual(opts, activeFilterValues)) { @@ -126,7 +176,8 @@ function FilterSelector({ onStagedValueChange={value => { setStagedFilterValues(value); }} - sizeLimit={10} + sizeLimit={30} + menuWidth={400} onClose={() => { setSearchQuery(''); setStagedFilterValues([]); diff --git a/static/app/views/dashboards/globalFilter/utils.tsx b/static/app/views/dashboards/globalFilter/utils.tsx index 7d80c2e0213428..6510ddc6035028 100644 --- a/static/app/views/dashboards/globalFilter/utils.tsx +++ b/static/app/views/dashboards/globalFilter/utils.tsx @@ -4,6 +4,7 @@ import { } from 'sentry/components/searchQueryBuilder/hooks/useQueryBuilderState'; import {getFilterValueType} from 'sentry/components/searchQueryBuilder/tokens/filter/utils'; import {cleanFilterValue} from 'sentry/components/searchQueryBuilder/tokens/filter/valueSuggestions/utils'; +import {getInitialFilterText} from 'sentry/components/searchQueryBuilder/tokens/utils'; import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils'; import { TermOperator, @@ -54,6 +55,19 @@ export function parseFilterValue( return parsedResult.filter(token => token.type === Token.FILTER); } +export function getFilterToken( + globalFilter: GlobalFilter, + fieldDefinition: FieldDefinition | null +) { + const {tag, value} = globalFilter; + let filterValue = value; + if (value === '') { + filterValue = getInitialFilterText(tag.key, fieldDefinition, false); + } + const filterTokens = parseFilterValue(filterValue, globalFilter); + return filterTokens[0] ?? null; +} + export function isValidNumericFilterValue( value: string, filterToken: TokenResult,