From 24d970e4b968c4a62da785c49141dd67a63537ff Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 20 Nov 2025 13:58:13 +0100 Subject: [PATCH] Alerting: Remove stale filtering code (#114231) Remove the old filters file, update import paths --- .../alerting/unified/rule-list/FilterView.tsx | 2 +- .../rule-list/PaginatedDataSourceLoader.tsx | 2 +- .../rule-list/PaginatedGrafanaLoader.tsx | 2 +- .../unified/rule-list/hooks/filters.ts | 377 ------------------ .../hooks/useFilteredRulesIterator.test.ts | 85 ---- .../hooks/useFilteredRulesIterator.ts | 26 +- 6 files changed, 5 insertions(+), 489 deletions(-) delete mode 100644 public/app/features/alerting/unified/rule-list/hooks/filters.ts delete mode 100644 public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 8ac1e1a8c58..fb46822f0c3 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -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'; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx index 2e2c2fda607..546d500ace2 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx @@ -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'; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index d1835d29757..a39509608b3 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -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'; diff --git a/public/app/features/alerting/unified/rule-list/hooks/filters.ts b/public/app/features/alerting/unified/rule-list/hooks/filters.ts deleted file mode 100644 index 6ac4d668e9d..00000000000 --- a/public/app/features/alerting/unified/rule-list/hooks/filters.ts +++ /dev/null @@ -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 -) => boolean; - -type RuleFilterConfig = Record< - Exclude, - RuleFilterHandler | null ->; - -type GroupFilterConfig = Record, 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, - 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): boolean { - if (filterState.namespace && !fuzzyMatches(group.file, filterState.namespace)) { - return false; - } - - return true; -} - -function groupNameFilter(group: PromRuleGroupDTO, filterState: Pick): 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(), - }; -} diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts deleted file mode 100644 index 1b226d50c5b..00000000000 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index 86f593661ea..407426bb3e1 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -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 { if (iterables.length === 0) { return empty();