Alerting: Remove stale filtering code (#114231)

Remove the old filters file, update import paths
This commit is contained in:
Konrad Lalik
2025-11-20 13:58:13 +01:00
committed by GitHub
parent e6a4e6fa4e
commit 24d970e4b9
6 changed files with 5 additions and 489 deletions
@@ -16,11 +16,11 @@ import { GrafanaRuleListItem } from './GrafanaRuleListItem';
import LoadMoreHelper from './LoadMoreHelper';
import { UnknownRuleListItem } from './components/AlertRuleListItem';
import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader';
import { hasClientSideFilters } from './hooks/grafanaFilter';
import {
GrafanaRuleWithOrigin,
PromRuleWithOrigin,
RuleWithOrigin,
hasClientSideFilters,
useFilteredRulesIteratorProvider,
} from './hooks/useFilteredRulesIterator';
import { FRONTEND_LIST_PAGE_SIZE, getSearchApiGroupPageSize } from './paginationLimits';
@@ -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 { getDatasourceFilter } from './hooks/filters';
import { getDatasourceFilter } from './hooks/datasourceFilter';
import { toIndividualRuleGroups, usePrometheusGroupsGenerator } from './hooks/prometheusGroupsGenerator';
import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups';
import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits';
@@ -18,7 +18,7 @@ import { ListGroup } from './components/ListGroup';
import { ListSection } from './components/ListSection';
import { LoadMoreButton } from './components/LoadMoreButton';
import { NoRulesFound } from './components/NoRulesFound';
import { getGrafanaFilter } from './hooks/filters';
import { getGrafanaFilter } from './hooks/grafanaFilter';
import { toIndividualRuleGroups, useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator';
import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups';
import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits';
@@ -1,377 +0,0 @@
import { attempt, compact, isString } from 'lodash';
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';
import { getDatasourceAPIUid } from '../../utils/datasource';
import { fuzzyMatches } from '../../utils/fuzzySearch';
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,
searchGroupName: useBackendFilters ? normalizedFilterState.groupName : 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: useBackendFilters ? null : 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
*/
function groupMatches(
group: PromRuleGroupDTO,
filterState: Pick<RulesFilter, 'namespace' | 'groupName'>,
filterConfig: GroupFilterConfig
): boolean {
if (filterConfig.namespace && filterConfig.namespace(group, filterState) === false) {
return false;
}
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;
}
return true;
}
/**
* @returns True if the rule matches the filter, false otherwise
*/
function ruleMatches(rule: PromRuleDTO, filterState: RulesFilter, filterConfig: RuleFilterConfig) {
if (filterConfig.freeFormWords && filterConfig.freeFormWords(rule, filterState) === false) {
return false;
}
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;
}
}
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(rule.labels || {}, matchers);
// Also check alerts if they exist
const doAlertsContainMatchingLabels =
matchers.length > 0 &&
prometheusRuleType.alertingRule(rule) &&
rule.alerts &&
rule.alerts.some((alert) => labelsMatchMatchers(alert.labels || {}, matchers));
if (!doRuleLabelsMatchQuery && !doAlertsContainMatchingLabels) {
return false;
}
}
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;
}
if (rule.state !== filterState.ruleState) {
return false;
}
}
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;
}
if (!rule.notificationSettings) {
return false;
}
if (filterState.contactPoint !== rule.notificationSettings.receiver) {
return false;
}
}
return true;
}
function dashboardUidFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean {
if (filterState.dashboardUid) {
if (!prometheusRuleType.alertingRule(rule)) {
return false;
}
const dashboardAnnotation = rule.annotations?.[Annotation.dashboardUID];
if (dashboardAnnotation !== filterState.dashboardUid) {
return false;
}
}
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
// - dataSourceNames filter
if (filterState.dataSourceNames.length > 0) {
const isGrafanaRule = prometheusRuleType.grafana.rule(rule);
if (isGrafanaRule) {
try {
const filterDatasourceUids = mapDataSourceNamesToUids(filterState.dataSourceNames);
const queriedDatasourceUids = rule.queriedDatasourceUIDs || [];
const queryIncludesDataSource = queriedDatasourceUids.some((uid) => filterDatasourceUids.includes(uid));
if (!queryIncludesDataSource) {
return false;
}
} catch (error) {
return false;
}
}
}
return true;
}
function looseParseMatcher(matcherQuery: string): Matcher | undefined {
try {
return parseMatcher(matcherQuery);
} catch {
// Try to createa a matcher than matches all values for a given key
return { name: matcherQuery, value: '', isRegex: true, isEqual: true };
}
}
// Memoize the function to avoid calling getDatasourceAPIUid for the filter values multiple times
const mapDataSourceNamesToUids = memoize(
(names: string[]): string[] => {
return names.map((name) => attempt(getDatasourceAPIUid, name)).filter(isString);
},
{ 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(),
};
}
@@ -1,85 +0,0 @@
import { config } from '@grafana/runtime';
import { PromRuleType } from 'app/types/unified-alerting-dto';
import { RuleSource } from '../../search/rulesSearchParser';
import { getFilter } from '../../utils/search';
import { hasClientSideFilters } from './useFilteredRulesIterator';
describe('hasClientSideFilters', () => {
const originalFeatureToggles = config.featureToggles;
beforeEach(() => {
config.featureToggles = { ...originalFeatureToggles };
});
afterEach(() => {
config.featureToggles = originalFeatureToggles;
});
describe('when alertingUIUseBackendFilters is enabled', () => {
beforeEach(() => {
config.featureToggles.alertingUIUseBackendFilters = true;
});
it('should return false for backend-supported filters', () => {
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false);
expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false);
expect(hasClientSideFilters(getFilter({ groupName: 'my-group' }))).toBe(false);
});
it('should return true for client-side only filters', () => {
expect(hasClientSideFilters(getFilter({ namespace: 'test' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ ruleSource: RuleSource.DataSource }))).toBe(true);
});
it('should return false when no filters are applied', () => {
expect(hasClientSideFilters(getFilter({}))).toBe(false);
});
});
describe('when alertingUIUseBackendFilters is disabled', () => {
beforeEach(() => {
config.featureToggles.alertingUIUseBackendFilters = false;
});
it('should return true for backend-supported filters when backend filtering is disabled', () => {
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true);
expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ groupName: 'my-group' }))).toBe(true);
});
it('should return true for client-side only filters', () => {
expect(hasClientSideFilters(getFilter({ namespace: 'test' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ ruleSource: RuleSource.DataSource }))).toBe(true);
});
it('should return false when no filters are applied', () => {
expect(hasClientSideFilters(getFilter({}))).toBe(false);
});
});
describe('when alertingUIUseBackendFilters is undefined (default)', () => {
beforeEach(() => {
config.featureToggles.alertingUIUseBackendFilters = undefined;
});
it('should default to client-side filtering for backward compatibility', () => {
// Default behavior should be client-side filtering
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true);
expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ groupName: 'my-group' }))).toBe(true);
});
});
});
@@ -15,7 +15,6 @@ import {
PromRuleGroupDTO,
} from 'app/types/unified-alerting-dto';
import { shouldUseBackendFilters } from '../../featureToggles';
import { RuleSource, RulesFilter } from '../../search/rulesSearchParser';
import {
getDataSourceByUid,
@@ -25,7 +24,8 @@ import {
} from '../../utils/datasource';
import { RulePositionHash, createRulePositionHash } from '../rulePositionHash';
import { getDatasourceFilter, getGrafanaFilter } from './filters';
import { getDatasourceFilter } from './datasourceFilter';
import { getGrafanaFilter } from './grafanaFilter';
import { useGrafanaGroupsGenerator, usePrometheusGroupsGenerator } from './prometheusGroupsGenerator';
export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin;
@@ -146,28 +146,6 @@ export function useFilteredRulesIteratorProvider() {
return getFilteredRulesIterable;
}
/**
* Determines if client-side filtering is needed for Grafana-managed rules.
*/
export function hasClientSideFilters(filterState: RulesFilter): boolean {
const useBackendFilters = shouldUseBackendFilters();
return (
// When backend filters are disabled, title search, type filter, dashboard filter, and group name filter need client-side filtering
(!useBackendFilters &&
(filterState.freeFormWords.length > 0 ||
Boolean(filterState.ruleName) ||
Boolean(filterState.ruleType) ||
Boolean(filterState.dashboardUid) ||
Boolean(filterState.groupName))) ||
// Client-side only filters:
Boolean(filterState.namespace) ||
filterState.dataSourceNames.length > 0 ||
filterState.labels.length > 0 ||
filterState.ruleSource === RuleSource.DataSource
);
}
function mergeIterables(iterables: Array<AsyncIterableX<RuleWithOrigin>>): AsyncIterableX<RuleWithOrigin> {
if (iterables.length === 0) {
return empty();