From 9f1fe51edc778f9d67d2acbad498196ff4f5ae9c Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 21 Apr 2023 12:07:06 +0200 Subject: [PATCH] Alerting: Fix filtering when panel variables are in use (#66977) Fix alert instances filtering when panel variables are in use --- .../panel/alertlist/UnifiedAlertList.tsx | 21 ++++- .../panel/alertlist/UnifiedalertList.test.tsx | 78 +++++++++++++++++-- public/app/plugins/panel/alertlist/util.ts | 14 ++-- 3 files changed, 98 insertions(+), 15 deletions(-) diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index d6b42e78033..dbfed7f94a9 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -106,6 +106,13 @@ export function UnifiedAlertList(props: PanelProps) { ); } + const { options, replaceVariables } = props; + const parsedOptions: UnifiedAlertListOptions = { + ...props.options, + alertName: replaceVariables(options.alertName), + alertInstanceLabelFilter: replaceVariables(options.alertInstanceLabelFilter), + }; + return (
@@ -124,10 +131,10 @@ export function UnifiedAlertList(props: PanelProps) { /> )} {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Custom && haveResults && ( - + )} {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Default && haveResults && ( - + )}
@@ -209,7 +216,15 @@ function filterRules(props: PanelProps, rules: Combined // when we display a rule with 0 instances filteredRules = filteredRules.reduce((rules, rule) => { const alertingRule = getAlertingRule(rule); - const filteredAlerts = alertingRule ? filterAlerts(options, alertingRule.alerts ?? []) : []; + const filteredAlerts = alertingRule + ? filterAlerts( + { + stateFilter: options.stateFilter, + alertInstanceLabelFilter: replaceVariables(options.alertInstanceLabelFilter), + }, + alertingRule.alerts ?? [] + ) + : []; if (filteredAlerts.length) { // We intentionally don't set alerts to filteredAlerts // because later we couldn't display that some alerts are hidden (ref AlertInstances filtering) diff --git a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx index ed2a5cc6a94..2ae4648c602 100644 --- a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx @@ -1,14 +1,26 @@ import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; +import { byRole, byText } from 'testing-library-selector'; import { getDefaultTimeRange, LoadingState, PanelProps, FieldConfigSource } from '@grafana/data'; import { TimeRangeUpdatedEvent } from '@grafana/runtime'; import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { configureStore } from 'app/store/configureStore'; + +import { contextSrv } from '../../../core/services/context_srv'; +import { + mockPromAlert, + mockPromAlertingRule, + mockPromRuleGroup, + mockPromRuleNamespace, + mockUnifiedAlertingStore, +} from '../../../features/alerting/unified/mocks'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../../features/alerting/unified/utils/datasource'; import { UnifiedAlertList } from './UnifiedAlertList'; import { UnifiedAlertListOptions, SortOrder, GroupMode, ViewMode } from './types'; +import * as utils from './util'; jest.mock('app/features/alerting/unified/api/alertmanager'); @@ -60,14 +72,36 @@ const dashboard = { }, }; -const renderPanel = (options: UnifiedAlertListOptions = defaultOptions) => { - const store = configureStore(); +const renderPanel = (options: Partial = defaultOptions) => { + const store = mockUnifiedAlertingStore({ + promRules: { + grafana: { + loading: false, + dispatched: true, + result: [ + mockPromRuleNamespace({ + name: 'ns1', + groups: [ + mockPromRuleGroup({ + name: 'group1', + rules: [ + mockPromAlertingRule({ + name: 'rule1', + alerts: [mockPromAlert({ labels: { severity: 'critical' } })], + }), + ], + }), + ], + }), + ], + }, + }, + }); const dashSrv: unknown = { getCurrent: () => dashboard }; setDashboardSrv(dashSrv as DashboardSrv); - defaultProps.options = options; - const props = { ...defaultProps }; + const props = { ...defaultProps, options: { ...defaultOptions, ...options } }; return render( @@ -82,4 +116,38 @@ describe('UnifiedAlertList', () => { expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1); expect(dashboard.events.subscribe.mock.calls[0][0]).toEqual(TimeRangeUpdatedEvent); }); + + it('should replace option variables before filtering', async () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + const filterAlertsSpy = jest.spyOn(utils, 'filterAlerts'); + + const replaceVarsSpy = jest.spyOn(defaultProps, 'replaceVariables').mockReturnValue('severity=critical'); + + const user = userEvent.setup(); + + renderPanel({ + alertInstanceLabelFilter: '$label', + dashboardAlerts: false, + alertName: '', + datasource: GRAFANA_RULES_SOURCE_NAME, + folder: undefined, + }); + + expect(byText('rule1').get()).toBeInTheDocument(); + + const expandElement = byText('1 instance').get(); + + await user.click(expandElement); + + const tagsElement = await byRole('list', { name: 'Tags' }).find(); + expect(await byRole('listitem').find(tagsElement)).toHaveTextContent('severity=critical'); + + expect(replaceVarsSpy).toHaveBeenLastCalledWith('$label'); + expect(filterAlertsSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + alertInstanceLabelFilter: 'severity=critical', + }), + expect.anything() + ); + }); }); diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts index 258b4c519df..47479c7004e 100644 --- a/public/app/plugins/panel/alertlist/util.ts +++ b/public/app/plugins/panel/alertlist/util.ts @@ -1,20 +1,21 @@ import { isEmpty } from 'lodash'; -import { Labels, PanelProps } from '@grafana/data'; +import { Labels } 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); + const matchers = parseMatchers(alertInstanceLabelFilter); return labelsMatchMatchers(labels, matchers); } -export function filterAlerts(options: PanelProps['options'], alerts: Alert[]): Alert[] { +export function filterAlerts( + options: Pick, + alerts: Alert[] +): Alert[] { const { stateFilter, alertInstanceLabelFilter } = options; if (isEmpty(stateFilter)) { @@ -31,8 +32,7 @@ export function filterAlerts(options: PanelProps['optio (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) + (alertInstanceLabelFilter ? hasLabelFilter(options.alertInstanceLabelFilter, alert.labels) : true) ); }); }