This is an automated email from the ASF dual-hosted git repository.
jli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 6a61baf5be8 fix(alerts): show friendly filter names in report edit
modal (#38054)
6a61baf5be8 is described below
commit 6a61baf5be818821d3ce110fa8cdc5c459132e34
Author: Joe Li <[email protected]>
AuthorDate: Thu Feb 19 10:33:33 2026 -0800
fix(alerts): show friendly filter names in report edit modal (#38054)
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../src/features/alerts/AlertReportModal.test.tsx | 284 +++++++++++++++++++--
.../src/features/alerts/AlertReportModal.tsx | 7 +
2 files changed, 266 insertions(+), 25 deletions(-)
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(<AlertReportModal {...generateMockedProps(true, true)} />, {
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(<AlertReportModal {...editProps} />, {
+ 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(<AlertReportModal {...editProps} />, {
+ 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(<AlertReportModal {...editProps} />, {
+ 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<AlertReportModalProps> = ({
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