diff --git a/static/app/views/dashboards/detail.spec.tsx b/static/app/views/dashboards/detail.spec.tsx index 726f050ddd1cd6..a8da89b4830899 100644 --- a/static/app/views/dashboards/detail.spec.tsx +++ b/static/app/views/dashboards/detail.spec.tsx @@ -149,6 +149,10 @@ describe('Dashboards > Detail', () => { url: '/organizations/org-slug/releases/stats/', body: [], }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/measurements-meta/', + body: [], + }); }); afterEach(() => { diff --git a/static/app/views/dashboards/filtersBar.tsx b/static/app/views/dashboards/filtersBar.tsx index 34807ae196ccfd..04a060fd9df43c 100644 --- a/static/app/views/dashboards/filtersBar.tsx +++ b/static/app/views/dashboards/filtersBar.tsx @@ -19,6 +19,7 @@ import usePageFilters from 'sentry/utils/usePageFilters'; import {useUser} from 'sentry/utils/useUser'; import {useUserTeams} from 'sentry/utils/useUserTeams'; import AddFilter from 'sentry/views/dashboards/globalFilter/addFilter'; +import {useDatasetSearchBarData} from 'sentry/views/dashboards/hooks/useDatasetSearchBarData'; import {useInvalidateStarredDashboards} from 'sentry/views/dashboards/hooks/useInvalidateStarredDashboards'; import {getDashboardFiltersFromURL} from 'sentry/views/dashboards/utils'; @@ -59,6 +60,8 @@ export default function FiltersBar({ const organization = useOrganization(); const currentUser = useUser(); const {teams: userTeams} = useUserTeams(); + const getSearchBarData = useDatasetSearchBarData(); + const hasEditAccess = checkUserHasEditAccess( currentUser, userTeams, @@ -147,6 +150,7 @@ export default function FiltersBar({ { updateGlobalFilters( activeGlobalFilters.map(f => @@ -162,6 +166,7 @@ export default function FiltersBar({ /> ))} { updateGlobalFilters([...activeGlobalFilters, newFilter]); }} diff --git a/static/app/views/dashboards/globalFilter/addFilter.spec.tsx b/static/app/views/dashboards/globalFilter/addFilter.spec.tsx index db5a04e0e9d2ba..82cca789c53665 100644 --- a/static/app/views/dashboards/globalFilter/addFilter.spec.tsx +++ b/static/app/views/dashboards/globalFilter/addFilter.spec.tsx @@ -2,13 +2,10 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import type {TagCollection} from 'sentry/types/group'; import {FieldKind} from 'sentry/utils/fields'; -import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; +import {type SearchBarData} from 'sentry/views/dashboards/datasetConfig/base'; import AddFilter, {DATASET_CHOICES} from 'sentry/views/dashboards/globalFilter/addFilter'; import {WidgetType} from 'sentry/views/dashboards/types'; -// Mock getDatasetConfig -jest.mock('sentry/views/dashboards/datasetConfig/base'); - describe('AddFilter', () => { // Mock filter keys returned by the search bar data provider const mockFilterKeys: TagCollection = { @@ -34,25 +31,14 @@ describe('AddFilter', () => { }, }; - const mockUseSearchBarDataProvider = jest.fn(() => ({ + const getSearchBarData = (_: WidgetType): SearchBarData => ({ getFilterKeys: () => mockFilterKeys, - })); - - beforeEach(() => { - MockApiClient.clearMockResponses(); - - // Mock getDatasetConfig that returns a config with a mock search bar data provider - jest.mocked(getDatasetConfig).mockReturnValue({ - useSearchBarDataProvider: mockUseSearchBarDataProvider, - } as any); - }); - - afterEach(() => { - jest.clearAllMocks(); + getFilterKeySections: () => [], + getTagValues: () => Promise.resolve([]), }); it('renders all dataset options', async () => { - render( {}} />); + render( {}} />); await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'})); for (const dataset of DATASET_CHOICES.values()) { expect(screen.getByText(dataset)).toBeInTheDocument(); @@ -60,17 +46,11 @@ describe('AddFilter', () => { }); it('retrieves filter keys for each dataset', async () => { - render( {}} />); + render( {}} />); // Open the add global filter drop down await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'})); - // Verify search bar data provider was called for each dataset type - for (const [widgetType] of DATASET_CHOICES.entries()) { - expect(getDatasetConfig).toHaveBeenCalledWith(widgetType); - } - expect(mockUseSearchBarDataProvider).toHaveBeenCalledTimes(DATASET_CHOICES.size); - // Verify filter keys are shown for each dataset for (const datasetLabel of DATASET_CHOICES.values()) { await userEvent.click(screen.getByText(datasetLabel)); @@ -86,7 +66,7 @@ describe('AddFilter', () => { }); it('does not render unsupported filter keys', async () => { - render( {}} />); + render( {}} />); // Open the dropdown and select an arbitrary dataset await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'})); @@ -103,7 +83,7 @@ describe('AddFilter', () => { it('calls onAddFilter with expected global filter object', async () => { const onAddFilter = jest.fn(); - render(); + render(); // Open add global filter drop down await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'})); diff --git a/static/app/views/dashboards/globalFilter/addFilter.tsx b/static/app/views/dashboards/globalFilter/addFilter.tsx index 22f739b55f44f6..949208618bf41b 100644 --- a/static/app/views/dashboards/globalFilter/addFilter.tsx +++ b/static/app/views/dashboards/globalFilter/addFilter.tsx @@ -10,10 +10,9 @@ import {ValueType} from 'sentry/components/searchQueryBuilder/tokens/filterKeyLi import {IconAdd, IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import type {Tag, TagCollection} from 'sentry/types/group'; +import type {Tag} from 'sentry/types/group'; import {FieldKind, getFieldDefinition, prettifyTagKey} from 'sentry/utils/fields'; -import usePageFilters from 'sentry/utils/usePageFilters'; -import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; +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'; @@ -31,7 +30,7 @@ export function getDatasetLabel(dataset: WidgetType) { return DATASET_CHOICES.get(dataset) ?? ''; } -function getTagType(tag: Tag, dataset: WidgetType | null) { +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); @@ -40,14 +39,14 @@ function getTagType(tag: Tag, dataset: WidgetType | null) { } type AddFilterProps = { + getSearchBarData: (widgetType: WidgetType) => SearchBarData; onAddFilter: (filter: GlobalFilter) => void; }; -function AddFilter({onAddFilter}: AddFilterProps) { +function AddFilter({getSearchBarData, onAddFilter}: AddFilterProps) { const [selectedDataset, setSelectedDataset] = useState(null); const [selectedFilterKey, setSelectedFilterKey] = useState(null); const [isSelectingFilterKey, setIsSelectingFilterKey] = useState(false); - const {selection} = usePageFilters(); // Dataset selection before showing filter keys const datasetOptions = useMemo(() => { @@ -58,34 +57,26 @@ function AddFilter({onAddFilter}: AddFilterProps) { })); }, []); - const datasetFilterKeysMap = new Map(); - - DATASET_CHOICES.forEach((_, widgetType) => { - const datasetConfig = getDatasetConfig(widgetType); - if (datasetConfig.useSearchBarDataProvider) { - const dataProvider = datasetConfig.useSearchBarDataProvider({ - pageFilters: selection, - }); - const filterKeys = Object.fromEntries( - Object.entries(dataProvider.getFilterKeys()).filter( + const filterKeys: Record = selectedDataset + ? Object.fromEntries( + Object.entries(getSearchBarData(selectedDataset).getFilterKeys()).filter( ([key, value]) => !shouldExcludeTracingKeys(key) && (!value.kind || !UNSUPPORTED_FIELD_KINDS.includes(value.kind)) ) - ); - datasetFilterKeysMap.set(widgetType, filterKeys); - } - }); + ) + : {}; // Get filter keys for the selected dataset - const filterKeys = (selectedDataset && datasetFilterKeysMap.get(selectedDataset)) || {}; - const filterKeyOptions = Object.entries(filterKeys).map(([_, tag]) => { - return { - value: tag.key, - label: prettifyTagKey(tag.key), - trailingItems: {getTagType(tag, selectedDataset)}, - }; - }); + const filterKeyOptions = selectedDataset + ? Object.entries(filterKeys).map(([_, tag]) => { + return { + value: tag.key, + label: prettifyTagKey(tag.key), + trailingItems: {getTagType(tag, selectedDataset)}, + }; + }) + : []; // Footer for filter key selection for adding filters and returning to dataset selection const filterOptionsMenuFooter = ({closeOverlay}: {closeOverlay: () => void}) => ( diff --git a/static/app/views/dashboards/globalFilter/filterSelector.spec.tsx b/static/app/views/dashboards/globalFilter/filterSelector.spec.tsx index 5078d95e88c7d4..4a8024fa52469e 100644 --- a/static/app/views/dashboards/globalFilter/filterSelector.spec.tsx +++ b/static/app/views/dashboards/globalFilter/filterSelector.spec.tsx @@ -1,6 +1,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {FieldKind} from 'sentry/utils/fields'; +import type {SearchBarData} from 'sentry/views/dashboards/datasetConfig/base'; import FilterSelector from 'sentry/views/dashboards/globalFilter/filterSelector'; import {WidgetType, type GlobalFilter} from 'sentry/views/dashboards/types'; @@ -17,40 +18,18 @@ describe('FilterSelector', () => { }, value: '', }; - beforeEach(() => { - MockApiClient.clearMockResponses(); - // Mock tags endpoint - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/tags/', - body: [], - }); - - // Mock custom measurements endpoint - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/measurements-meta/', - body: {}, - }); - - // Mock tag values endpoint - MockApiClient.addMockResponse({ - url: `/organizations/org-slug/tags/${mockGlobalFilter.tag.key}/values/`, - body: [ - {name: 'chrome', value: 'chrome', count: 100}, - {name: 'firefox', value: 'firefox', count: 50}, - {name: 'safari', value: 'safari', count: 25}, - ], - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); + const mockSearchBarData: SearchBarData = { + getFilterKeySections: () => [], + getFilterKeys: () => ({}), + getTagValues: () => Promise.resolve(['chrome', 'firefox', 'safari']), + }; it('renders all filter values', async () => { render( @@ -68,6 +47,7 @@ describe('FilterSelector', () => { render( @@ -98,6 +78,7 @@ describe('FilterSelector', () => { render( @@ -114,6 +95,7 @@ describe('FilterSelector', () => { render( diff --git a/static/app/views/dashboards/globalFilter/filterSelector.tsx b/static/app/views/dashboards/globalFilter/filterSelector.tsx index fa0ad81ca4a4cf..a804ca34682ef3 100644 --- a/static/app/views/dashboards/globalFilter/filterSelector.tsx +++ b/static/app/views/dashboards/globalFilter/filterSelector.tsx @@ -7,8 +7,7 @@ import {MutableSearch} from 'sentry/components/searchSyntax/mutableSearch'; import {t} from 'sentry/locale'; import {keepPreviousData, useQuery} from 'sentry/utils/queryClient'; import {useDebouncedValue} from 'sentry/utils/useDebouncedValue'; -import usePageFilters from 'sentry/utils/usePageFilters'; -import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; +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 type {GlobalFilter} from 'sentry/views/dashboards/types'; @@ -17,10 +16,12 @@ type FilterSelectorProps = { globalFilter: GlobalFilter; onRemoveFilter: (filter: GlobalFilter) => void; onUpdateFilter: (filter: GlobalFilter) => void; + searchBarData: SearchBarData; }; function FilterSelector({ globalFilter, + searchBarData, onRemoveFilter, onUpdateFilter, }: FilterSelectorProps) { @@ -37,10 +38,6 @@ function FilterSelector({ }, [initialValues]); const {dataset, tag} = globalFilter; - const {selection} = usePageFilters(); - const dataProvider = getDatasetConfig(dataset).useSearchBarDataProvider!({ - pageFilters: selection, - }); const baseQueryKey = useMemo(() => ['global-dashboard-filters-tag-values', tag], [tag]); const queryKey = useDebouncedValue(baseQueryKey); @@ -50,7 +47,7 @@ function FilterSelector({ // eslint-disable-next-line @tanstack/query/exhaustive-deps queryKey, queryFn: async () => { - const result = await dataProvider?.getTagValues(tag, ''); + const result = await searchBarData.getTagValues(tag, ''); return result ?? []; }, placeholderData: keepPreviousData, diff --git a/static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx b/static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx new file mode 100644 index 00000000000000..8ef7d82404a7e7 --- /dev/null +++ b/static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx @@ -0,0 +1,53 @@ +import usePageFilters from 'sentry/utils/usePageFilters'; +import { + getDatasetConfig, + type SearchBarData, +} from 'sentry/views/dashboards/datasetConfig/base'; +import {WidgetType} from 'sentry/views/dashboards/types'; + +export function useDatasetSearchBarData(): (widgetType: WidgetType) => SearchBarData { + const {selection} = usePageFilters(); + + const errorsData = getDatasetConfig(WidgetType.ERRORS).useSearchBarDataProvider!({ + pageFilters: selection, + }); + + const logsData = getDatasetConfig(WidgetType.LOGS).useSearchBarDataProvider!({ + pageFilters: selection, + }); + + const spansData = getDatasetConfig(WidgetType.SPANS).useSearchBarDataProvider!({ + pageFilters: selection, + }); + + const issuesData = getDatasetConfig(WidgetType.ISSUE).useSearchBarDataProvider!({ + pageFilters: selection, + }); + + const releasesData = getDatasetConfig(WidgetType.RELEASE).useSearchBarDataProvider!({ + pageFilters: selection, + }); + + const getSearchBarData = (widgetType: WidgetType): SearchBarData => { + switch (widgetType) { + case WidgetType.ERRORS: + return errorsData; + case WidgetType.LOGS: + return logsData; + case WidgetType.SPANS: + return spansData; + case WidgetType.ISSUE: + return issuesData; + case WidgetType.RELEASE: + return releasesData; + default: + return { + getFilterKeySections: () => [], + getFilterKeys: () => ({}), + getTagValues: () => Promise.resolve([]), + }; + } + }; + + return getSearchBarData; +}