Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions static/app/views/dashboards/detail.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ describe('Dashboards > Detail', () => {
url: '/organizations/org-slug/releases/stats/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/measurements-meta/',
body: [],
});
});

afterEach(() => {
Expand Down
5 changes: 5 additions & 0 deletions static/app/views/dashboards/filtersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -147,6 +150,7 @@ export default function FiltersBar({
<FilterSelector
key={filter.tag.key}
globalFilter={filter}
searchBarData={getSearchBarData(filter.dataset)}
onUpdateFilter={updatedFilter => {
updateGlobalFilters(
activeGlobalFilters.map(f =>
Expand All @@ -162,6 +166,7 @@ export default function FiltersBar({
/>
))}
<AddFilter
getSearchBarData={getSearchBarData}
onAddFilter={newFilter => {
updateGlobalFilters([...activeGlobalFilters, newFilter]);
}}
Expand Down
36 changes: 8 additions & 28 deletions static/app/views/dashboards/globalFilter/addFilter.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -34,43 +31,26 @@ 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(<AddFilter onAddFilter={() => {}} />);
render(<AddFilter getSearchBarData={getSearchBarData} onAddFilter={() => {}} />);
await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'}));
for (const dataset of DATASET_CHOICES.values()) {
expect(screen.getByText(dataset)).toBeInTheDocument();
}
});

it('retrieves filter keys for each dataset', async () => {
render(<AddFilter onAddFilter={() => {}} />);
render(<AddFilter getSearchBarData={getSearchBarData} onAddFilter={() => {}} />);

// 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));
Expand All @@ -86,7 +66,7 @@ describe('AddFilter', () => {
});

it('does not render unsupported filter keys', async () => {
render(<AddFilter onAddFilter={() => {}} />);
render(<AddFilter getSearchBarData={getSearchBarData} onAddFilter={() => {}} />);

// Open the dropdown and select an arbitrary dataset
await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'}));
Expand All @@ -103,7 +83,7 @@ describe('AddFilter', () => {

it('calls onAddFilter with expected global filter object', async () => {
const onAddFilter = jest.fn();
render(<AddFilter onAddFilter={onAddFilter} />);
render(<AddFilter getSearchBarData={getSearchBarData} onAddFilter={onAddFilter} />);

// Open add global filter drop down
await userEvent.click(screen.getByRole('button', {name: 'Add Global Filter'}));
Expand Down
47 changes: 19 additions & 28 deletions static/app/views/dashboards/globalFilter/addFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
Expand All @@ -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<WidgetType | null>(null);
const [selectedFilterKey, setSelectedFilterKey] = useState<Tag | null>(null);
const [isSelectingFilterKey, setIsSelectingFilterKey] = useState(false);
const {selection} = usePageFilters();

// Dataset selection before showing filter keys
const datasetOptions = useMemo(() => {
Expand All @@ -58,34 +57,26 @@ function AddFilter({onAddFilter}: AddFilterProps) {
}));
}, []);

const datasetFilterKeysMap = new Map<WidgetType, TagCollection>();

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<string, Tag> = 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: <TagBadge>{getTagType(tag, selectedDataset)}</TagBadge>,
};
});
const filterKeyOptions = selectedDataset
? Object.entries(filterKeys).map(([_, tag]) => {
return {
value: tag.key,
label: prettifyTagKey(tag.key),
trailingItems: <TagBadge>{getTagType(tag, selectedDataset)}</TagBadge>,
};
})
: [];

// Footer for filter key selection for adding filters and returning to dataset selection
const filterOptionsMenuFooter = ({closeOverlay}: {closeOverlay: () => void}) => (
Expand Down
38 changes: 10 additions & 28 deletions static/app/views/dashboards/globalFilter/filterSelector.spec.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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(
<FilterSelector
globalFilter={mockGlobalFilter}
searchBarData={mockSearchBarData}
onUpdateFilter={mockOnUpdateFilter}
onRemoveFilter={mockOnRemoveFilter}
/>
Expand All @@ -68,6 +47,7 @@ describe('FilterSelector', () => {
render(
<FilterSelector
globalFilter={mockGlobalFilter}
searchBarData={mockSearchBarData}
onUpdateFilter={mockOnUpdateFilter}
onRemoveFilter={mockOnRemoveFilter}
/>
Expand Down Expand Up @@ -98,6 +78,7 @@ describe('FilterSelector', () => {
render(
<FilterSelector
globalFilter={{...mockGlobalFilter, value: 'browser:[firefox,chrome]'}}
searchBarData={mockSearchBarData}
onUpdateFilter={mockOnUpdateFilter}
onRemoveFilter={mockOnRemoveFilter}
/>
Expand All @@ -114,6 +95,7 @@ describe('FilterSelector', () => {
render(
<FilterSelector
globalFilter={mockGlobalFilter}
searchBarData={mockSearchBarData}
onUpdateFilter={mockOnUpdateFilter}
onRemoveFilter={mockOnRemoveFilter}
/>
Expand Down
11 changes: 4 additions & 7 deletions static/app/views/dashboards/globalFilter/filterSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
Loading