mirror of
https://github.com/apache/superset.git
synced 2026-08-03 20:42:30 +00:00
fix(alerts): show friendly filter names in report edit modal (#38054)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user