Alerting: Refactor rules filtering (#113898)
* Refactor rules filter to easily switch between frontend and backend filters * Refactor getFilter functions to receive filters as an argument * Move filter normalization to getFilter utils
This commit is contained in:
@@ -34,7 +34,7 @@ type PromRulesOptions = WithNotificationOptions<{
|
||||
groupNextToken?: string;
|
||||
}>;
|
||||
|
||||
type GrafanaPromRulesOptions = Omit<PromRulesOptions, 'ruleSource' | 'namespace' | 'excludeAlerts'> & {
|
||||
export type GrafanaPromRulesOptions = Omit<PromRulesOptions, 'ruleSource' | 'namespace' | 'excludeAlerts'> & {
|
||||
folderUid?: string;
|
||||
dashboardUid?: string;
|
||||
panelId?: number;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ListGroup } from './components/ListGroup';
|
||||
import { ListSection } from './components/ListSection';
|
||||
import { LoadMoreButton } from './components/LoadMoreButton';
|
||||
import { NoRulesFound } from './components/NoRulesFound';
|
||||
import { groupFilter as groupFilterFn } from './hooks/filters';
|
||||
import { getDatasourceFilter } from './hooks/filters';
|
||||
import { toIndividualRuleGroups, usePrometheusGroupsGenerator } from './hooks/prometheusGroupsGenerator';
|
||||
import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups';
|
||||
import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits';
|
||||
@@ -68,14 +68,24 @@ function PaginatedGroupsLoader({ rulesSourceIdentifier, application, groupFilter
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filterFn = useMemo(
|
||||
() => (group: PromRuleGroupDTO) =>
|
||||
groupFilterFn(group, {
|
||||
namespace: namespaceFilter,
|
||||
groupName: groupFilter,
|
||||
}),
|
||||
[namespaceFilter, groupFilter]
|
||||
);
|
||||
const filterFn = useMemo(() => {
|
||||
const { groupMatches } = getDatasourceFilter({
|
||||
namespace: namespaceFilter,
|
||||
groupName: groupFilter,
|
||||
freeFormWords: [],
|
||||
ruleName: '',
|
||||
labels: [],
|
||||
ruleType: undefined,
|
||||
ruleState: undefined,
|
||||
ruleHealth: undefined,
|
||||
dashboardUid: undefined,
|
||||
dataSourceNames: [],
|
||||
plugins: undefined,
|
||||
contactPoint: undefined,
|
||||
ruleSource: undefined,
|
||||
});
|
||||
return (group: PromRuleGroupDTO) => groupMatches(group);
|
||||
}, [namespaceFilter, groupFilter]);
|
||||
|
||||
const { isLoading, groups, hasMoreGroups, fetchMoreGroups, error } = useLazyLoadPrometheusGroups(
|
||||
groupsGenerator.current,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ListGroup } from './components/ListGroup';
|
||||
import { ListSection } from './components/ListSection';
|
||||
import { LoadMoreButton } from './components/LoadMoreButton';
|
||||
import { NoRulesFound } from './components/NoRulesFound';
|
||||
import { groupFilter as groupFilterFn } from './hooks/filters';
|
||||
import { getGrafanaFilter } from './hooks/filters';
|
||||
import { toIndividualRuleGroups, useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator';
|
||||
import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups';
|
||||
import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits';
|
||||
@@ -57,14 +57,24 @@ function PaginatedGroupsLoader({ groupFilter, namespaceFilter }: LoaderProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filterFn = useMemo(
|
||||
() => (group: PromRuleGroupDTO) =>
|
||||
groupFilterFn(group, {
|
||||
namespace: namespaceFilter,
|
||||
groupName: groupFilter,
|
||||
}),
|
||||
[namespaceFilter, groupFilter]
|
||||
);
|
||||
const filterFn = useMemo(() => {
|
||||
const { frontendFilter } = getGrafanaFilter({
|
||||
namespace: namespaceFilter,
|
||||
groupName: groupFilter,
|
||||
freeFormWords: [],
|
||||
ruleName: '',
|
||||
labels: [],
|
||||
ruleType: undefined,
|
||||
ruleState: undefined,
|
||||
ruleHealth: undefined,
|
||||
dashboardUid: undefined,
|
||||
dataSourceNames: [],
|
||||
plugins: undefined,
|
||||
contactPoint: undefined,
|
||||
ruleSource: undefined,
|
||||
});
|
||||
return (group: PromRuleGroupDTO) => frontendFilter.groupMatches(group);
|
||||
}, [namespaceFilter, groupFilter]);
|
||||
|
||||
const { isLoading, groups, hasMoreGroups, fetchMoreGroups, error } = useLazyLoadPrometheusGroups(
|
||||
groupsGenerator.current,
|
||||
|
||||
@@ -1,302 +1,744 @@
|
||||
import { testWithFeatureToggles } from 'test/test-utils';
|
||||
|
||||
import { PromAlertingRuleState, PromRuleGroupDTO, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { mockGrafanaPromAlertingRule, mockPromAlertingRule, mockPromRecordingRule } from '../../mocks';
|
||||
import { RuleHealth } from '../../search/rulesSearchParser';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import * as datasourceUtils from '../../utils/datasource';
|
||||
import { getDatasourceAPIUid } from '../../utils/datasource';
|
||||
import { getFilter } from '../../utils/search';
|
||||
|
||||
import { groupFilter, ruleFilter } from './filters';
|
||||
import { getDatasourceFilter, getGrafanaFilter } from './filters';
|
||||
|
||||
describe('groupFilter', () => {
|
||||
it('should filter by namespace (file path)', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
jest.mock('../../utils/datasource');
|
||||
|
||||
expect(groupFilter(group, getFilter({ namespace: 'production' }))).toBe(true);
|
||||
expect(groupFilter(group, getFilter({ namespace: 'staging' }))).toBe(false);
|
||||
const getDatasourceAPIUidMock = jest.mocked(getDatasourceAPIUid);
|
||||
|
||||
getDatasourceAPIUidMock.mockImplementation((ruleSourceName) => {
|
||||
if (ruleSourceName === 'prometheus') {
|
||||
return 'datasource-uid-1';
|
||||
}
|
||||
if (ruleSourceName === 'loki') {
|
||||
return 'datasource-uid-3';
|
||||
}
|
||||
throw new Error(`Unknown datasource name: ${ruleSourceName}`);
|
||||
});
|
||||
|
||||
describe('datasource-managed rules', () => {
|
||||
describe('groupFilter', () => {
|
||||
it('should filter by namespace (file path)', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
|
||||
const { groupMatches } = getDatasourceFilter(getFilter({ namespace: 'production' }));
|
||||
expect(groupMatches(group)).toBe(true);
|
||||
|
||||
const { groupMatches: groupMatches2 } = getDatasourceFilter(getFilter({ namespace: 'staging' }));
|
||||
expect(groupMatches2(group)).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter by group name', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'CPU Usage Alerts',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
|
||||
const { groupMatches } = getDatasourceFilter(getFilter({ groupName: 'cpu' }));
|
||||
expect(groupMatches(group)).toBe(true);
|
||||
|
||||
const { groupMatches: groupMatches2 } = getDatasourceFilter(getFilter({ groupName: 'memory' }));
|
||||
expect(groupMatches2(group)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when no filters are applied', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
|
||||
const { groupMatches } = getDatasourceFilter(getFilter({}));
|
||||
expect(groupMatches(group)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by group name', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'CPU Usage Alerts',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
describe('ruleFilter', () => {
|
||||
it('should filter by free form words in rule name', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
expect(groupFilter(group, getFilter({ groupName: 'cpu' }))).toBe(true);
|
||||
expect(groupFilter(group, getFilter({ groupName: 'memory' }))).toBe(false);
|
||||
});
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ freeFormWords: ['cpu'] }));
|
||||
expect(ruleMatches(rule)).toBe(true);
|
||||
|
||||
it('should return true when no filters are applied', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(getFilter({ freeFormWords: ['memory'] }));
|
||||
expect(ruleMatches2(rule)).toBe(false);
|
||||
});
|
||||
|
||||
expect(groupFilter(group, getFilter({}))).toBe(true);
|
||||
it('should filter by rule name', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ ruleName: 'cpu' }));
|
||||
expect(ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(getFilter({ ruleName: 'memory' }));
|
||||
expect(ruleMatches2(rule)).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter by labels', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { severity: 'critical', team: 'ops' },
|
||||
alerts: [],
|
||||
});
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ labels: ['severity=critical'] }));
|
||||
expect(ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(getFilter({ labels: ['severity=warning'] }));
|
||||
expect(ruleMatches2(rule)).toBe(false);
|
||||
|
||||
const { ruleMatches: ruleMatches3 } = getDatasourceFilter(getFilter({ labels: ['team=ops'] }));
|
||||
expect(ruleMatches3(rule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by alert instance labels', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { severity: 'critical' },
|
||||
alerts: [
|
||||
{
|
||||
labels: { instance: 'server-1', env: 'production' },
|
||||
state: PromAlertingRuleState.Firing,
|
||||
value: '100',
|
||||
activeAt: '',
|
||||
annotations: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ labels: ['instance=server-1'] }));
|
||||
expect(ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(getFilter({ labels: ['env=production'] }));
|
||||
expect(ruleMatches2(rule)).toBe(true);
|
||||
|
||||
const { ruleMatches: ruleMatches3 } = getDatasourceFilter(getFilter({ labels: ['instance=server-2'] }));
|
||||
expect(ruleMatches3(rule)).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter by rule type', () => {
|
||||
const alertingRule = mockPromAlertingRule({ name: 'Test Alert' });
|
||||
const recordingRule = mockPromRecordingRule({ name: 'Test Recording' });
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ ruleType: PromRuleType.Alerting }));
|
||||
expect(ruleMatches(alertingRule)).toBe(true);
|
||||
expect(ruleMatches(recordingRule)).toBe(false);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(getFilter({ ruleType: PromRuleType.Recording }));
|
||||
expect(ruleMatches2(alertingRule)).toBe(false);
|
||||
expect(ruleMatches2(recordingRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by rule state', () => {
|
||||
const firingRule = mockPromAlertingRule({
|
||||
name: 'Firing Alert',
|
||||
state: PromAlertingRuleState.Firing,
|
||||
});
|
||||
|
||||
const pendingRule = mockPromAlertingRule({
|
||||
name: 'Pending Alert',
|
||||
state: PromAlertingRuleState.Pending,
|
||||
});
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ ruleState: PromAlertingRuleState.Firing }));
|
||||
expect(ruleMatches(firingRule)).toBe(true);
|
||||
expect(ruleMatches(pendingRule)).toBe(false);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(
|
||||
getFilter({ ruleState: PromAlertingRuleState.Pending })
|
||||
);
|
||||
expect(ruleMatches2(firingRule)).toBe(false);
|
||||
expect(ruleMatches2(pendingRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter out recording rules when filtering by rule state', () => {
|
||||
const recordingRule = mockPromRecordingRule({
|
||||
name: 'Recording Rule',
|
||||
});
|
||||
|
||||
// Recording rules should always be filtered out when any rule state filter is applied as they don't have a state
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ ruleState: PromAlertingRuleState.Firing }));
|
||||
expect(ruleMatches(recordingRule)).toBe(false);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(
|
||||
getFilter({ ruleState: PromAlertingRuleState.Pending })
|
||||
);
|
||||
expect(ruleMatches2(recordingRule)).toBe(false);
|
||||
|
||||
const { ruleMatches: ruleMatches3 } = getDatasourceFilter(
|
||||
getFilter({ ruleState: PromAlertingRuleState.Inactive })
|
||||
);
|
||||
expect(ruleMatches3(recordingRule)).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter by rule health', () => {
|
||||
const healthyRule = mockPromAlertingRule({
|
||||
name: 'Healthy Rule',
|
||||
health: RuleHealth.Ok,
|
||||
});
|
||||
|
||||
const errorRule = mockPromAlertingRule({
|
||||
name: 'Error Rule',
|
||||
health: RuleHealth.Error,
|
||||
});
|
||||
|
||||
const prometheusErrorRule = mockPromAlertingRule({
|
||||
name: 'Error Rule',
|
||||
health: 'err',
|
||||
});
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ ruleHealth: RuleHealth.Ok }));
|
||||
expect(ruleMatches(healthyRule)).toBe(true);
|
||||
expect(ruleMatches(errorRule)).toBe(false);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(getFilter({ ruleHealth: RuleHealth.Error }));
|
||||
expect(ruleMatches2(healthyRule)).toBe(false);
|
||||
expect(ruleMatches2(errorRule)).toBe(true);
|
||||
expect(ruleMatches2(prometheusErrorRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should normalize health values when filtering', () => {
|
||||
// Legacy Prometheus health value 'err' should be normalized to 'error'
|
||||
const legacyErrorRule = mockPromAlertingRule({
|
||||
name: 'Legacy Error Rule',
|
||||
health: 'err',
|
||||
});
|
||||
|
||||
// When filtering for 'error', it should match rules with health 'err' (legacy) or 'error'
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ ruleHealth: RuleHealth.Error }));
|
||||
expect(ruleMatches(legacyErrorRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by dashboard UID', () => {
|
||||
const ruleDashboardA = mockPromAlertingRule({
|
||||
name: 'Dashboard A Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-a' },
|
||||
});
|
||||
|
||||
const ruleDashboardB = mockPromAlertingRule({
|
||||
name: 'Dashboard B Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-b' },
|
||||
});
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ dashboardUid: 'dashboard-a' }));
|
||||
expect(ruleMatches(ruleDashboardA)).toBe(true);
|
||||
expect(ruleMatches(ruleDashboardB)).toBe(false);
|
||||
|
||||
const { ruleMatches: ruleMatches2 } = getDatasourceFilter(getFilter({ dashboardUid: 'dashboard-b' }));
|
||||
expect(ruleMatches2(ruleDashboardA)).toBe(false);
|
||||
expect(ruleMatches2(ruleDashboardB)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter out recording rules when filtering by dashboard UID', () => {
|
||||
const recordingRule = mockPromRecordingRule({
|
||||
name: 'Recording Rule',
|
||||
// Recording rules cannot have dashboard UIDs because they don't have annotations
|
||||
});
|
||||
|
||||
// Dashboard UID filter should filter out recording rules
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ dashboardUid: 'any-dashboard' }));
|
||||
expect(ruleMatches(recordingRule)).toBe(false);
|
||||
});
|
||||
|
||||
describe('dataSourceNames filter', () => {
|
||||
it('should match rules that use the filtered datasource', () => {
|
||||
// Create a Grafana rule with matching datasource
|
||||
const ruleWithMatchingDatasource = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1'],
|
||||
});
|
||||
|
||||
// 'prometheus' resolves to 'datasource-uid-1' which is in the rule
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ dataSourceNames: ['prometheus'] }));
|
||||
expect(ruleMatches(ruleWithMatchingDatasource)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out rules that don't use the filtered datasource", () => {
|
||||
// Create a Grafana rule without the target datasource
|
||||
const ruleWithoutMatchingDatasource = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1', 'datasource-uid-2'],
|
||||
});
|
||||
|
||||
// 'loki' resolves to 'datasource-uid-3' which is not in the rule
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ dataSourceNames: ['loki'] }));
|
||||
expect(ruleMatches(ruleWithoutMatchingDatasource)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when there is an error parsing the query', () => {
|
||||
const ruleWithInvalidQuery = mockGrafanaPromAlertingRule({
|
||||
query: 'not-valid-json',
|
||||
});
|
||||
|
||||
const { ruleMatches } = getDatasourceFilter(getFilter({ dataSourceNames: ['prometheus'] }));
|
||||
expect(ruleMatches(ruleWithInvalidQuery)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should combine multiple filters with AND logic', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
name: 'High CPU Usage Production',
|
||||
labels: { severity: 'critical', environment: 'production' },
|
||||
state: PromAlertingRuleState.Firing,
|
||||
health: RuleHealth.Ok,
|
||||
});
|
||||
|
||||
const filter = getFilter({
|
||||
ruleName: 'cpu',
|
||||
labels: ['severity=critical', 'environment=production'],
|
||||
ruleState: PromAlertingRuleState.Firing,
|
||||
ruleHealth: RuleHealth.Ok,
|
||||
});
|
||||
const { ruleMatches } = getDatasourceFilter(filter);
|
||||
expect(ruleMatches(rule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if any filter does not match', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
name: 'High CPU Usage Production',
|
||||
labels: { severity: 'critical', environment: 'production' },
|
||||
state: PromAlertingRuleState.Firing,
|
||||
health: RuleHealth.Ok,
|
||||
alerts: [],
|
||||
});
|
||||
|
||||
const filter = getFilter({
|
||||
ruleName: 'cpu',
|
||||
labels: ['severity=warning'],
|
||||
ruleState: PromAlertingRuleState.Firing,
|
||||
ruleHealth: RuleHealth.Ok,
|
||||
});
|
||||
const { ruleMatches } = getDatasourceFilter(filter);
|
||||
expect(ruleMatches(rule)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ruleFilter', () => {
|
||||
it('should filter by free form words in rule name', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
describe('grafana-managed rules', () => {
|
||||
describe('groupFilter', () => {
|
||||
it('should filter by namespace (file path)', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
|
||||
expect(ruleFilter(rule, getFilter({ freeFormWords: ['cpu'] }))).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ freeFormWords: ['memory'] }))).toBe(false);
|
||||
});
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ namespace: 'production' }));
|
||||
expect(frontendFilter.groupMatches(group)).toBe(true);
|
||||
|
||||
it('should filter by rule name', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
expect(ruleFilter(rule, getFilter({ ruleName: 'cpu' }))).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ ruleName: 'memory' }))).toBe(false);
|
||||
});
|
||||
|
||||
describe('backendFiltered parameter for backend filtering', () => {
|
||||
it('should skip title filtering when backendFiltered is true', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
// When backendFiltered is true, title search should be skipped (already filtered by backend)
|
||||
expect(ruleFilter(rule, getFilter({ freeFormWords: ['memory'] }), true)).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ ruleName: 'memory' }), true)).toBe(true);
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ namespace: 'staging' }));
|
||||
expect(frontendFilter2.groupMatches(group)).toBe(false);
|
||||
});
|
||||
|
||||
it('should perform title filtering when backendFiltered is false', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
it('should filter by group name', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'CPU Usage Alerts',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
|
||||
// When backendFiltered is false, title search should be performed client-side
|
||||
expect(ruleFilter(rule, getFilter({ freeFormWords: ['cpu'] }), false)).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ freeFormWords: ['memory'] }), false)).toBe(false);
|
||||
expect(ruleFilter(rule, getFilter({ ruleName: 'cpu' }), false)).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ ruleName: 'memory' }), false)).toBe(false);
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ groupName: 'cpu' }));
|
||||
expect(frontendFilter.groupMatches(group)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ groupName: 'memory' }));
|
||||
expect(frontendFilter2.groupMatches(group)).toBe(false);
|
||||
});
|
||||
|
||||
it('should perform title filtering when backendFiltered is not specified (backward compatibility)', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
it('should return true when no filters are applied', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
|
||||
// When backendFiltered is not provided, should perform client-side filtering (default behavior)
|
||||
expect(ruleFilter(rule, getFilter({ freeFormWords: ['cpu'] }))).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ freeFormWords: ['memory'] }))).toBe(false);
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({}));
|
||||
expect(frontendFilter.groupMatches(group)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by labels', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { severity: 'critical', team: 'ops' },
|
||||
alerts: [],
|
||||
describe('ruleFilter - frontend filters', () => {
|
||||
it('should filter by free form words in rule name', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ freeFormWords: ['cpu'] }));
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ freeFormWords: ['memory'] }));
|
||||
expect(frontendFilter2.ruleMatches(rule)).toBe(false);
|
||||
});
|
||||
|
||||
expect(ruleFilter(rule, getFilter({ labels: ['severity=critical'] }))).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ labels: ['severity=warning'] }))).toBe(false);
|
||||
expect(ruleFilter(rule, getFilter({ labels: ['team=ops'] }))).toBe(true);
|
||||
it('should filter by rule name', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleName: 'cpu' }));
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ ruleName: 'memory' }));
|
||||
expect(frontendFilter2.ruleMatches(rule)).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter by labels', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({
|
||||
labels: { severity: 'critical', team: 'ops' },
|
||||
alerts: [],
|
||||
});
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ labels: ['severity=critical'] }));
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ labels: ['severity=warning'] }));
|
||||
expect(frontendFilter2.ruleMatches(rule)).toBe(false);
|
||||
|
||||
const { frontendFilter: frontendFilter3 } = getGrafanaFilter(getFilter({ labels: ['team=ops'] }));
|
||||
expect(frontendFilter3.ruleMatches(rule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by rule type', () => {
|
||||
const alertingRule = mockGrafanaPromAlertingRule({ name: 'Test Alert' });
|
||||
const recordingRule = mockPromRecordingRule({ name: 'Test Recording' });
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleType: PromRuleType.Alerting }));
|
||||
expect(frontendFilter.ruleMatches(alertingRule)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(recordingRule)).toBe(false);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ ruleType: PromRuleType.Recording }));
|
||||
expect(frontendFilter2.ruleMatches(alertingRule)).toBe(false);
|
||||
expect(frontendFilter2.ruleMatches(recordingRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by dashboard UID', () => {
|
||||
const ruleDashboardA = mockGrafanaPromAlertingRule({
|
||||
name: 'Dashboard A Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-a' },
|
||||
});
|
||||
|
||||
const ruleDashboardB = mockGrafanaPromAlertingRule({
|
||||
name: 'Dashboard B Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-b' },
|
||||
});
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ dashboardUid: 'dashboard-a' }));
|
||||
expect(frontendFilter.ruleMatches(ruleDashboardA)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(ruleDashboardB)).toBe(false);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ dashboardUid: 'dashboard-b' }));
|
||||
expect(frontendFilter2.ruleMatches(ruleDashboardA)).toBe(false);
|
||||
expect(frontendFilter2.ruleMatches(ruleDashboardB)).toBe(true);
|
||||
});
|
||||
|
||||
describe('dataSourceNames filter', () => {
|
||||
it('should match rules that use the filtered datasource', () => {
|
||||
const ruleWithMatchingDatasource = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1'],
|
||||
});
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ dataSourceNames: ['prometheus'] }));
|
||||
expect(frontendFilter.ruleMatches(ruleWithMatchingDatasource)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out rules that don't use the filtered datasource", () => {
|
||||
const ruleWithoutMatchingDatasource = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1', 'datasource-uid-2'],
|
||||
});
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ dataSourceNames: ['loki'] }));
|
||||
expect(frontendFilter.ruleMatches(ruleWithoutMatchingDatasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by alert instance labels', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { severity: 'critical' },
|
||||
alerts: [
|
||||
{
|
||||
labels: { instance: 'server-1', env: 'production' },
|
||||
state: PromAlertingRuleState.Firing,
|
||||
value: '100',
|
||||
activeAt: '',
|
||||
annotations: {},
|
||||
describe('ruleFilter - backend filters (should NOT be applied in frontend)', () => {
|
||||
it('should NOT filter by rule state in frontend (returns true regardless)', () => {
|
||||
const firingRule = mockGrafanaPromAlertingRule({
|
||||
name: 'Firing Alert',
|
||||
state: PromAlertingRuleState.Firing,
|
||||
});
|
||||
|
||||
const pendingRule = mockGrafanaPromAlertingRule({
|
||||
name: 'Pending Alert',
|
||||
state: PromAlertingRuleState.Pending,
|
||||
});
|
||||
|
||||
// Frontend filter should return true for all states since backend handles this
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleState: PromAlertingRuleState.Firing }));
|
||||
expect(frontendFilter.ruleMatches(firingRule)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(pendingRule)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(
|
||||
getFilter({ ruleState: PromAlertingRuleState.Pending })
|
||||
);
|
||||
expect(frontendFilter2.ruleMatches(firingRule)).toBe(true);
|
||||
expect(frontendFilter2.ruleMatches(pendingRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT filter by rule health in frontend (returns true regardless)', () => {
|
||||
const healthyRule = mockGrafanaPromAlertingRule({
|
||||
name: 'Healthy Rule',
|
||||
health: RuleHealth.Ok,
|
||||
});
|
||||
|
||||
const errorRule = mockGrafanaPromAlertingRule({
|
||||
name: 'Error Rule',
|
||||
health: RuleHealth.Error,
|
||||
});
|
||||
|
||||
// Frontend filter should return true for all health states since backend handles this
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleHealth: RuleHealth.Ok }));
|
||||
expect(frontendFilter.ruleMatches(healthyRule)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(errorRule)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ ruleHealth: RuleHealth.Error }));
|
||||
expect(frontendFilter2.ruleMatches(healthyRule)).toBe(true);
|
||||
expect(frontendFilter2.ruleMatches(errorRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT filter by contact point in frontend (returns true regardless)', () => {
|
||||
const ruleWithContactPoint = mockGrafanaPromAlertingRule({
|
||||
name: 'Rule with Contact Point',
|
||||
notificationSettings: {
|
||||
receiver: 'contact-point-1',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
expect(ruleFilter(rule, getFilter({ labels: ['instance=server-1'] }))).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ labels: ['env=production'] }))).toBe(true);
|
||||
expect(ruleFilter(rule, getFilter({ labels: ['instance=server-2'] }))).toBe(false);
|
||||
const ruleWithDifferentContactPoint = mockGrafanaPromAlertingRule({
|
||||
name: 'Rule with Different Contact Point',
|
||||
notificationSettings: {
|
||||
receiver: 'contact-point-2',
|
||||
},
|
||||
});
|
||||
|
||||
// Frontend filter should return true for all contact points since backend handles this
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ contactPoint: 'contact-point-1' }));
|
||||
expect(frontendFilter.ruleMatches(ruleWithContactPoint)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(ruleWithDifferentContactPoint)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ contactPoint: 'contact-point-2' }));
|
||||
expect(frontendFilter2.ruleMatches(ruleWithContactPoint)).toBe(true);
|
||||
expect(frontendFilter2.ruleMatches(ruleWithDifferentContactPoint)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by rule type', () => {
|
||||
const alertingRule = mockPromAlertingRule({ name: 'Test Alert' });
|
||||
const recordingRule = mockPromRecordingRule({ name: 'Test Recording' });
|
||||
describe('backendFilter', () => {
|
||||
it('should include ruleState in backend filter', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ ruleState: PromAlertingRuleState.Firing }));
|
||||
|
||||
expect(ruleFilter(alertingRule, getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true);
|
||||
expect(ruleFilter(alertingRule, getFilter({ ruleType: PromRuleType.Recording }))).toBe(false);
|
||||
expect(ruleFilter(recordingRule, getFilter({ ruleType: PromRuleType.Recording }))).toBe(true);
|
||||
expect(ruleFilter(recordingRule, getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false);
|
||||
expect(backendFilter.state).toEqual([PromAlertingRuleState.Firing]);
|
||||
});
|
||||
|
||||
it('should include ruleHealth in backend filter', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ ruleHealth: RuleHealth.Error }));
|
||||
|
||||
expect(backendFilter.health).toEqual([RuleHealth.Error]);
|
||||
});
|
||||
|
||||
it('should include contactPoint in backend filter', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ contactPoint: 'my-contact-point' }));
|
||||
|
||||
expect(backendFilter.contactPoint).toBe('my-contact-point');
|
||||
});
|
||||
|
||||
it('should handle empty backend filters', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({}));
|
||||
|
||||
expect(backendFilter.state).toEqual([]);
|
||||
expect(backendFilter.health).toEqual([]);
|
||||
expect(backendFilter.contactPoint).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by rule state', () => {
|
||||
const firingRule = mockPromAlertingRule({
|
||||
name: 'Firing Alert',
|
||||
state: PromAlertingRuleState.Firing,
|
||||
});
|
||||
describe('backend filtering with alertingUIUseBackendFilters feature toggle', () => {
|
||||
describe('when alertingUIUseBackendFilters is enabled', () => {
|
||||
testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters'] });
|
||||
|
||||
const pendingRule = mockPromAlertingRule({
|
||||
name: 'Pending Alert',
|
||||
state: PromAlertingRuleState.Pending,
|
||||
});
|
||||
it('should include title in backend filter when freeFormWords are provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ freeFormWords: ['cpu', 'usage'] }));
|
||||
|
||||
expect(ruleFilter(firingRule, getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(true);
|
||||
expect(ruleFilter(firingRule, getFilter({ ruleState: PromAlertingRuleState.Pending }))).toBe(false);
|
||||
expect(ruleFilter(pendingRule, getFilter({ ruleState: PromAlertingRuleState.Pending }))).toBe(true);
|
||||
});
|
||||
expect(backendFilter.title).toBe('cpu usage');
|
||||
});
|
||||
|
||||
it('should filter out recording rules when filtering by rule state', () => {
|
||||
const recordingRule = mockPromRecordingRule({
|
||||
name: 'Recording Rule',
|
||||
});
|
||||
it('should include title in backend filter when ruleName is provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ ruleName: 'high cpu' }));
|
||||
|
||||
// Recording rules should always be filtered out when any rule state filter is applied as they don't have a state
|
||||
expect(ruleFilter(recordingRule, getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false);
|
||||
expect(ruleFilter(recordingRule, getFilter({ ruleState: PromAlertingRuleState.Pending }))).toBe(false);
|
||||
expect(ruleFilter(recordingRule, getFilter({ ruleState: PromAlertingRuleState.Inactive }))).toBe(false);
|
||||
});
|
||||
expect(backendFilter.title).toBe('high cpu');
|
||||
});
|
||||
|
||||
it('should filter by rule health', () => {
|
||||
const healthyRule = mockPromAlertingRule({
|
||||
name: 'Healthy Rule',
|
||||
health: RuleHealth.Ok,
|
||||
});
|
||||
it('should combine ruleName and freeFormWords in title', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ ruleName: 'alert', freeFormWords: ['cpu'] }));
|
||||
|
||||
const errorRule = mockPromAlertingRule({
|
||||
name: 'Error Rule',
|
||||
health: RuleHealth.Error,
|
||||
});
|
||||
expect(backendFilter.title).toBe('alert cpu');
|
||||
});
|
||||
|
||||
const prometheusErrorRule = mockPromAlertingRule({
|
||||
name: 'Error Rule',
|
||||
health: 'err',
|
||||
});
|
||||
it('should not include title when no title filters are provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ ruleState: PromAlertingRuleState.Firing }));
|
||||
|
||||
expect(ruleFilter(healthyRule, getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(true);
|
||||
expect(ruleFilter(healthyRule, getFilter({ ruleHealth: RuleHealth.Error }))).toBe(false);
|
||||
expect(ruleFilter(errorRule, getFilter({ ruleHealth: RuleHealth.Error }))).toBe(true);
|
||||
expect(ruleFilter(prometheusErrorRule, getFilter({ ruleHealth: RuleHealth.Error }))).toBe(true);
|
||||
});
|
||||
expect(backendFilter.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should normalize health values when filtering', () => {
|
||||
// Legacy Prometheus health value 'err' should be normalized to 'error'
|
||||
const legacyErrorRule = mockPromAlertingRule({
|
||||
name: 'Legacy Error Rule',
|
||||
health: 'err',
|
||||
});
|
||||
it('should skip freeFormWords filtering on frontend when backend filtering is enabled', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
// When filtering for 'error', it should match rules with health 'err' (legacy) or 'error'
|
||||
expect(ruleFilter(legacyErrorRule, getFilter({ ruleHealth: RuleHealth.Error }))).toBe(true);
|
||||
});
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ freeFormWords: ['memory'] }));
|
||||
// Should return true because freeFormWords filter is null (handled by backend)
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by dashboard UID', () => {
|
||||
const ruleDashboardA = mockPromAlertingRule({
|
||||
name: 'Dashboard A Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-a' },
|
||||
});
|
||||
it('should skip ruleName filtering on frontend when backend filtering is enabled', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
const ruleDashboardB = mockPromAlertingRule({
|
||||
name: 'Dashboard B Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-b' },
|
||||
});
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleName: 'memory' }));
|
||||
// Should return true because ruleName filter is null (handled by backend)
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
});
|
||||
|
||||
expect(ruleFilter(ruleDashboardA, getFilter({ dashboardUid: 'dashboard-a' }))).toBe(true);
|
||||
expect(ruleFilter(ruleDashboardA, getFilter({ dashboardUid: 'dashboard-b' }))).toBe(false);
|
||||
expect(ruleFilter(ruleDashboardB, getFilter({ dashboardUid: 'dashboard-b' }))).toBe(true);
|
||||
});
|
||||
it('should include ruleType in backend filter when provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ ruleType: PromRuleType.Alerting }));
|
||||
|
||||
it('should filter out recording rules when filtering by dashboard UID', () => {
|
||||
const recordingRule = mockPromRecordingRule({
|
||||
name: 'Recording Rule',
|
||||
// Recording rules cannot have dashboard UIDs because they don't have annotations
|
||||
});
|
||||
expect(backendFilter.type).toBe(PromRuleType.Alerting);
|
||||
});
|
||||
|
||||
// Dashboard UID filter should filter out recording rules
|
||||
expect(ruleFilter(recordingRule, getFilter({ dashboardUid: 'any-dashboard' }))).toBe(false);
|
||||
});
|
||||
it('should not include ruleType in backend filter when not provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({}));
|
||||
|
||||
describe('dataSourceNames filter', () => {
|
||||
let getDataSourceUIDSpy: jest.SpyInstance;
|
||||
expect(backendFilter.type).toBeUndefined();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getDataSourceUIDSpy = jest.spyOn(datasourceUtils, 'getDatasourceAPIUid').mockImplementation((ruleSourceName) => {
|
||||
if (ruleSourceName === 'prometheus') {
|
||||
return 'datasource-uid-1';
|
||||
}
|
||||
if (ruleSourceName === 'loki') {
|
||||
return 'datasource-uid-3';
|
||||
}
|
||||
throw new Error(`Unknown datasource name: ${ruleSourceName}`);
|
||||
it('should skip ruleType filtering on frontend when backend filtering is enabled', () => {
|
||||
const alertingRule = mockGrafanaPromAlertingRule({ name: 'Test Alert' });
|
||||
const recordingRule = mockPromRecordingRule({ name: 'Test Recording' });
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleType: PromRuleType.Alerting }));
|
||||
// Should return true for both because ruleType filter is null (handled by backend)
|
||||
expect(frontendFilter.ruleMatches(alertingRule)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(recordingRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should include dashboardUid in backend filter when provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ dashboardUid: 'dashboard-123' }));
|
||||
|
||||
expect(backendFilter.dashboardUid).toBe('dashboard-123');
|
||||
});
|
||||
|
||||
it('should not include dashboardUid in backend filter when not provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({}));
|
||||
|
||||
expect(backendFilter.dashboardUid).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip dashboardUid filtering on frontend when backend filtering is enabled', () => {
|
||||
const ruleWithDashboard = mockGrafanaPromAlertingRule({
|
||||
name: 'Dashboard Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-a' },
|
||||
});
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ dashboardUid: 'dashboard-b' }));
|
||||
// Should return true because dashboardUid filter is null (handled by backend)
|
||||
expect(frontendFilter.ruleMatches(ruleWithDashboard)).toBe(true);
|
||||
});
|
||||
|
||||
it('should still apply other frontend filters', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({
|
||||
name: 'High CPU Usage',
|
||||
labels: { severity: 'critical', team: 'ops' },
|
||||
alerts: [],
|
||||
});
|
||||
|
||||
// Label filter should still work on frontend
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ labels: ['severity=warning'] }));
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(false);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ labels: ['severity=critical'] }));
|
||||
expect(frontendFilter2.ruleMatches(rule)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up
|
||||
getDataSourceUIDSpy.mockRestore();
|
||||
});
|
||||
describe('when alertingUIUseBackendFilters is disabled', () => {
|
||||
testWithFeatureToggles({ disable: ['alertingUIUseBackendFilters'] });
|
||||
|
||||
it('should match rules that use the filtered datasource', () => {
|
||||
// Create a Grafana rule with matching datasource
|
||||
const ruleWithMatchingDatasource = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1'],
|
||||
it('should not include title in backend filter', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ freeFormWords: ['cpu'] }));
|
||||
|
||||
expect(backendFilter.title).toBeUndefined();
|
||||
});
|
||||
|
||||
// 'prometheus' resolves to 'datasource-uid-1' which is in the rule
|
||||
expect(ruleFilter(ruleWithMatchingDatasource, getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
|
||||
});
|
||||
it('should perform freeFormWords filtering on frontend', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
it("should filter out rules that don't use the filtered datasource", () => {
|
||||
// Create a Grafana rule without the target datasource
|
||||
const ruleWithoutMatchingDatasource = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1', 'datasource-uid-2'],
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ freeFormWords: ['cpu'] }));
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ freeFormWords: ['memory'] }));
|
||||
expect(frontendFilter2.ruleMatches(rule)).toBe(false);
|
||||
});
|
||||
|
||||
// 'loki' resolves to 'datasource-uid-3' which is not in the rule
|
||||
expect(ruleFilter(ruleWithoutMatchingDatasource, getFilter({ dataSourceNames: ['loki'] }))).toBe(false);
|
||||
});
|
||||
it('should perform ruleName filtering on frontend', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
it('should return false when there is an error parsing the query', () => {
|
||||
const ruleWithInvalidQuery = mockGrafanaPromAlertingRule({
|
||||
query: 'not-valid-json',
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleName: 'cpu' }));
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ ruleName: 'memory' }));
|
||||
expect(frontendFilter2.ruleMatches(rule)).toBe(false);
|
||||
});
|
||||
|
||||
expect(ruleFilter(ruleWithInvalidQuery, getFilter({ dataSourceNames: ['prometheus'] }))).toBe(false);
|
||||
it('should not include ruleType in backend filter', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ ruleType: PromRuleType.Alerting }));
|
||||
|
||||
expect(backendFilter.type).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should perform ruleType filtering on frontend', () => {
|
||||
const alertingRule = mockGrafanaPromAlertingRule({ name: 'Test Alert' });
|
||||
const recordingRule = mockPromRecordingRule({ name: 'Test Recording' });
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ ruleType: PromRuleType.Alerting }));
|
||||
expect(frontendFilter.ruleMatches(alertingRule)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(recordingRule)).toBe(false);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ ruleType: PromRuleType.Recording }));
|
||||
expect(frontendFilter2.ruleMatches(alertingRule)).toBe(false);
|
||||
expect(frontendFilter2.ruleMatches(recordingRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not include dashboardUid in backend filter', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ dashboardUid: 'dashboard-123' }));
|
||||
|
||||
expect(backendFilter.dashboardUid).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should perform dashboardUid filtering on frontend', () => {
|
||||
const ruleDashboardA = mockGrafanaPromAlertingRule({
|
||||
name: 'Dashboard A Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-a' },
|
||||
});
|
||||
|
||||
const ruleDashboardB = mockGrafanaPromAlertingRule({
|
||||
name: 'Dashboard B Rule',
|
||||
annotations: { [Annotation.dashboardUID]: 'dashboard-b' },
|
||||
});
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ dashboardUid: 'dashboard-a' }));
|
||||
expect(frontendFilter.ruleMatches(ruleDashboardA)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(ruleDashboardB)).toBe(false);
|
||||
|
||||
const { frontendFilter: frontendFilter2 } = getGrafanaFilter(getFilter({ dashboardUid: 'dashboard-b' }));
|
||||
expect(frontendFilter2.ruleMatches(ruleDashboardA)).toBe(false);
|
||||
expect(frontendFilter2.ruleMatches(ruleDashboardB)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should combine multiple filters with AND logic', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
name: 'High CPU Usage Production',
|
||||
labels: { severity: 'critical', environment: 'production' },
|
||||
state: PromAlertingRuleState.Firing,
|
||||
health: RuleHealth.Ok,
|
||||
});
|
||||
|
||||
const filter = getFilter({
|
||||
ruleName: 'cpu',
|
||||
labels: ['severity=critical', 'environment=production'],
|
||||
ruleState: PromAlertingRuleState.Firing,
|
||||
ruleHealth: RuleHealth.Ok,
|
||||
});
|
||||
|
||||
expect(ruleFilter(rule, filter)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if any filter does not match', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
name: 'High CPU Usage Production',
|
||||
labels: { severity: 'critical', environment: 'production' },
|
||||
state: PromAlertingRuleState.Firing,
|
||||
health: RuleHealth.Ok,
|
||||
alerts: [],
|
||||
});
|
||||
|
||||
const filter = getFilter({
|
||||
ruleName: 'cpu',
|
||||
labels: ['severity=warning'],
|
||||
ruleState: PromAlertingRuleState.Firing,
|
||||
ruleHealth: RuleHealth.Ok,
|
||||
});
|
||||
|
||||
expect(ruleFilter(rule, filter)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import memoize from 'micro-memoize';
|
||||
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { GrafanaPromRulesOptions } from '../../api/prometheusApi';
|
||||
import { shouldUseBackendFilters } from '../../featureToggles';
|
||||
import { RulesFilter } from '../../search/rulesSearchParser';
|
||||
import { labelsMatchMatchers } from '../../utils/alertmanager';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
@@ -13,21 +15,119 @@ import { parseMatcher } from '../../utils/matchers';
|
||||
import { isPluginProvidedRule, prometheusRuleType } from '../../utils/rules';
|
||||
import { normalizeHealth } from '../components/util';
|
||||
|
||||
type RuleFilterHandler = (rule: PromRuleDTO, filterState: RulesFilter) => boolean;
|
||||
type GroupFilterHandler = (
|
||||
group: PromRuleGroupDTO,
|
||||
filterState: Pick<RulesFilter, 'namespace' | 'groupName'>
|
||||
) => boolean;
|
||||
|
||||
type RuleFilterConfig = Record<
|
||||
Exclude<keyof RulesFilter, 'namespace' | 'groupName' | 'ruleSource'>,
|
||||
RuleFilterHandler | null
|
||||
>;
|
||||
|
||||
type GroupFilterConfig = Record<keyof Pick<RulesFilter, 'namespace' | 'groupName'>, GroupFilterHandler | null>;
|
||||
|
||||
export function getGrafanaFilter(filterState: RulesFilter) {
|
||||
const normalizedFilterState = normalizeFilterState(filterState);
|
||||
const useBackendFilters = shouldUseBackendFilters();
|
||||
|
||||
// Build title search for backend filtering
|
||||
const titleSearch = buildTitleSearch(normalizedFilterState);
|
||||
|
||||
const backendFilter: GrafanaPromRulesOptions = {
|
||||
state: normalizedFilterState.ruleState ? [normalizedFilterState.ruleState] : [],
|
||||
health: normalizedFilterState.ruleHealth ? [normalizedFilterState.ruleHealth] : [],
|
||||
contactPoint: normalizedFilterState.contactPoint ?? undefined,
|
||||
title: useBackendFilters ? titleSearch : undefined,
|
||||
type: useBackendFilters ? normalizedFilterState.ruleType : undefined,
|
||||
dashboardUid: useBackendFilters ? normalizedFilterState.dashboardUid : undefined,
|
||||
};
|
||||
|
||||
const grafanaFilterProcessingConfig: RuleFilterConfig = {
|
||||
// When backend filtering is enabled, these filters are handled by the backend
|
||||
freeFormWords: useBackendFilters ? null : freeFormFilter,
|
||||
ruleName: useBackendFilters ? null : ruleNameFilter,
|
||||
ruleState: null,
|
||||
ruleType: useBackendFilters ? null : ruleTypeFilter,
|
||||
dataSourceNames: dataSourceNamesFilter,
|
||||
labels: labelsFilter,
|
||||
ruleHealth: null,
|
||||
dashboardUid: useBackendFilters ? null : dashboardUidFilter,
|
||||
plugins: pluginsFilter,
|
||||
contactPoint: null,
|
||||
};
|
||||
|
||||
const grafanaGroupFilterConfig: GroupFilterConfig = {
|
||||
namespace: namespaceFilter,
|
||||
groupName: groupNameFilter,
|
||||
};
|
||||
|
||||
return {
|
||||
backendFilter,
|
||||
frontendFilter: {
|
||||
groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, grafanaGroupFilterConfig),
|
||||
ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, grafanaFilterProcessingConfig),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getDatasourceFilter(filterState: RulesFilter) {
|
||||
const normalizedFilterState = normalizeFilterState(filterState);
|
||||
|
||||
const dsRuleFilterConfig: RuleFilterConfig = {
|
||||
freeFormWords: freeFormFilter,
|
||||
ruleName: ruleNameFilter,
|
||||
ruleState: ruleStateFilter,
|
||||
ruleType: ruleTypeFilter,
|
||||
dataSourceNames: dataSourceNamesFilter,
|
||||
labels: labelsFilter,
|
||||
ruleHealth: ruleHealthFilter,
|
||||
dashboardUid: dashboardUidFilter,
|
||||
plugins: pluginsFilter,
|
||||
contactPoint: contactPointFilter,
|
||||
};
|
||||
|
||||
const dsGroupFilterConfig: GroupFilterConfig = {
|
||||
namespace: namespaceFilter,
|
||||
groupName: groupNameFilter,
|
||||
};
|
||||
|
||||
return {
|
||||
groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, dsGroupFilterConfig),
|
||||
ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, dsRuleFilterConfig),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns True if the group matches the filter, false otherwise. Keeps rules intact
|
||||
*/
|
||||
export function groupFilter(
|
||||
function groupMatches(
|
||||
group: PromRuleGroupDTO,
|
||||
filterState: Pick<RulesFilter, 'namespace' | 'groupName'>
|
||||
filterState: Pick<RulesFilter, 'namespace' | 'groupName'>,
|
||||
filterConfig: GroupFilterConfig
|
||||
): boolean {
|
||||
const { name, file } = group;
|
||||
const { namespace, groupName } = filterState;
|
||||
|
||||
if (namespace && !fuzzyMatches(file, namespace)) {
|
||||
if (filterConfig.namespace && filterConfig.namespace(group, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (groupName && !fuzzyMatches(name, groupName)) {
|
||||
if (filterConfig.groupName && filterConfig.groupName(group, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function namespaceFilter(group: PromRuleGroupDTO, filterState: Pick<RulesFilter, 'namespace' | 'groupName'>): boolean {
|
||||
if (filterState.namespace && !fuzzyMatches(group.file, filterState.namespace)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function groupNameFilter(group: PromRuleGroupDTO, filterState: Pick<RulesFilter, 'namespace' | 'groupName'>): boolean {
|
||||
if (filterState.groupName && !fuzzyMatches(group.name, filterState.groupName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -36,26 +136,74 @@ export function groupFilter(
|
||||
|
||||
/**
|
||||
* @returns True if the rule matches the filter, false otherwise
|
||||
* @param backendFiltered - If true, title search is skipped (already filtered by backend)
|
||||
*/
|
||||
export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter, backendFiltered?: boolean) {
|
||||
const { name, labels = {}, health, type } = rule;
|
||||
function ruleMatches(rule: PromRuleDTO, filterState: RulesFilter, filterConfig: RuleFilterConfig) {
|
||||
if (filterConfig.freeFormWords && filterConfig.freeFormWords(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterState.freeFormWords.length > 0 && !backendFiltered) {
|
||||
const nameMatches = fuzzyMatches(name, filterState.freeFormWords.join(' '));
|
||||
if (filterConfig.ruleName && filterConfig.ruleName(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.labels && filterConfig.labels(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.ruleType && filterConfig.ruleType(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.ruleState && filterConfig.ruleState(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.ruleHealth && filterConfig.ruleHealth(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.contactPoint && filterConfig.contactPoint(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.dashboardUid && filterConfig.dashboardUid(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.plugins && filterConfig.plugins(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterConfig.dataSourceNames && filterConfig.dataSourceNames(rule, filterState) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function freeFormFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.freeFormWords.length > 0) {
|
||||
const nameMatches = fuzzyMatches(rule.name, filterState.freeFormWords.join(' '));
|
||||
if (!nameMatches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Rule name search: Backend-supported for backend-filtered rules, client-side otherwise
|
||||
if (filterState.ruleName && !backendFiltered && !fuzzyMatches(name, filterState.ruleName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function ruleNameFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.ruleName && !fuzzyMatches(rule.name, filterState.ruleName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function labelsFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.labels.length > 0) {
|
||||
const matchers = compact(filterState.labels.map(looseParseMatcher));
|
||||
const doRuleLabelsMatchQuery = matchers.length > 0 && labelsMatchMatchers(labels, matchers);
|
||||
const doRuleLabelsMatchQuery = matchers.length > 0 && labelsMatchMatchers(rule.labels || {}, matchers);
|
||||
|
||||
// Also check alerts if they exist
|
||||
const doAlertsContainMatchingLabels =
|
||||
@@ -69,10 +217,18 @@ export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter, backendF
|
||||
}
|
||||
}
|
||||
|
||||
if (filterState.ruleType && type !== filterState.ruleType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function ruleTypeFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.ruleType && rule.type !== filterState.ruleType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function ruleStateFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.ruleState) {
|
||||
if (!prometheusRuleType.alertingRule(rule)) {
|
||||
return false;
|
||||
@@ -82,10 +238,18 @@ export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter, backendF
|
||||
}
|
||||
}
|
||||
|
||||
if (filterState.ruleHealth && normalizeHealth(health) !== filterState.ruleHealth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function ruleHealthFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.ruleHealth && normalizeHealth(rule.health) !== filterState.ruleHealth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function contactPointFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.contactPoint) {
|
||||
if (!prometheusRuleType.grafana.alertingRule(rule)) {
|
||||
return false;
|
||||
@@ -100,6 +264,10 @@ export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter, backendF
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function dashboardUidFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
if (filterState.dashboardUid) {
|
||||
if (!prometheusRuleType.alertingRule(rule)) {
|
||||
return false;
|
||||
@@ -111,11 +279,19 @@ export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter, backendF
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function pluginsFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
// Plugins filter - hide plugin-provided rules when set to 'hide'
|
||||
if (filterState.plugins === 'hide' && isPluginProvidedRule(rule)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function dataSourceNamesFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
|
||||
// Note: We can't implement these filters from reduceGroups because they rely on rulerRule property
|
||||
// which is not available in PromRuleDTO:
|
||||
// - contactPoint filter
|
||||
@@ -156,3 +332,45 @@ const mapDataSourceNamesToUids = memoize(
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
);
|
||||
|
||||
/**
|
||||
* Build title search parameter for backend filtering
|
||||
* Combines ruleName and freeFormWords into a single search string
|
||||
*/
|
||||
export function buildTitleSearch(filterState: RulesFilter): string | undefined {
|
||||
const titleParts: string[] = [];
|
||||
|
||||
const ruleName = filterState.ruleName?.trim();
|
||||
if (ruleName) {
|
||||
titleParts.push(ruleName);
|
||||
}
|
||||
|
||||
const freeFormSegment = filterState.freeFormWords
|
||||
.map((word) => word.trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
if (freeFormSegment) {
|
||||
titleParts.push(freeFormSegment);
|
||||
}
|
||||
|
||||
if (titleParts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return titleParts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize filter state for case-insensitive matching
|
||||
* Lowercase free form words, rule name, group name and namespace
|
||||
*/
|
||||
function normalizeFilterState(filterState: RulesFilter): RulesFilter {
|
||||
return {
|
||||
...filterState,
|
||||
freeFormWords: filterState.freeFormWords.map((word) => word.toLowerCase()),
|
||||
ruleName: filterState.ruleName?.toLowerCase(),
|
||||
groupName: filterState.groupName?.toLowerCase(),
|
||||
namespace: filterState.namespace?.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ import { PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
import { RuleSource } from '../../search/rulesSearchParser';
|
||||
import { getFilter } from '../../utils/search';
|
||||
|
||||
import { buildTitleSearch, hasClientSideFilters } from './useFilteredRulesIterator';
|
||||
import { buildTitleSearch } from './filters';
|
||||
import { hasClientSideFilters } from './useFilteredRulesIterator';
|
||||
|
||||
describe('hasClientSideFilters', () => {
|
||||
const originalFeatureToggles = config.featureToggles;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '../../utils/datasource';
|
||||
import { RulePositionHash, createRulePositionHash } from '../rulePositionHash';
|
||||
|
||||
import { groupFilter, ruleFilter } from './filters';
|
||||
import { getDatasourceFilter, getGrafanaFilter } from './filters';
|
||||
import { useGrafanaGroupsGenerator, usePrometheusGroupsGenerator } from './prometheusGroupsGenerator';
|
||||
|
||||
export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin;
|
||||
@@ -78,30 +78,19 @@ export function useFilteredRulesIteratorProvider() {
|
||||
/* this is the abort controller that allows us to stop an AsyncIterable */
|
||||
const abortController = new AbortController();
|
||||
|
||||
const normalizedFilterState = normalizeFilterState(filterState);
|
||||
const hasDataSourceFilterActive = Boolean(filterState.dataSourceNames.length);
|
||||
const useBackendFilters = shouldUseBackendFilters();
|
||||
|
||||
const titleSearch = useBackendFilters ? buildTitleSearch(filterState) : undefined;
|
||||
const ruleType = useBackendFilters ? filterState.ruleType : undefined;
|
||||
const dashboardUid = useBackendFilters ? filterState.dashboardUid : undefined;
|
||||
const { backendFilter, frontendFilter } = getGrafanaFilter(filterState);
|
||||
|
||||
const grafanaRulesGenerator: AsyncIterableX<RuleWithOrigin> = from(
|
||||
grafanaGroupsGenerator(groupLimit, {
|
||||
contactPoint: filterState.contactPoint ?? undefined,
|
||||
health: filterState.ruleHealth ? [filterState.ruleHealth] : [],
|
||||
state: filterState.ruleState ? [filterState.ruleState] : [],
|
||||
title: titleSearch,
|
||||
type: ruleType,
|
||||
dashboardUid,
|
||||
})
|
||||
grafanaGroupsGenerator(groupLimit, backendFilter)
|
||||
).pipe(
|
||||
withAbort(abortController.signal),
|
||||
concatMap((groups) =>
|
||||
groups
|
||||
.filter((group) => groupFilter(group, normalizedFilterState))
|
||||
.filter((group) => frontendFilter.groupMatches(group))
|
||||
.flatMap((group) => group.rules.map((rule) => ({ group, rule })))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState, useBackendFilters))
|
||||
.filter(({ rule }) => frontendFilter.ruleMatches(rule))
|
||||
.map(({ group, rule }) => mapGrafanaRuleToRuleWithOrigin(group, rule))
|
||||
),
|
||||
catchError(() => empty())
|
||||
@@ -116,6 +105,8 @@ export function useFilteredRulesIteratorProvider() {
|
||||
return { iterable: grafanaRulesGenerator, abortController };
|
||||
}
|
||||
|
||||
const { groupMatches, ruleMatches } = getDatasourceFilter(filterState);
|
||||
|
||||
const dataSourceGenerators: Array<AsyncIterableX<RuleWithOrigin>> = externalRulesSourcesToFetchFrom.map(
|
||||
(dataSourceIdentifier) => {
|
||||
const promGroupsGenerator: AsyncIterableX<RuleWithOrigin> = from(
|
||||
@@ -124,9 +115,9 @@ export function useFilteredRulesIteratorProvider() {
|
||||
withAbort(abortController.signal),
|
||||
concatMap((groups) =>
|
||||
groups
|
||||
.filter((group) => groupFilter(group, normalizedFilterState))
|
||||
.filter((group) => groupMatches(group))
|
||||
.flatMap((group) => group.rules.map((rule, index) => ({ group, rule, index })))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState, false))
|
||||
.filter(({ rule }) => ruleMatches(rule))
|
||||
.map(({ group, rule, index }) => mapRuleToRuleWithOrigin(dataSourceIdentifier, group, rule, index))
|
||||
),
|
||||
catchError(() => empty())
|
||||
@@ -176,30 +167,6 @@ export function hasClientSideFilters(filterState: RulesFilter): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function buildTitleSearch(filterState: RulesFilter): string | undefined {
|
||||
const titleParts: string[] = [];
|
||||
|
||||
const ruleName = filterState.ruleName?.trim();
|
||||
if (ruleName) {
|
||||
titleParts.push(ruleName);
|
||||
}
|
||||
|
||||
const freeFormSegment = filterState.freeFormWords
|
||||
.map((word) => word.trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
if (freeFormSegment) {
|
||||
titleParts.push(freeFormSegment);
|
||||
}
|
||||
|
||||
if (titleParts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return titleParts.join(' ');
|
||||
}
|
||||
|
||||
function mergeIterables(iterables: Array<AsyncIterableX<RuleWithOrigin>>): AsyncIterableX<RuleWithOrigin> {
|
||||
if (iterables.length === 0) {
|
||||
return empty();
|
||||
@@ -268,16 +235,3 @@ function mapGrafanaRuleToRuleWithOrigin(
|
||||
origin: 'grafana',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowercase free form words, rule name, group name and namespace
|
||||
*/
|
||||
function normalizeFilterState(filterState: RulesFilter): RulesFilter {
|
||||
return {
|
||||
...filterState,
|
||||
freeFormWords: filterState.freeFormWords.map((word) => word.toLowerCase()),
|
||||
ruleName: filterState.ruleName?.toLowerCase(),
|
||||
groupName: filterState.groupName?.toLowerCase(),
|
||||
namespace: filterState.namespace?.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user