diff --git a/public/app/features/alerting/unified/api/prometheusApi.ts b/public/app/features/alerting/unified/api/prometheusApi.ts index 565a50e46ad..a8e4279de7f 100644 --- a/public/app/features/alerting/unified/api/prometheusApi.ts +++ b/public/app/features/alerting/unified/api/prometheusApi.ts @@ -39,6 +39,7 @@ export type GrafanaPromRulesOptions = Omit { const currentGenerator = groupsGenerator.current; diff --git a/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts b/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts index e6aa3089e55..b2360913581 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts @@ -22,6 +22,27 @@ import { ruleTypeFilter, } from './filterPredicates'; +/** + * Determines if client-side filtering is needed for data source-managed rules. + */ +export function hasDatasourceClientSideFilters(filterState: Partial): boolean { + // Check if any filter that applies to datasource rules is active + return ( + (filterState.freeFormWords && filterState.freeFormWords.length > 0) || + Boolean(filterState.ruleName) || + Boolean(filterState.ruleState) || + Boolean(filterState.ruleType) || + (filterState.dataSourceNames && filterState.dataSourceNames.length > 0) || + (filterState.labels && filterState.labels.length > 0) || + Boolean(filterState.ruleHealth) || + Boolean(filterState.dashboardUid) || + Boolean(filterState.plugins) || + Boolean(filterState.contactPoint) || + Boolean(filterState.namespace) || + Boolean(filterState.groupName) + ); +} + /** * Builds filter configurations for data source-managed alert rules. * diff --git a/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts b/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts index d8594135463..7bcf22e30dc 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts @@ -32,12 +32,14 @@ export function buildTitleSearch(filterState: RulesFilter): string | undefined { * Normalize filter state for case-insensitive matching * Lowercase free form words, rule name, group name and namespace */ -export function normalizeFilterState(filterState: RulesFilter): RulesFilter { +export function normalizeFilterState(filterState: Partial): RulesFilter { return { ...filterState, - freeFormWords: filterState.freeFormWords.map((word) => word.toLowerCase()), + freeFormWords: filterState.freeFormWords?.map((word) => word.toLowerCase()) ?? [], ruleName: filterState.ruleName?.toLowerCase(), groupName: filterState.groupName?.toLowerCase(), namespace: filterState.namespace?.toLowerCase(), + dataSourceNames: filterState.dataSourceNames ?? [], + labels: filterState.labels ?? [], }; } diff --git a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts index 56be25ee248..6a714382a96 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts @@ -8,7 +8,7 @@ import { Annotation } from '../../utils/constants'; import { getDatasourceAPIUid } from '../../utils/datasource'; import { getFilter } from '../../utils/search'; -import { getGrafanaFilter, hasClientSideFilters } from './grafanaFilter'; +import { getGrafanaFilter, hasGrafanaClientSideFilters } from './grafanaFilter'; jest.mock('../../utils/datasource'); @@ -670,41 +670,41 @@ describe('grafana-managed rules', () => { }); }); - describe('hasClientSideFilters', () => { + describe('hasGrafanaClientSideFilters', () => { describe('when alertingUIUseBackendFilters is disabled', () => { testWithFeatureToggles({ disable: ['alertingUIUseBackendFilters'] }); it('should return false when no filters are applied', () => { - expect(hasClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); }); it('should return true for title-related filters (freeFormWords, ruleName)', () => { - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); }); it('should return true for ruleType filter', () => { - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true); }); it('should return true for dashboardUid filter', () => { - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true); }); it('should return true for groupName filter', () => { - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); }); it('should return true for client-side only filters', () => { - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); it('should return false for backend-only filters (state, health, contactPoint)', () => { - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); }); }); @@ -712,36 +712,36 @@ describe('grafana-managed rules', () => { testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters'] }); it('should return false when no filters are applied', () => { - expect(hasClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); }); it('should return false for title-related filters (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); }); it('should return false for ruleType filter (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); }); it('should return false for dashboardUid filter (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); }); it('should return false for groupName filter (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); }); it('should return true for client-side only filters', () => { - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); it('should return false for backend-only filters (state, health, contactPoint)', () => { - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); }); }); @@ -750,20 +750,20 @@ describe('grafana-managed rules', () => { it('should return correct values for all filter types', () => { // Should return false for: empty, backend-handled (ruleType, dashboardUid), and backend-only filters - expect(hasClientSideFilters(getFilter({}))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); // Should return true for: frontend-handled filters - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); }); @@ -772,20 +772,20 @@ describe('grafana-managed rules', () => { it('should return correct values for all filter types', () => { // Should return false for: empty, all backend-handled filters, and backend-only filters - expect(hasClientSideFilters(getFilter({}))).toBe(false); - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + 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 - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); }); }); 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 5b5ddd81f9b..0cc89ceafcf 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts @@ -24,26 +24,26 @@ import { /** * Determines if client-side filtering is needed for Grafana-managed rules. */ -export function hasClientSideFilters(filterState: RulesFilter): boolean { +export function hasGrafanaClientSideFilters(filterState: Partial): boolean { const { ruleFilterConfig, groupFilterConfig } = buildGrafanaFilterConfigs(); // Check each rule filter: if the config has a non-null handler AND the filter state has a value, we need client-side filtering const hasActiveRuleFilters = - (ruleFilterConfig.freeFormWords !== null && filterState.freeFormWords.length > 0) || - (ruleFilterConfig.ruleName !== null && Boolean(filterState.ruleName)) || - (ruleFilterConfig.ruleState !== null && Boolean(filterState.ruleState)) || - (ruleFilterConfig.ruleType !== null && Boolean(filterState.ruleType)) || - (ruleFilterConfig.dataSourceNames !== null && filterState.dataSourceNames.length > 0) || - (ruleFilterConfig.labels !== null && filterState.labels.length > 0) || - (ruleFilterConfig.ruleHealth !== null && Boolean(filterState.ruleHealth)) || - (ruleFilterConfig.dashboardUid !== null && Boolean(filterState.dashboardUid)) || - (ruleFilterConfig.plugins !== null && Boolean(filterState.plugins)) || - (ruleFilterConfig.contactPoint !== null && Boolean(filterState.contactPoint)); + (ruleFilterConfig.freeFormWords !== null && Boolean(filterState?.freeFormWords?.length)) || + (ruleFilterConfig.ruleName !== null && Boolean(filterState?.ruleName)) || + (ruleFilterConfig.ruleState !== null && Boolean(filterState?.ruleState)) || + (ruleFilterConfig.ruleType !== null && Boolean(filterState?.ruleType)) || + (ruleFilterConfig.dataSourceNames !== null && Boolean(filterState?.dataSourceNames?.length)) || + (ruleFilterConfig.labels !== null && Boolean(filterState?.labels?.length)) || + (ruleFilterConfig.ruleHealth !== null && Boolean(filterState?.ruleHealth)) || + (ruleFilterConfig.dashboardUid !== null && Boolean(filterState?.dashboardUid)) || + (ruleFilterConfig.plugins !== null && Boolean(filterState?.plugins)) || + (ruleFilterConfig.contactPoint !== null && Boolean(filterState?.contactPoint)); // Check each group filter: if the config has a non-null handler AND the filter state has a value, we need client-side filtering const hasActiveGroupFilters = - (groupFilterConfig.namespace !== null && Boolean(filterState.namespace)) || - (groupFilterConfig.groupName !== null && Boolean(filterState.groupName)); + (groupFilterConfig.namespace !== null && Boolean(filterState?.namespace)) || + (groupFilterConfig.groupName !== null && Boolean(filterState?.groupName)); return hasActiveRuleFilters || hasActiveGroupFilters; } @@ -55,7 +55,7 @@ export function hasClientSideFilters(filterState: RulesFilter): boolean { * The backend filter is used for server-side filtering when `shouldUseBackendFilters()` is enabled, * while the frontend filter provides client-side matching functions for rules and groups. */ -export function getGrafanaFilter(filterState: RulesFilter) { +export function getGrafanaFilter(filterState: Partial) { const normalizedFilterState = normalizeFilterState(filterState); const { ruleFilterConfig, groupFilterConfig } = buildGrafanaFilterConfigs(); 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 db2d5df077d..add1097fa0f 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { MergeExclusive } from 'type-fest'; import { DataSourceRulesSourceIdentifier, RuleHealth } from 'app/types/unified-alerting'; import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; @@ -16,27 +17,23 @@ interface UseGeneratorHookOptions { limitAlerts?: number; } -interface FetchGroupsOptions { - groupLimit?: number; - groupNextToken?: string; -} - export function usePrometheusGroupsGenerator() { const [getGroups] = useLazyGetGroupsQuery(); return useCallback( async function* (ruleSource: DataSourceRulesSourceIdentifier, groupLimit: number) { - const getRuleSourceGroupsWithCache = async (fetchOptions: FetchGroupsOptions) => { + const getRuleSourceGroupsWithCache = async (fetchOptions: GroupsNextPageOptions) => { const response = await getGroups({ ruleSource: { uid: ruleSource.uid }, notificationOptions: { showErrorAlert: false }, + groupLimit, ...fetchOptions, }).unwrap(); return response; }; - yield* genericGroupsGenerator(getRuleSourceGroupsWithCache, groupLimit); + yield* genericGroupsGenerator(getRuleSourceGroupsWithCache); }, [getGroups] ); @@ -52,8 +49,21 @@ interface GrafanaPromApiFilter { dashboardUid?: string; } -interface GrafanaFetchGroupsOptions extends FetchGroupsOptions { +interface GrafanaFetchGroupsOptions extends GroupsNextPageOptions { filter?: GrafanaPromApiFilter; + groupLimit?: number; + // Limits the number of total rules returned across all groups + // Rounds up to full groups, so the response may contain more rules than the group limit + ruleLimit?: number; +} + +export type GrafanaFetchGroupsLimit = MergeExclusive<{ groupLimit: number }, { ruleLimit: number }>; + +export type DataSourceFetchGroupsLimit = { groupLimit: number }; + +export interface FetchGroupsLimitOptions { + grafanaManagedLimit: GrafanaFetchGroupsLimit; + datasourceManagedLimit: DataSourceFetchGroupsLimit; } export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions = {}) { @@ -78,11 +88,16 @@ export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions = ); return useCallback( - async function* (groupLimit: number, filter?: GrafanaPromApiFilter) { - yield* genericGroupsGenerator( - (fetchOptions) => getGroupsAndProvideCache({ ...fetchOptions, filter }), - groupLimit - ); + async function* (limit: GrafanaFetchGroupsLimit, filter?: GrafanaPromApiFilter) { + const fetchGroups = (fetchOptions: GroupsNextPageOptions) => + getGroupsAndProvideCache({ + ...fetchOptions, + filter, + groupLimit: 'groupLimit' in limit ? limit.groupLimit : undefined, + ruleLimit: 'ruleLimit' in limit ? limit.ruleLimit : undefined, + }); + + yield* genericGroupsGenerator(fetchGroups); }, [getGroupsAndProvideCache] ); @@ -105,21 +120,24 @@ export function toIndividualRuleGroups( })(); } +interface GroupsNextPageOptions { + groupNextToken?: string; +} + // Generator lazily provides groups one by one only when needed // This might look a bit complex but it allows us to have one API for paginated and non-paginated Prometheus data sources // For unpaginated data sources we fetch everything in one go // For paginated we fetch the next page when needed async function* genericGroupsGenerator( - fetchGroups: (options: FetchGroupsOptions) => Promise>, - groupLimit: number + fetchGroups: (options: GroupsNextPageOptions) => Promise> ) { - let response = await fetchGroups({ groupLimit }); + let response = await fetchGroups({ groupNextToken: undefined }); yield response.data.groups; let lastToken: string | undefined = response.data?.groupNextToken; while (lastToken) { - response = await fetchGroups({ groupNextToken: lastToken, groupLimit: groupLimit }); + response = await fetchGroups({ groupNextToken: lastToken }); yield response.data.groups; lastToken = response.data?.groupNextToken; } 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 407426bb3e1..7c5533beea5 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -26,7 +26,11 @@ import { RulePositionHash, createRulePositionHash } from '../rulePositionHash'; import { getDatasourceFilter } from './datasourceFilter'; import { getGrafanaFilter } from './grafanaFilter'; -import { useGrafanaGroupsGenerator, usePrometheusGroupsGenerator } from './prometheusGroupsGenerator'; +import { + FetchGroupsLimitOptions, + useGrafanaGroupsGenerator, + usePrometheusGroupsGenerator, +} from './prometheusGroupsGenerator'; export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin; @@ -74,7 +78,7 @@ export function useFilteredRulesIteratorProvider() { const prometheusGroupsGenerator = usePrometheusGroupsGenerator(); const grafanaGroupsGenerator = useGrafanaGroupsGenerator({ limitAlerts: 0 }); - const getFilteredRulesIterable = (filterState: RulesFilter, groupLimit: number): GetIteratorResult => { + const getFilteredRulesIterable = (filterState: RulesFilter, options: FetchGroupsLimitOptions): GetIteratorResult => { /* this is the abort controller that allows us to stop an AsyncIterable */ const abortController = new AbortController(); @@ -83,7 +87,7 @@ export function useFilteredRulesIteratorProvider() { const { backendFilter, frontendFilter } = getGrafanaFilter(filterState); const grafanaRulesGenerator: AsyncIterableX = from( - grafanaGroupsGenerator(groupLimit, backendFilter) + grafanaGroupsGenerator(options.grafanaManagedLimit, backendFilter) ).pipe( withAbort(abortController.signal), concatMap((groups) => @@ -110,7 +114,7 @@ export function useFilteredRulesIteratorProvider() { const dataSourceGenerators: Array> = externalRulesSourcesToFetchFrom.map( (dataSourceIdentifier) => { const promGroupsGenerator: AsyncIterableX = from( - prometheusGroupsGenerator(dataSourceIdentifier, groupLimit) + prometheusGroupsGenerator(dataSourceIdentifier, options.datasourceManagedLimit.groupLimit) ).pipe( withAbort(abortController.signal), concatMap((groups) => diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts new file mode 100644 index 00000000000..5ef8431aced --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts @@ -0,0 +1,181 @@ +import { testWithFeatureToggles } from 'test/test-utils'; + +import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; + +import { RuleHealth, RulesFilter } from '../search/rulesSearchParser'; +import { getFilter } from '../utils/search'; + +import { + FILTERED_GROUPS_LARGE_API_PAGE_SIZE, + FILTERED_GROUPS_SMALL_API_PAGE_SIZE, + RULE_LIMIT_WITH_BACKEND_FILTERS, + getFilteredRulesLimits, +} from './paginationLimits'; + +describe('paginationLimits', () => { + describe('getFilteredRulesLimits', () => { + describe('when backend filters are disabled', () => { + testWithFeatureToggles({ disable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] }); + + it('should return small limits when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])('should return small grafana limit + large datasource limit for backend-only filter: %p', (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + }); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { ruleType: PromRuleType.Alerting }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + { dashboardUid: 'test-dashboard' }, + { plugins: 'hide' as const }, + { namespace: 'production' }, + { groupName: 'test-group' }, + { namespace: 'production', freeFormWords: ['cpu'] }, + ])('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 alertingUIUseBackendFilters is enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters'] }); + + it('should return rule limit for grafana + default limit for datasource when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { ruleType: PromRuleType.Alerting }, + { dashboardUid: 'test-dashboard' }, + { groupName: 'test-group' }, + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])( + 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', + (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + } + ); + + it.each>([ + { namespace: 'production' }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + { 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', () => { + testWithFeatureToggles({ enable: ['alertingUIUseFullyCompatBackendFilters'] }); + + it('should return rule limit for grafana + default limit for datasource when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { ruleType: PromRuleType.Alerting }, + { dashboardUid: 'test-dashboard' }, + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])( + 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', + (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + } + ); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { groupName: 'test-group' }, + { namespace: 'production' }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + ])('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 both backend filter toggles are enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] }); + + it('should return rule limit for grafana + default limit for datasource when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { ruleType: PromRuleType.Alerting }, + { dashboardUid: 'test-dashboard' }, + { groupName: 'test-group' }, + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])( + 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', + (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + } + ); + + it.each>([ + { namespace: 'production' }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + ])('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 }); + }); + }); + }); +}); diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.ts index a8e03ac5a48..a5d359ded7b 100644 --- a/public/app/features/alerting/unified/rule-list/paginationLimits.ts +++ b/public/app/features/alerting/unified/rule-list/paginationLimits.ts @@ -1,3 +1,10 @@ +import { shouldUseBackendFilters, shouldUseFullyCompatibleBackendFilters } from '../featureToggles'; +import { RulesFilter } from '../search/rulesSearchParser'; + +import { hasDatasourceClientSideFilters } from './hooks/datasourceFilter'; +import { hasGrafanaClientSideFilters } from './hooks/grafanaFilter'; +import { FetchGroupsLimitOptions } from './hooks/prometheusGroupsGenerator'; + export const FRONTEND_LIST_PAGE_SIZE = 100; export const FILTERED_GROUPS_LARGE_API_PAGE_SIZE = 2000; @@ -6,6 +13,8 @@ export const FILTERED_GROUPS_SMALL_API_PAGE_SIZE = 100; export const DEFAULT_GROUPS_API_PAGE_SIZE = 40; export const FRONTED_GROUPED_PAGE_SIZE = DEFAULT_GROUPS_API_PAGE_SIZE; +export const RULE_LIMIT_WITH_BACKEND_FILTERS = 100; + export function getApiGroupPageSize(hasFilters: boolean) { return hasFilters ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE : DEFAULT_GROUPS_API_PAGE_SIZE; } @@ -13,3 +22,27 @@ export function getApiGroupPageSize(hasFilters: boolean) { export function getSearchApiGroupPageSize(hasFrontendFilters: boolean) { return hasFrontendFilters ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE : FILTERED_GROUPS_SMALL_API_PAGE_SIZE; } + +export function getFilteredRulesLimits(filterState: RulesFilter): FetchGroupsLimitOptions { + return { + grafanaManagedLimit: getGrafanaFilterLimits(filterState), + datasourceManagedLimit: { + groupLimit: hasDatasourceClientSideFilters(filterState) + ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE + : FILTERED_GROUPS_SMALL_API_PAGE_SIZE, + }, + }; +} + +function getGrafanaFilterLimits(filterState: RulesFilter) { + const backendFiltersEnabled = shouldUseFullyCompatibleBackendFilters() || shouldUseBackendFilters(); + + const frontendFiltersInUse = hasGrafanaClientSideFilters(filterState); + const onlyBackendFiltersInUse = frontendFiltersInUse === false; + + if (backendFiltersEnabled && onlyBackendFiltersInUse) { + return { ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }; + } + + return { groupLimit: getSearchApiGroupPageSize(frontendFiltersInUse) }; +}