From fd955f90ac8d6c57b0d138f21a14f3f1dc275346 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 14 Jan 2026 09:48:07 +0100 Subject: [PATCH] Alerting: Enable server-side folder search for GMA rules (#116201) * Alerting: Support backend filtering for folder search Updates the Grafana managed rules API and filter logic to support server-side filtering by folder (namespace). Changes: - Add `searchFolder` parameter to `getGrafanaGroups` API endpoint - Map filter state `namespace` to `searchFolder` in backend filter - Disable client-side namespace filtering when backend filtering is enabled - Update tests to verify correct behavior for folder search with backend filters * Add missing property in filter options * Update tests --- .../alerting/unified/api/prometheusApi.ts | 3 ++ .../rule-list/hooks/grafanaFilter.test.ts | 39 +++++++++++++++---- .../unified/rule-list/hooks/grafanaFilter.ts | 3 +- .../hooks/prometheusGroupsGenerator.ts | 1 + .../rule-list/paginationLimits.test.ts | 22 +---------- 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/public/app/features/alerting/unified/api/prometheusApi.ts b/public/app/features/alerting/unified/api/prometheusApi.ts index 9432da368b6..c03b52eab4e 100644 --- a/public/app/features/alerting/unified/api/prometheusApi.ts +++ b/public/app/features/alerting/unified/api/prometheusApi.ts @@ -46,6 +46,7 @@ export type GrafanaPromRulesOptions = Omit { expect(frontendFilter.ruleMatches(regularRule)).toBe(true); expect(frontendFilter.ruleMatches(pluginRule)).toBe(true); }); + + it('should include searchFolder in backend filter when namespace is provided', () => { + const { backendFilter } = getGrafanaFilter(getFilter({ namespace: 'my-folder' })); + + expect(backendFilter.searchFolder).toBe('my-folder'); + }); + + it('should skip namespace filtering on frontend when backend filtering is enabled', () => { + const group: PromRuleGroupDTO = { + name: 'Test Group', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + const { frontendFilter } = getGrafanaFilter(getFilter({ namespace: 'staging' })); + // Should return true because namespace filter is null (handled by backend) + expect(frontendFilter.groupMatches(group)).toBe(true); + }); }); describe('when alertingUIUseBackendFilters is disabled', () => { @@ -537,6 +556,12 @@ describe('grafana-managed rules', () => { expect(backendFilter.searchGroupName).toBeUndefined(); }); + it('should not include searchFolder in backend filter', () => { + const { backendFilter } = getGrafanaFilter(getFilter({ namespace: 'my-folder' })); + + expect(backendFilter.searchFolder).toBeUndefined(); + }); + it('should perform groupName filtering on frontend', () => { const group: PromRuleGroupDTO = { name: 'CPU Usage Alerts', @@ -706,8 +731,8 @@ describe('grafana-managed rules', () => { expect(frontendFilter.groupMatches(group)).toBe(true); }); - it('should still apply always-frontend filters (namespace)', () => { - // Namespace filter should still work + it('should skip namespace filtering on frontend', () => { + // Namespace filter should be handled by backend const group: PromRuleGroupDTO = { name: 'Test Group', file: 'production/alerts', @@ -719,7 +744,7 @@ describe('grafana-managed rules', () => { expect(nsFilter.groupMatches(group)).toBe(true); const { frontendFilter: nsFilter2 } = getGrafanaFilter(getFilter({ namespace: 'staging' })); - expect(nsFilter2.groupMatches(group)).toBe(false); + expect(nsFilter2.groupMatches(group)).toBe(true); }); it('should skip dataSourceNames filtering on frontend (handled by backend)', () => { @@ -807,8 +832,8 @@ describe('grafana-managed rules', () => { expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(false); }); - it('should return true for client-side only filters', () => { - expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + it('should return false for namespace filter (handled by backend)', () => { + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(false); }); it('should return false for plugins filter (handled by backend when feature toggle is enabled)', () => { @@ -862,8 +887,8 @@ describe('grafana-managed rules', () => { expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); - // Should return true for: always-frontend filters only (namespace) - expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + // Should return false for: namespace (handled by backend) + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(false); // plugins is backend-handled when both feature toggles are enabled expect(hasGrafanaClientSideFilters(getFilter({ plugins: 'hide' }))).toBe(false); diff --git a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts index e8c4cf3c44a..cca395a9cb2 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts @@ -96,6 +96,7 @@ export function getGrafanaFilter(filterState: Partial) { datasources: ruleFilterConfig.dataSourceNames ? undefined : datasourceUids, ruleMatchers: ruleMatchersBackendFilter, plugins: ruleFilterConfig.plugins ? undefined : normalizedFilterState.plugins, + searchFolder: groupFilterConfig.namespace ? undefined : normalizedFilterState.namespace, }; return { @@ -134,7 +135,7 @@ function buildGrafanaFilterConfigs() { }; const groupFilterConfig: GroupFilterConfig = { - namespace: namespaceFilter, + namespace: useBackendFilters ? null : namespaceFilter, groupName: useBackendFilters ? null : groupNameFilter, }; diff --git a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts index add1097fa0f..e2cb1247ac8 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts @@ -45,6 +45,7 @@ interface GrafanaPromApiFilter { contactPoint?: string; title?: string; searchGroupName?: string; + searchFolder?: string; type?: 'alerting' | 'recording'; dashboardUid?: string; } diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts index 5d6b7c97782..648cfc18190 100644 --- a/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts +++ b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts @@ -75,6 +75,7 @@ describe('paginationLimits', () => { { contactPoint: 'slack' }, { dataSourceNames: ['prometheus'] }, { labels: ['severity=critical'] }, + { namespace: 'production' }, ])( 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', (filterState) => { @@ -84,16 +85,6 @@ describe('paginationLimits', () => { expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); } ); - - it.each>([ - { namespace: 'production' }, - { ruleState: PromAlertingRuleState.Firing, namespace: 'production' }, - ])('should return large limits for both when frontend filters are used: %p', (filterState) => { - const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); - - expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - }); }); describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => { @@ -158,6 +149,7 @@ describe('paginationLimits', () => { { contactPoint: 'slack' }, { dataSourceNames: ['prometheus'] }, { labels: ['severity=critical'] }, + { namespace: 'production' }, ])( 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', (filterState) => { @@ -167,16 +159,6 @@ describe('paginationLimits', () => { expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); } ); - - it.each>([{ namespace: 'production' }])( - 'should return large limits for both when frontend filters are used: %p', - (filterState) => { - const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); - - expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - } - ); }); }); });