diff --git a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx index 91adaa3eeb4..585fd7c2bf3 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx @@ -18,6 +18,7 @@ */ import fetchMock from 'fetch-mock'; import { + act, render, screen, userEvent, @@ -104,9 +105,74 @@ const generateMockPayload = (dashboard = true) => { // mocking resource endpoints const FETCH_DASHBOARD_ENDPOINT = 'glob:*/api/v1/report/1'; const FETCH_CHART_ENDPOINT = 'glob:*/api/v1/report/2'; +const FETCH_REPORT_WITH_FILTERS_ENDPOINT = 'glob:*/api/v1/report/3'; +const FETCH_REPORT_NO_FILTER_NAME_ENDPOINT = 'glob:*/api/v1/report/4'; +const FETCH_REPORT_OVERWRITE_ENDPOINT = 'glob:*/api/v1/report/5'; fetchMock.get(FETCH_DASHBOARD_ENDPOINT, { result: generateMockPayload(true) }); fetchMock.get(FETCH_CHART_ENDPOINT, { result: generateMockPayload(false) }); +fetchMock.get(FETCH_REPORT_WITH_FILTERS_ENDPOINT, { + result: { + ...generateMockPayload(true), + id: 3, + type: 'Report', + extra: { + dashboard: { + nativeFilters: [ + { + nativeFilterId: 'NATIVE_FILTER-abc123', + filterName: 'Country', + filterType: 'filter_select', + columnName: 'country', + columnLabel: 'Country', + filterValues: ['USA'], + }, + ], + }, + }, + }, +}); +fetchMock.get(FETCH_REPORT_NO_FILTER_NAME_ENDPOINT, { + result: { + ...generateMockPayload(true), + id: 4, + type: 'Report', + extra: { + dashboard: { + nativeFilters: [ + { + nativeFilterId: 'NATIVE_FILTER-xyz789', + filterType: 'filter_select', + columnName: 'region', + columnLabel: 'Region', + filterValues: ['West'], + }, + ], + }, + }, + }, +}); +fetchMock.get(FETCH_REPORT_OVERWRITE_ENDPOINT, { + result: { + ...generateMockPayload(true), + id: 5, + type: 'Report', + extra: { + dashboard: { + nativeFilters: [ + { + nativeFilterId: 'NATIVE_FILTER-abc123', + filterName: 'Country', + filterType: 'filter_select', + columnName: 'country', + columnLabel: 'Country', + filterValues: ['USA'], + }, + ], + }, + }, + }, +}); // Related mocks const ownersEndpoint = 'glob:*/api/v1/alert/related/owners?*'; @@ -130,6 +196,38 @@ fetchMock.get( { name: tabsEndpoint }, ); +// Restore the default tabs route and remove any test-specific overrides. +// Called in afterEach so cleanup runs even when a test fails mid-way. +const restoreDefaultTabsRoute = () => { + for (const name of [ + 'clear-icon-tabs', + 'clear-icon-chart-data', + 'deferred-tabs', + 'overwrite-chart-data', + ]) { + try { + fetchMock.removeRoute(name); + } catch { + // route may not exist if the test that adds it didn't run + } + } + // Re-add the default empty tabs route if it was replaced + try { + fetchMock.removeRoute(tabsEndpoint); + } catch { + // already removed + } + fetchMock.get( + tabsEndpoint, + { result: { all_tabs: {}, tab_tree: [] } }, + { name: tabsEndpoint }, + ); +}; + +afterEach(() => { + restoreDefaultTabsRoute(); +}); + // Create a valid alert with all required fields entered for validation check // @ts-expect-error will add id in factory function @@ -713,34 +811,44 @@ test('filter reappears in dropdown after clearing with X icon', async () => { const chartDataEndpoint = 'glob:*/api/v1/chart/data*'; fetchMock.removeRoute(tabsEndpoint); - fetchMock.get(tabsEndpoint, { - result: { - all_tabs: { tab1: 'Tab 1' }, - tab_tree: [{ title: 'Tab 1', value: 'tab1' }], - native_filters: { - all: [ - { - id: 'NATIVE_FILTER-test1', - name: 'Test Filter 1', - filterType: 'filter_select', - targets: [{ column: { name: 'test_column_1' } }], - adhoc_filters: [], - }, - ], - tab1: [ - { - id: 'NATIVE_FILTER-test2', - name: 'Test Filter 2', - filterType: 'filter_select', - targets: [{ column: { name: 'test_column_2' } }], - adhoc_filters: [], - }, - ], + fetchMock.get( + tabsEndpoint, + { + result: { + all_tabs: { tab1: 'Tab 1' }, + tab_tree: [{ title: 'Tab 1', value: 'tab1' }], + native_filters: { + all: [ + { + id: 'NATIVE_FILTER-test1', + name: 'Test Filter 1', + filterType: 'filter_select', + targets: [{ column: { name: 'test_column_1' } }], + adhoc_filters: [], + }, + ], + tab1: [ + { + id: 'NATIVE_FILTER-test2', + name: 'Test Filter 2', + filterType: 'filter_select', + targets: [{ column: { name: 'test_column_2' } }], + adhoc_filters: [], + }, + ], + }, }, }, - }); + { name: 'clear-icon-tabs' }, + ); - fetchMock.post(chartDataEndpoint, { result: [{ data: [] }] }); + fetchMock.post( + chartDataEndpoint, + { result: [{ data: [] }] }, + { + name: 'clear-icon-chart-data', + }, + ); render(, { useRedux: true, @@ -793,3 +901,129 @@ test('filter reappears in dropdown after clearing with X icon', async () => { ).toBeInTheDocument(); }); }); + +test('edit mode shows friendly filter names instead of raw IDs', async () => { + const props = generateMockedProps(true, true); + const editProps = { + ...props, + alert: { ...validAlert, id: 3 }, + }; + + render(, { + useRedux: true, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + + await waitFor(() => { + const selectionItem = document.querySelector( + '.ant-select-selection-item[title="Country"]', + ); + expect(selectionItem).toBeInTheDocument(); + }); + + expect( + document.querySelector( + '.ant-select-selection-item[title="NATIVE_FILTER-abc123"]', + ), + ).not.toBeInTheDocument(); +}); + +test('edit mode falls back to raw ID when filterName is missing', async () => { + const props = generateMockedProps(true, true); + const editProps = { + ...props, + alert: { ...validAlert, id: 4 }, + }; + + render(, { + useRedux: true, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + + await waitFor(() => { + const selectionItem = document.querySelector( + '.ant-select-selection-item[title="NATIVE_FILTER-xyz789"]', + ); + expect(selectionItem).toBeInTheDocument(); + }); +}); + +test('tabs metadata overwrites seeded filter options', async () => { + const chartDataEndpoint = 'glob:*/api/v1/chart/data*'; + + // Deferred promise to control when the tabs response resolves + let resolveTabsResponse!: (value: unknown) => void; + const deferredTabs = new Promise(resolve => { + resolveTabsResponse = resolve; + }); + + const tabsResult = { + result: { + all_tabs: { tab1: 'Tab 1' }, + tab_tree: [{ title: 'Tab 1', value: 'tab1' }], + native_filters: { + all: [ + { + id: 'NATIVE_FILTER-abc123', + name: 'Country (All Filters)', + filterType: 'filter_select', + targets: [{ column: { name: 'country' }, datasetId: 1 }], + adhoc_filters: [], + }, + ], + tab1: [], + }, + }, + }; + + // Replace only the tabs route with a deferred version + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, () => deferredTabs.then(() => tabsResult), { + name: 'deferred-tabs', + }); + fetchMock.post( + chartDataEndpoint, + { result: [{ data: [] }] }, + { + name: 'overwrite-chart-data', + }, + ); + + const props = generateMockedProps(true, true); + const editProps = { + ...props, + alert: { ...validAlert, id: 5 }, + }; + + render(, { + useRedux: true, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + + // Seeded label from saved data appears before tabs respond + const filterSelect = screen.getByRole('combobox', { + name: /select filter/i, + }); + const selectContainer = filterSelect.closest('.ant-select') as HTMLElement; + await waitFor(() => { + expect(within(selectContainer).getByTitle('Country')).toBeInTheDocument(); + }); + + // Resolve the deferred tabs response + await act(async () => { + resolveTabsResponse(undefined); + }); + + // Tabs metadata overwrites the seeded label + await waitFor(() => { + expect( + within(selectContainer).getByTitle('Country (All Filters)'), + ).toBeInTheDocument(); + }); + expect( + within(selectContainer).queryByTitle('Country'), + ).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/features/alerts/AlertReportModal.tsx b/superset-frontend/src/features/alerts/AlertReportModal.tsx index 3974db42e33..fa25c7c75fd 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.tsx @@ -1897,6 +1897,13 @@ const AlertReportModal: FunctionComponent = ({ if (resource.extra?.dashboard?.nativeFilters) { const filters = resource.extra.dashboard.nativeFilters; setNativeFilterData(filters); + // Seed options from saved data so names display while dashboard metadata loads + const savedOptions = filters + .filter(f => f.nativeFilterId && f.filterName) + .map(f => ({ value: f.nativeFilterId!, label: f.filterName! })); + if (savedOptions.length > 0) { + setNativeFilterOptions(savedOptions); + } } // Add notification settings