diff --git a/static/app/views/dashboards/globalFilter/addFilter.spec.tsx b/static/app/views/dashboards/globalFilter/addFilter.spec.tsx index f76ed337b2b45c..fd9fe6d70dd698 100644 --- a/static/app/views/dashboards/globalFilter/addFilter.spec.tsx +++ b/static/app/views/dashboards/globalFilter/addFilter.spec.tsx @@ -9,9 +9,9 @@ import {WidgetType} from 'sentry/views/dashboards/types'; describe('AddFilter', () => { // Mock filter keys returned by the search bar data provider const mockFilterKeys: TagCollection = { - browser: { - key: 'browser', - name: 'Browser', + 'browser.name': { + key: 'browser.name', + name: 'Browser Name', kind: FieldKind.FIELD, }, environment: { @@ -19,12 +19,12 @@ describe('AddFilter', () => { name: 'Environment', kind: FieldKind.FIELD, }, - unsupportedFunction: { + 'unsupported.function': { key: 'unsupported.function', name: 'Unsupported Function', kind: FieldKind.FUNCTION, }, - unsupportedMeasurement: { + 'unsupported.measurement': { key: 'unsupported.measurement', name: 'Unsupported Measurement', kind: FieldKind.MEASUREMENT, @@ -64,17 +64,15 @@ describe('AddFilter', () => { await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'})); // Verify filter keys are shown for each dataset - for (const datasetLabel of DATASET_CHOICES.values()) { - await userEvent.click(screen.getByText(datasetLabel)); + await userEvent.click(screen.getByText('Errors')); - // Should see filter key options for the dataset - expect(screen.getByText('Select Filter Tag')).toBeInTheDocument(); - expect(screen.getByText(mockFilterKeys.browser!.key)).toBeInTheDocument(); - expect(screen.getByText(mockFilterKeys.environment!.key)).toBeInTheDocument(); + // Should see filter key options for the dataset + expect(screen.getByText('Select Filter Tag')).toBeInTheDocument(); + expect(screen.getByText(mockFilterKeys['browser.name']!.key)).toBeInTheDocument(); + expect(screen.getByText(mockFilterKeys.environment!.key)).toBeInTheDocument(); - // Return to dataset selection - await userEvent.click(screen.getByText('Back')); - } + // Return to dataset selection + await userEvent.click(screen.getByText('Back')); }); it('does not render unsupported filter keys', async () => { @@ -92,10 +90,10 @@ describe('AddFilter', () => { // Unsupported filter keys should not be included in the options expect( - screen.queryByText(mockFilterKeys.unsupportedFunction!.key) + screen.queryByText(mockFilterKeys['unsupported.function']!.key) ).not.toBeInTheDocument(); expect( - screen.queryByText(mockFilterKeys.unsupportedMeasurement!.key) + screen.queryByText(mockFilterKeys['unsupported.measurement']!.key) ).not.toBeInTheDocument(); }); @@ -114,14 +112,16 @@ describe('AddFilter', () => { // Select arbitrary dataset and filter key await userEvent.click(screen.getByText('Errors')); - await userEvent.click(screen.getByText(mockFilterKeys.browser!.key)); - await userEvent.click(screen.getByText('Add Filter')); + await userEvent.click( + screen.getByRole('option', {name: mockFilterKeys['browser.name']!.key}) + ); + await userEvent.click(screen.getByRole('button', {name: 'Add Filter'})); // Verify onAddFilter was called with the added global filter object expect(onAddFilter).toHaveBeenCalledTimes(1); expect(onAddFilter).toHaveBeenCalledWith({ dataset: WidgetType.ERRORS, - tag: mockFilterKeys.browser, + tag: mockFilterKeys['browser.name'], value: '', }); }); diff --git a/static/app/views/dashboards/globalFilter/addFilter.tsx b/static/app/views/dashboards/globalFilter/addFilter.tsx index 18b3fe47217ff7..b924ee57c040ea 100644 --- a/static/app/views/dashboards/globalFilter/addFilter.tsx +++ b/static/app/views/dashboards/globalFilter/addFilter.tsx @@ -11,7 +11,12 @@ import {IconAdd, IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Tag} from 'sentry/types/group'; -import {FieldKind, getFieldDefinition, prettifyTagKey} from 'sentry/utils/fields'; +import { + FieldKind, + FieldValueType, + getFieldDefinition, + prettifyTagKey, +} from 'sentry/utils/fields'; import type {SearchBarData} from 'sentry/views/dashboards/datasetConfig/base'; import {WidgetType, type GlobalFilter} from 'sentry/views/dashboards/types'; import {shouldExcludeTracingKeys} from 'sentry/views/performance/utils'; @@ -25,19 +30,12 @@ export const DATASET_CHOICES = new Map([ ]); const UNSUPPORTED_FIELD_KINDS = [FieldKind.FUNCTION, FieldKind.MEASUREMENT]; +const SUPPORTED_FIELD_VALUE_TYPES = [FieldValueType.STRING, FieldValueType.BOOLEAN]; export function getDatasetLabel(dataset: WidgetType) { return DATASET_CHOICES.get(dataset) ?? ''; } -function getTagType(tag: Tag, dataset: WidgetType) { - const fieldType = - dataset === WidgetType.SPANS ? 'span' : dataset === WidgetType.LOGS ? 'log' : 'event'; - const fieldDefinition = getFieldDefinition(tag.key, fieldType, tag.kind); - - return ; -} - type AddFilterProps = { getSearchBarData: (widgetType: WidgetType) => SearchBarData; globalFilters: GlobalFilter[]; @@ -70,12 +68,39 @@ function AddFilter({globalFilters, getSearchBarData, onAddFilter}: AddFilterProp // Get filter keys for the selected dataset const filterKeyOptions = selectedDataset - ? Object.entries(filterKeys).map(([_, tag]) => { + ? Object.entries(filterKeys).flatMap(([_, tag]) => { + const fieldType = (datasetType: WidgetType) => { + switch (datasetType) { + case WidgetType.SPANS: + return 'span'; + case WidgetType.LOGS: + return 'log'; + default: + return 'event'; + } + }; + const fieldDefinition = getFieldDefinition( + tag.key, + fieldType(selectedDataset), + tag.kind + ); + const valueType = fieldDefinition?.valueType; + + if (!valueType || !SUPPORTED_FIELD_VALUE_TYPES.includes(valueType)) { + return []; + } + return { value: tag.key, label: prettifyTagKey(tag.key), - trailingItems: {getTagType(tag, selectedDataset)}, - disabled: globalFilters.some(filter => filter.tag.key === tag.key), + trailingItems: ( + + + + ), + disabled: globalFilters.some( + filter => filter.tag.key === tag.key && filter.dataset === selectedDataset + ), }; }) : []; diff --git a/static/app/views/dashboards/globalFilter/filterSelector.tsx b/static/app/views/dashboards/globalFilter/filterSelector.tsx index e1400b0e95fc87..60a16a01840d51 100644 --- a/static/app/views/dashboards/globalFilter/filterSelector.tsx +++ b/static/app/views/dashboards/globalFilter/filterSelector.tsx @@ -11,6 +11,7 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {keepPreviousData, useQuery} from 'sentry/utils/queryClient'; import {useDebouncedValue} from 'sentry/utils/useDebouncedValue'; +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'; @@ -42,8 +43,12 @@ function FilterSelector({ }, [initialValues]); const {dataset, tag} = globalFilter; + const pageFilters = usePageFilters(); - const baseQueryKey = useMemo(() => ['global-dashboard-filters-tag-values', tag], [tag]); + const baseQueryKey = useMemo( + () => ['global-dashboard-filters-tag-values', tag, pageFilters.selection], + [tag, pageFilters.selection] + ); const queryKey = useDebouncedValue(baseQueryKey); const queryResult = useQuery({ @@ -59,7 +64,7 @@ function FilterSelector({ }); const {data, isFetching} = queryResult; - const options = useMemo(() => { + const fetchedOptions = useMemo(() => { if (!data) return []; return data.map(value => ({ label: value, @@ -67,6 +72,17 @@ function FilterSelector({ })); }, [data]); + const savedFilterValueOptions = useMemo(() => { + return activeFilterValues + .map(value => ({ + label: value, + value, + })) + .filter(option => !fetchedOptions.some(o => o.value === option.value)); + }, [activeFilterValues, fetchedOptions]); + + const options = [...savedFilterValueOptions, ...fetchedOptions]; + const handleChange = (opts: string[]) => { if (isEqual(opts, activeFilterValues)) { return;