Alerting: Support rule title search on the backend (#113738)
This commit is contained in:
@@ -345,6 +345,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingProvenanceLockWrites?: boolean;
|
||||
/**
|
||||
* Enables the UI to use certain backend-side filters
|
||||
*/
|
||||
alertingUIUseBackendFilters?: boolean;
|
||||
/**
|
||||
* Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.
|
||||
*/
|
||||
alertmanagerRemotePrimary?: boolean;
|
||||
|
||||
@@ -575,6 +575,14 @@ var (
|
||||
HideFromAdminPage: true,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "alertingUIUseBackendFilters",
|
||||
Description: "Enables the UI to use certain backend-side filters",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
HideFromAdminPage: true,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "alertmanagerRemotePrimary",
|
||||
Description: "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.",
|
||||
|
||||
Generated
+1
@@ -76,6 +76,7 @@ cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-e
|
||||
addFieldFromCalculationStatFunctions,GA,@grafana/datapro,false,false,true
|
||||
alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingProvenanceLockWrites,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingUIUseBackendFilters,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false
|
||||
annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false
|
||||
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
|
||||
|
||||
|
Generated
+4
@@ -315,6 +315,10 @@ const (
|
||||
// Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL
|
||||
FlagAlertingProvenanceLockWrites = "alertingProvenanceLockWrites"
|
||||
|
||||
// FlagAlertingUIUseBackendFilters
|
||||
// Enables the UI to use certain backend-side filters
|
||||
FlagAlertingUIUseBackendFilters = "alertingUIUseBackendFilters"
|
||||
|
||||
// FlagAlertmanagerRemotePrimary
|
||||
// Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.
|
||||
FlagAlertmanagerRemotePrimary = "alertmanagerRemotePrimary"
|
||||
|
||||
+14
@@ -614,6 +614,20 @@
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingUIUseBackendFilters",
|
||||
"resourceVersion": "1762966218072",
|
||||
"creationTimestamp": "2025-11-12T16:50:18Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the UI to use certain backend-side filters",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingUseNewSimplifiedRoutingHashAlgorithm",
|
||||
|
||||
@@ -479,6 +479,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleGroups := opts.Query["rule_group"]
|
||||
|
||||
receiverName := opts.Query.Get("receiver_name")
|
||||
title := opts.Query.Get("search.rule_name")
|
||||
|
||||
maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1)
|
||||
nextToken := opts.Query.Get("group_next_token")
|
||||
@@ -495,6 +496,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
PanelID: panelID,
|
||||
RuleGroups: ruleGroups,
|
||||
ReceiverName: receiverName,
|
||||
SearchTitle: title,
|
||||
},
|
||||
Limit: maxGroups,
|
||||
ContinueToken: nextToken,
|
||||
@@ -624,6 +626,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
|
||||
ruleGroups := opts.Query["rule_group"]
|
||||
|
||||
receiverName := opts.Query.Get("receiver_name")
|
||||
title := opts.Query.Get("search.rule_name")
|
||||
|
||||
alertRuleQuery := ngmodels.ListAlertRulesQuery{
|
||||
OrgID: opts.OrgID,
|
||||
@@ -632,6 +635,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
|
||||
PanelID: panelID,
|
||||
RuleGroups: ruleGroups,
|
||||
ReceiverName: receiverName,
|
||||
SearchTitle: title,
|
||||
}
|
||||
ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
|
||||
if err != nil {
|
||||
|
||||
@@ -978,6 +978,9 @@ type ListAlertRulesQuery struct {
|
||||
|
||||
ReceiverName string
|
||||
TimeIntervalName string
|
||||
// SearchTitle allows searching for alert rules that contain
|
||||
// the given string in their title (case insensitive)
|
||||
SearchTitle string
|
||||
|
||||
HasPrometheusRuleDefinition *bool
|
||||
}
|
||||
|
||||
@@ -855,6 +855,16 @@ func (st DBstore) buildListAlertRulesQuery(sess *db.Session, query *ngmodels.Lis
|
||||
}
|
||||
}
|
||||
|
||||
if query.SearchTitle != "" {
|
||||
words := strings.Fields(query.SearchTitle)
|
||||
if len(words) > 0 {
|
||||
// Build sequential pattern: %word1%word2%word3%
|
||||
pattern := strings.Join(words, "%")
|
||||
sql, param := st.SQLStore.GetDialect().LikeOperator("title", true, pattern, true)
|
||||
q = q.And(sql, param)
|
||||
}
|
||||
}
|
||||
|
||||
if query.HasPrometheusRuleDefinition != nil {
|
||||
q, err = st.filterWithPrometheusRuleDefinition(*query.HasPrometheusRuleDefinition, q)
|
||||
if err != nil {
|
||||
|
||||
@@ -2020,12 +2020,10 @@ func Benchmark_ListAlertRules(b *testing.B) {
|
||||
func TestIntegration_ListAlertRules(t *testing.T) {
|
||||
tutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{
|
||||
BaseInterval: time.Duration(rand.Int64N(100)) * time.Second,
|
||||
}
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
orgID := int64(1)
|
||||
ruleGen := models.RuleGen
|
||||
@@ -2034,6 +2032,8 @@ func TestIntegration_ListAlertRules(t *testing.T) {
|
||||
ruleGen.WithOrgID(orgID),
|
||||
)
|
||||
t.Run("filter by HasPrometheusRuleDefinition", func(t *testing.T) {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
|
||||
regularRule := createRule(t, store, ruleGen)
|
||||
importedRule := createRule(t, store, ruleGen.With(
|
||||
@@ -2072,6 +2072,75 @@ func TestIntegration_ListAlertRules(t *testing.T) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("filter by SearchTitle", func(t *testing.T) {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
|
||||
rule1 := createRule(t, store, ruleGen.With(models.RuleMuts.WithTitle("CPU Usage Alert")))
|
||||
rule2 := createRule(t, store, ruleGen.With(models.RuleMuts.WithTitle("Memory Usage Alert")))
|
||||
rule3 := createRule(t, store, ruleGen.With(models.RuleMuts.WithTitle("Disk Space Alert")))
|
||||
rule4 := createRule(t, store, ruleGen.With(models.RuleMuts.WithTitle("Application Error Rate")))
|
||||
|
||||
tc := []struct {
|
||||
name string
|
||||
titleSearch string
|
||||
expectedRules []*models.AlertRule
|
||||
}{
|
||||
{
|
||||
name: "should find rules",
|
||||
titleSearch: "alert",
|
||||
expectedRules: []*models.AlertRule{rule1, rule2, rule3},
|
||||
},
|
||||
{
|
||||
name: "should find rule with partial match",
|
||||
titleSearch: "aPpl",
|
||||
expectedRules: []*models.AlertRule{rule4},
|
||||
},
|
||||
{
|
||||
name: "should return no rules when no match",
|
||||
titleSearch: "nonexistent",
|
||||
expectedRules: []*models.AlertRule{},
|
||||
},
|
||||
{
|
||||
name: "should return all rules when empty",
|
||||
titleSearch: "",
|
||||
expectedRules: []*models.AlertRule{rule1, rule2, rule3, rule4},
|
||||
},
|
||||
{
|
||||
name: "should not find rules when word order is reversed",
|
||||
titleSearch: "usage cpu",
|
||||
expectedRules: []*models.AlertRule{},
|
||||
},
|
||||
{
|
||||
name: "should find multiple rules matching sequential words",
|
||||
titleSearch: "usage alert",
|
||||
expectedRules: []*models.AlertRule{rule1, rule2},
|
||||
},
|
||||
{
|
||||
name: "should handle extra whitespace between words",
|
||||
titleSearch: " cpu usage ",
|
||||
expectedRules: []*models.AlertRule{rule1},
|
||||
},
|
||||
{
|
||||
name: "should handle multiple words with partial matches",
|
||||
titleSearch: "aPp erR",
|
||||
expectedRules: []*models.AlertRule{rule4},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query := &models.ListAlertRulesQuery{
|
||||
OrgID: orgID,
|
||||
SearchTitle: tt.titleSearch,
|
||||
}
|
||||
result, err := store.ListAlertRules(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, tt.expectedRules, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_ListAlertRulesPaginated(t *testing.T) {
|
||||
|
||||
@@ -42,6 +42,7 @@ type GrafanaPromRulesOptions = Omit<PromRulesOptions, 'ruleSource' | 'namespace'
|
||||
contactPoint?: string;
|
||||
health?: RuleHealth[];
|
||||
state?: PromAlertingRuleState[];
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const prometheusApi = alertingApi.injectEndpoints({
|
||||
@@ -91,6 +92,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
groupLimit,
|
||||
limitAlerts,
|
||||
groupNextToken,
|
||||
title,
|
||||
}) => ({
|
||||
url: `api/prometheus/grafana/api/v1/rules`,
|
||||
params: {
|
||||
@@ -103,6 +105,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
limit_alerts: limitAlerts,
|
||||
group_limit: groupLimit?.toFixed(0),
|
||||
group_next_token: groupNextToken,
|
||||
'search.rule_name': title,
|
||||
},
|
||||
}),
|
||||
providesTags: (_result, _error, { folderUid, groupName, ruleName }) => {
|
||||
|
||||
@@ -21,3 +21,5 @@ export const shouldAllowRecoveringDeletedRules = () =>
|
||||
|
||||
export const shouldAllowPermanentlyDeletingRules = () =>
|
||||
(shouldAllowRecoveringDeletedRules() && config.featureToggles.alertingRulePermanentlyDelete) ?? false;
|
||||
|
||||
export const shouldUseBackendFilters = () => config.featureToggles.alertingUIUseBackendFilters ?? false;
|
||||
|
||||
@@ -20,9 +20,10 @@ import {
|
||||
GrafanaRuleWithOrigin,
|
||||
PromRuleWithOrigin,
|
||||
RuleWithOrigin,
|
||||
hasClientSideFilters,
|
||||
useFilteredRulesIteratorProvider,
|
||||
} from './hooks/useFilteredRulesIterator';
|
||||
import { FRONTEND_LIST_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits';
|
||||
import { FRONTEND_LIST_PAGE_SIZE, getSearchApiGroupPageSize } from './paginationLimits';
|
||||
|
||||
interface FilterViewProps {
|
||||
filterState: RulesFilter;
|
||||
@@ -77,7 +78,10 @@ function FilterViewResults({ filterState }: FilterViewProps) {
|
||||
* ⚠️ Make sure we are returning / using a "iterator" and not an "iterable" since the iterable is only a blueprint
|
||||
* and the iterator will allow us to exhaust the iterable in a stateful way
|
||||
*/
|
||||
const { iterable, abortController } = getFilteredRulesIterator(filterState, getApiGroupPageSize(true));
|
||||
const { iterable, abortController } = getFilteredRulesIterator(
|
||||
filterState,
|
||||
getSearchApiGroupPageSize(hasClientSideFilters(filterState))
|
||||
);
|
||||
const rulesBatchIterator = iterable
|
||||
.pipe(
|
||||
bufferCountOrTime(FRONTEND_LIST_PAGE_SIZE, 1000),
|
||||
|
||||
@@ -60,6 +60,34 @@ describe('ruleFilter', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('should perform title filtering when backendFiltered is false', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
it('should perform title filtering when backendFiltered is not specified (backward compatibility)', () => {
|
||||
const rule = mockPromAlertingRule({ name: 'High CPU Usage' });
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by labels', () => {
|
||||
const rule = mockPromAlertingRule({
|
||||
labels: { severity: 'critical', team: 'ops' },
|
||||
|
||||
@@ -36,18 +36,20 @@ 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) {
|
||||
export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter, backendFiltered?: boolean) {
|
||||
const { name, labels = {}, health, type } = rule;
|
||||
|
||||
if (filterState.freeFormWords.length > 0) {
|
||||
if (filterState.freeFormWords.length > 0 && !backendFiltered) {
|
||||
const nameMatches = fuzzyMatches(name, filterState.freeFormWords.join(' '));
|
||||
if (!nameMatches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterState.ruleName && !fuzzyMatches(name, filterState.ruleName)) {
|
||||
// Rule name search: Backend-supported for backend-filtered rules, client-side otherwise
|
||||
if (filterState.ruleName && !backendFiltered && !fuzzyMatches(name, filterState.ruleName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ interface GrafanaPromApiFilter {
|
||||
state?: PromAlertingRuleState[];
|
||||
health?: RuleHealth[];
|
||||
contactPoint?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface GrafanaFetchGroupsOptions extends FetchGroupsOptions {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { RuleSource } from '../../search/rulesSearchParser';
|
||||
import { getFilter } from '../../utils/search';
|
||||
|
||||
import { buildTitleSearch, 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 title search filters (backend-supported)', () => {
|
||||
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false);
|
||||
expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).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({ 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 disabled', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.alertingUIUseBackendFilters = false;
|
||||
});
|
||||
|
||||
it('should return true for title search filters (client-side fallback)', () => {
|
||||
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true);
|
||||
expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).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 return true for title search filters (backward compatibility)', () => {
|
||||
// Default behavior should be client-side filtering
|
||||
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true);
|
||||
expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTitleSearch', () => {
|
||||
it('returns undefined when no search terms are provided', () => {
|
||||
expect(buildTitleSearch(getFilter({}))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the rule name when only a rule search is provided', () => {
|
||||
expect(buildTitleSearch(getFilter({ ruleName: 'cpu' }))).toBe('cpu');
|
||||
});
|
||||
|
||||
it('returns joined free-form words when only text search is provided', () => {
|
||||
expect(buildTitleSearch(getFilter({ freeFormWords: ['cpu', 'memory'] }))).toBe('cpu memory');
|
||||
});
|
||||
|
||||
it('concatenates rule search and free-form text when both are provided', () => {
|
||||
expect(buildTitleSearch(getFilter({ ruleName: 'cpu', freeFormWords: ['memory', 'latency'] }))).toBe(
|
||||
'cpu memory latency'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
PromRuleGroupDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { shouldUseBackendFilters } from '../../featureToggles';
|
||||
import { RuleSource, RulesFilter } from '../../search/rulesSearchParser';
|
||||
import {
|
||||
getDataSourceByUid,
|
||||
@@ -79,12 +80,16 @@ export function useFilteredRulesIteratorProvider() {
|
||||
|
||||
const normalizedFilterState = normalizeFilterState(filterState);
|
||||
const hasDataSourceFilterActive = Boolean(filterState.dataSourceNames.length);
|
||||
const useBackendFilters = shouldUseBackendFilters();
|
||||
|
||||
const titleSearch = useBackendFilters ? buildTitleSearch(filterState) : undefined;
|
||||
|
||||
const grafanaRulesGenerator: AsyncIterableX<RuleWithOrigin> = from(
|
||||
grafanaGroupsGenerator(groupLimit, {
|
||||
contactPoint: filterState.contactPoint ?? undefined,
|
||||
health: filterState.ruleHealth ? [filterState.ruleHealth] : [],
|
||||
state: filterState.ruleState ? [filterState.ruleState] : [],
|
||||
title: titleSearch,
|
||||
})
|
||||
).pipe(
|
||||
withAbort(abortController.signal),
|
||||
@@ -92,7 +97,7 @@ export function useFilteredRulesIteratorProvider() {
|
||||
groups
|
||||
.filter((group) => groupFilter(group, normalizedFilterState))
|
||||
.flatMap((group) => group.rules.map((rule) => ({ group, rule })))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState, useBackendFilters))
|
||||
.map(({ group, rule }) => mapGrafanaRuleToRuleWithOrigin(group, rule))
|
||||
),
|
||||
catchError(() => empty())
|
||||
@@ -117,7 +122,7 @@ export function useFilteredRulesIteratorProvider() {
|
||||
groups
|
||||
.filter((group) => groupFilter(group, normalizedFilterState))
|
||||
.flatMap((group) => group.rules.map((rule, index) => ({ group, rule, index })))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState, false))
|
||||
.map(({ group, rule, index }) => mapRuleToRuleWithOrigin(dataSourceIdentifier, group, rule, index))
|
||||
),
|
||||
catchError(() => empty())
|
||||
@@ -146,6 +151,48 @@ 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 needs client-side filtering
|
||||
(!useBackendFilters && (filterState.freeFormWords.length > 0 || Boolean(filterState.ruleName))) ||
|
||||
// Client-side only filters:
|
||||
Boolean(filterState.namespace) ||
|
||||
filterState.dataSourceNames.length > 0 ||
|
||||
filterState.labels.length > 0 ||
|
||||
Boolean(filterState.dashboardUid) ||
|
||||
filterState.ruleSource === RuleSource.DataSource
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export const FRONTEND_LIST_PAGE_SIZE = 100;
|
||||
|
||||
export const FILTERED_GROUPS_API_PAGE_SIZE = 2000;
|
||||
export const FILTERED_GROUPS_LARGE_API_PAGE_SIZE = 2000;
|
||||
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 function getApiGroupPageSize(hasFilters: boolean) {
|
||||
return hasFilters ? FILTERED_GROUPS_API_PAGE_SIZE : DEFAULT_GROUPS_API_PAGE_SIZE;
|
||||
return hasFilters ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE : DEFAULT_GROUPS_API_PAGE_SIZE;
|
||||
}
|
||||
|
||||
export function getSearchApiGroupPageSize(hasFrontendFilters: boolean) {
|
||||
return hasFrontendFilters ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE : FILTERED_GROUPS_SMALL_API_PAGE_SIZE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user