diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 6a1f3dbd55a..9a8bc0a82bd 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -35,6 +35,7 @@ import { } from 'app/types/unified-alerting'; import { AlertQuery, + GrafanaAlertState, GrafanaAlertStateDecision, GrafanaRuleDefinition, PromAlertingRuleState, @@ -630,3 +631,7 @@ export function getCloudRule(override?: Partial) { ...override, }); } + +export function mockAlertWithState(state: GrafanaAlertState, labels?: {}): Alert { + return { activeAt: '', annotations: {}, labels: labels || {}, state: state, value: '' }; +} diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index b33c853f824..d6b42e78033 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -21,7 +21,6 @@ import alertDef from 'app/features/alerting/state/alertDef'; import { useCombinedRuleNamespaces } from 'app/features/alerting/unified/hooks/useCombinedRuleNamespaces'; import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector'; import { fetchAllPromAndRulerRulesAction } from 'app/features/alerting/unified/state/actions'; -import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; import { Annotation } from 'app/features/alerting/unified/utils/constants'; import { getAllRulesSourceNames, @@ -190,22 +189,6 @@ function filterRules(props: PanelProps, rules: Combined ); }); - if (options.alertInstanceLabelFilter) { - const replacedLabelFilter = replaceVariables(options.alertInstanceLabelFilter); - const matchers = parseMatchers(replacedLabelFilter); - - // Reduce rules and instances to only those that match - filteredRules = filteredRules.reduce((rules, rule) => { - const alertingRule = getAlertingRule(rule); - const filteredAlerts = (alertingRule?.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers)); - if (filteredAlerts.length) { - const alertRule: AlertingRule | null = getAlertingRule(rule); - alertRule && rules.push({ ...rule, promRule: { ...alertRule, alerts: filteredAlerts } }); - } - return rules; - }, []); - } - if (options.folder) { filteredRules = filteredRules.filter((rule) => { return rule.namespaceName === options.folder.title; diff --git a/public/app/plugins/panel/alertlist/util.test.tsx b/public/app/plugins/panel/alertlist/util.test.tsx new file mode 100644 index 00000000000..cf4f87a6ae5 --- /dev/null +++ b/public/app/plugins/panel/alertlist/util.test.tsx @@ -0,0 +1,73 @@ +import { mockAlertWithState as withState } from 'app/features/alerting/unified/mocks'; +import { Alert } from 'app/types/unified-alerting'; +import { GrafanaAlertState } from 'app/types/unified-alerting-dto'; + +import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types'; +import { filterAlerts } from './util'; + +const defaultOption: UnifiedAlertListOptions = { + maxItems: 2, + sortOrder: SortOrder.AlphaAsc, + dashboardAlerts: true, + groupMode: GroupMode.Default, + groupBy: [''], + alertName: 'test', + showInstances: false, + folder: { id: 1, title: 'test folder' }, + stateFilter: { firing: true, pending: true, noData: true, normal: true, error: true }, + alertInstanceLabelFilter: '', + datasource: 'Alertmanager', + viewMode: ViewMode.List, +}; + +const alerts: Alert[] = [ + withState(GrafanaAlertState.Pending, { severity: 'critical' }), + withState(GrafanaAlertState.Error, { severity: 'low' }), + withState(GrafanaAlertState.Error, { region: 'asia' }), + withState(GrafanaAlertState.Normal), +]; + +describe('filterAlerts', () => { + it('Returns all alert instances when there are no filters', () => { + const result = filterAlerts(defaultOption, alerts); + + expect(result.length).toBe(4); + }); + + it('Filters by alert instance state ', () => { + const noNormalStateOptions = { + ...defaultOption, + ...{ stateFilter: { firing: true, pending: true, noData: true, normal: false, error: true } }, + }; + + expect(filterAlerts(noNormalStateOptions, alerts).length).toBe(3); + + const noErrorOrNormalStateOptions = { + ...defaultOption, + ...{ stateFilter: { firing: true, pending: true, noData: true, normal: false, error: false } }, + }; + + expect(filterAlerts(noErrorOrNormalStateOptions, alerts).length).toBe(1); + }); + + it('Filters by alert instance label', () => { + const options = { + ...defaultOption, + ...{ alertInstanceLabelFilter: '{severity=low}' }, + }; + const result = filterAlerts(options, alerts); + + expect(result.length).toBe(1); + }); + + it('Filters by alert instance state and label', () => { + const options = { + ...defaultOption, + ...{ stateFilter: { firing: false, pending: false, noData: false, normal: false, error: true } }, + ...{ alertInstanceLabelFilter: '{severity=low}' }, + }; + const result = filterAlerts(options, alerts); + + expect(result.length).toBe(1); + }); +}); diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts index 370aacc7a17..258b4c519df 100644 --- a/public/app/plugins/panel/alertlist/util.ts +++ b/public/app/plugins/panel/alertlist/util.ts @@ -1,13 +1,21 @@ import { isEmpty } from 'lodash'; -import { PanelProps } from '@grafana/data'; +import { Labels, PanelProps } from '@grafana/data'; +import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; +import { replaceVariables } from 'app/plugins/datasource/prometheus/querybuilder/shared/parsingUtils'; import { Alert, hasAlertState } from 'app/types/unified-alerting'; import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { UnifiedAlertListOptions } from './types'; +function hasLabelFilter(alertInstanceLabelFilter: string, labels: Labels) { + const replacedLabelFilter = replaceVariables(alertInstanceLabelFilter); + const matchers = parseMatchers(replacedLabelFilter); + return labelsMatchMatchers(labels, matchers); +} + export function filterAlerts(options: PanelProps['options'], alerts: Alert[]): Alert[] { - const { stateFilter } = options; + const { stateFilter, alertInstanceLabelFilter } = options; if (isEmpty(stateFilter)) { return alerts; @@ -15,14 +23,16 @@ export function filterAlerts(options: PanelProps['optio return alerts.filter((alert) => { return ( - (stateFilter.firing && + ((stateFilter.firing && (hasAlertState(alert, GrafanaAlertState.Alerting) || hasAlertState(alert, PromAlertingRuleState.Firing))) || - (stateFilter.pending && - (hasAlertState(alert, GrafanaAlertState.Pending) || hasAlertState(alert, PromAlertingRuleState.Pending))) || - (stateFilter.noData && hasAlertState(alert, GrafanaAlertState.NoData)) || - (stateFilter.normal && hasAlertState(alert, GrafanaAlertState.Normal)) || - (stateFilter.error && hasAlertState(alert, GrafanaAlertState.Error)) || - (stateFilter.inactive && hasAlertState(alert, PromAlertingRuleState.Inactive)) + (stateFilter.pending && + (hasAlertState(alert, GrafanaAlertState.Pending) || hasAlertState(alert, PromAlertingRuleState.Pending))) || + (stateFilter.noData && hasAlertState(alert, GrafanaAlertState.NoData)) || + (stateFilter.normal && hasAlertState(alert, GrafanaAlertState.Normal)) || + (stateFilter.error && hasAlertState(alert, GrafanaAlertState.Error)) || + (stateFilter.inactive && hasAlertState(alert, PromAlertingRuleState.Inactive))) && + ((alertInstanceLabelFilter && hasLabelFilter(options.alertInstanceLabelFilter, alert.labels)) || + !alertInstanceLabelFilter) ); }); }