Files
grafana/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
Konrad Lalik 0a8dccc19a Alerting: New alert list filter improvements (#103107)
* Move filtering code to generators for performance reasons

Discarding rules and groups early in the iterable chain limits the number of promises we need to wait for which improves performance significantly

* Add error handling for generators

* Add support for data source filter for GMA rules

* search WIP fix

* Fix datasource filter

* Move filtering back to filtered rules hook, use paged groups for improved performance

* Add queriedDatasources field to grafana managed rules and update filtering logic to rely on it

- Introduced a new field `queriedDatasources` in the AlertingRule struct to track data sources used in rules.
- Updated the Prometheus API to populate `queriedDatasources` when creating alerting rules.
- Modified filtering logic in the ruleFilter function to utilize the new `queriedDatasources` field for improved data source matching.
- Adjusted related tests to reflect changes in rule structure and filtering behavior.

* Add FilterView performance logging

* Improve GMA Prometheus types, rename queried datasources property

* Use custom generator helpers for flattening and filtering rule groups

* Fix lint errors, add missing translations

* Revert test condition

* Refactor api prom changes

* Fix lint errors

* Update backend tests

* Refactor rule list components to improve error handling and data source management

- Enhanced error handling in FilterViewResults by logging errors before returning an empty iterable.
- Simplified conditional rendering in GrafanaRuleLoader for better readability.
- Updated data source handling in PaginatedDataSourceLoader and PaginatedGrafanaLoader to use new individual rule group generator.
- Renamed toPageless function to toIndividualRuleGroups for clarity in prometheusGroupsGenerator.
- Improved filtering logic in useFilteredRulesIterator to utilize a dedicated function for data source type validation.
- Added isRulesDataSourceType utility function for better data source type checks.
- Removed commented-out code in PromRuleDTOBase for cleaner interface definition.

* Fix abort controller on FilterView

* Improve generators filtering

* fix abort controller

* refactor cancelSearch

* make states exclusive

* Load full page in one loadResultPage call

* Update tests, update translations

* Refactor filter status into separate component

* hoist hook

* Use the new function for supported rules source type

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
2025-04-11 10:02:34 +02:00

360 lines
9.8 KiB
TypeScript

import { render } from 'test/test-utils';
import { byTestId, byText } from 'testing-library-selector';
import { PromOptions } from '@grafana/prometheus';
import { setPluginLinksHook } from '@grafana/runtime';
import config from 'app/core/config';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { AccessControlAction } from 'app/types';
import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto';
import { PanelAlertTabContent } from './PanelAlertTabContent';
import * as apiRuler from './api/ruler';
import * as alertingAbilities from './hooks/useAbilities';
import { mockAlertRuleApi, setupMswServer } from './mockApi';
import {
grantUserPermissions,
mockDataSource,
mockPromAlert,
mockPromAlertingRule,
mockRulerAlertingRule,
mockRulerRuleGroup,
} from './mocks';
import { captureRequests } from './mocks/server/events';
import { RuleFormValues } from './types/rule-form';
import { Annotation } from './utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('./api/ruler');
jest.spyOn(alertingAbilities, 'useAlertRuleAbility');
const prometheusModuleSettings = { alerting: true, module: 'core:plugin/prometheus' };
const dataSources = {
prometheus: mockDataSource<PromOptions>(
{
name: 'Prometheus',
type: DataSourceType.Prometheus,
isDefault: false,
jsonData: { manageAlerts: true },
},
prometheusModuleSettings
),
default: mockDataSource<PromOptions>(
{
name: 'Default',
type: DataSourceType.Prometheus,
isDefault: true,
jsonData: { manageAlerts: true },
},
prometheusModuleSettings
),
prometheusMinInterval: mockDataSource<PromOptions>(
{
name: 'Prometheus Min Interval',
type: DataSourceType.Prometheus,
isDefault: false,
jsonData: { manageAlerts: true, timeInterval: '7m' },
},
prometheusModuleSettings
),
};
const mocks = {
useAlertRuleAbilityMock: jest.mocked(alertingAbilities.useAlertRuleAbility),
rulerBuilderMock: jest.mocked(apiRuler.rulerUrlBuilder),
};
const renderAlertTabContent = (dashboard: DashboardModel, panel: PanelModel) =>
render(<PanelAlertTabContent dashboard={dashboard} panel={panel} />);
const promResponse: PromRulesResponse = {
status: 'success',
data: {
groups: [
{
name: 'mygroup',
file: 'default',
rules: [
mockPromAlertingRule({
name: 'dashboardrule1',
alerts: [
mockPromAlert({
labels: { severity: 'critical' },
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
}),
],
totals: { alerting: 1 },
totalsFiltered: { alerting: 1 },
}),
],
interval: 20,
},
{
name: 'othergroup',
file: 'default',
rules: [
mockPromAlertingRule({
name: 'dashboardrule2',
alerts: [
mockPromAlert({
labels: { severity: 'critical' },
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
}),
],
totals: { alerting: 1 },
totalsFiltered: { alerting: 1 },
}),
],
interval: 20,
},
],
},
};
const rulerResponse = {
default: [
mockRulerRuleGroup({
name: 'mygroup',
rules: [
mockRulerAlertingRule({
alert: 'dashboardrule1',
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
}),
],
}),
{
name: 'othergroup',
rules: [
mockRulerAlertingRule({
alert: 'dashboardrule2',
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
}),
],
},
],
};
const dashboard = {
uid: '12',
time: {
from: 'now-6h',
to: 'now',
},
meta: {
canSave: true,
folderUid: 'abc',
folderTitle: 'super folder',
},
} as DashboardModel;
const panel = new PanelModel({
datasource: {
type: 'prometheus',
uid: dataSources.prometheus.uid,
},
title: 'mypanel',
id: 34,
targets: [
{
expr: 'sum(some_metric [$__interval])) by (app)',
refId: 'A',
},
],
});
const ui = {
row: byTestId('row'),
createButton: byTestId<HTMLAnchorElement>('create-alert-rule-button'),
notSavedYet: byText('Dashboard not saved'),
};
const server = setupMswServer();
describe('PanelAlertTabContent', () => {
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleDelete,
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
setupDataSources(...Object.values(dataSources));
setPluginLinksHook(() => ({
links: [],
isLoading: false,
}));
mocks.rulerBuilderMock.mockReturnValue({
rules: () => ({ path: `api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules` }),
namespace: () => ({ path: 'ruler' }),
namespaceGroup: () => ({ path: 'ruler' }),
});
mocks.useAlertRuleAbilityMock.mockReturnValue([true, true]);
mockAlertRuleApi(server).prometheusRuleNamespaces(GRAFANA_RULES_SOURCE_NAME, promResponse);
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, rulerResponse);
config.unifiedAlertingEnabled = true;
});
it('Will take into account panel maxDataPoints', async () => {
renderAlertTabContent(
dashboard,
new PanelModel({
...panel,
maxDataPoints: 100,
interval: '10s',
})
);
const button = await ui.createButton.find();
const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [5m])) by (app)',
refId: 'A',
datasource: {
type: 'prometheus',
uid: 'mock-ds-2',
},
interval: '',
intervalMs: 300000,
maxDataPoints: 100,
});
});
it('Will work with default datasource', async () => {
renderAlertTabContent(
dashboard,
new PanelModel({
...panel,
datasource: undefined,
maxDataPoints: 100,
interval: '10s',
})
);
const button = await ui.createButton.find();
const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [5m])) by (app)',
refId: 'A',
datasource: {
type: 'prometheus',
uid: 'mock-ds-3',
},
interval: '',
intervalMs: 300000,
maxDataPoints: 100,
});
});
it('should not make requests for unsaved dashboard', async () => {
const capture = captureRequests();
const unsavedDashboard = {
...dashboard,
uid: null,
} as DashboardModel;
renderAlertTabContent(
unsavedDashboard,
new PanelModel({
...panel,
datasource: undefined,
maxDataPoints: 100,
interval: '10s',
})
);
expect(await ui.notSavedYet.find()).toBeInTheDocument();
const requests = await capture;
expect(requests.length).toBe(0);
});
it('Will take into account datasource minInterval', async () => {
renderAlertTabContent(
dashboard,
new PanelModel({
...panel,
maxDataPoints: 100,
datasource: {
type: 'prometheus',
uid: dataSources.prometheusMinInterval.uid,
},
})
);
const button = await ui.createButton.find();
const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [7m])) by (app)',
refId: 'A',
datasource: {
type: 'prometheus',
uid: 'mock-ds-4',
},
interval: '',
intervalMs: 420000,
maxDataPoints: 100,
});
});
it('Will render alerts belonging to panel and a button to create alert from panel queries', async () => {
config.unifiedAlertingEnabled = true;
renderAlertTabContent(dashboard, panel);
const rows = await ui.row.findAll();
// after updating to RTKQ, the response is already returning the alerts belonging to the panel
expect(rows).toHaveLength(2);
expect(rows[0]).toHaveTextContent(/dashboardrule1/);
expect(rows[0]).not.toHaveTextContent(/dashboardrule2/);
const button = await ui.createButton.find();
const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1]));
const defaultsWithDeterministicTime: Partial<RuleFormValues> = {
...defaults,
queries: defaults.queries.map((q: AlertQuery) => {
return {
...q,
// Fix computed time stamp to avoid assertion flakiness
...(q.relativeTimeRange ? { relativeTimeRange: { from: 21600, to: 0 } } : {}),
};
}),
};
expect(defaultsWithDeterministicTime).toMatchSnapshot();
});
});