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:
Joe Li
2026-02-19 10:33:33 -08:00
committed by GitHub
parent 5cc8ae5427
commit 6a61baf5be
2 changed files with 266 additions and 25 deletions

View File

@@ -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();
});

View File

@@ -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