Alerting: Fix filtering when panel variables are in use (#66977)

Fix alert instances filtering when panel variables are in use
pull/67039/head
Konrad Lalik 2 years ago committed by GitHub
parent f3dbb7b34a
commit 9f1fe51edc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
  2. 78
      public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx
  3. 14
      public/app/plugins/panel/alertlist/util.ts

@ -106,6 +106,13 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
); );
} }
const { options, replaceVariables } = props;
const parsedOptions: UnifiedAlertListOptions = {
...props.options,
alertName: replaceVariables(options.alertName),
alertInstanceLabelFilter: replaceVariables(options.alertInstanceLabelFilter),
};
return ( return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%"> <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<div className={styles.container}> <div className={styles.container}>
@ -124,10 +131,10 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
/> />
)} )}
{props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Custom && haveResults && ( {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Custom && haveResults && (
<GroupedModeView rules={rules} options={props.options} /> <GroupedModeView rules={rules} options={parsedOptions} />
)} )}
{props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Default && haveResults && ( {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Default && haveResults && (
<UngroupedModeView rules={rules} options={props.options} /> <UngroupedModeView rules={rules} options={parsedOptions} />
)} )}
</section> </section>
</div> </div>
@ -209,7 +216,15 @@ function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: Combined
// when we display a rule with 0 instances // when we display a rule with 0 instances
filteredRules = filteredRules.reduce<CombinedRuleWithLocation[]>((rules, rule) => { filteredRules = filteredRules.reduce<CombinedRuleWithLocation[]>((rules, rule) => {
const alertingRule = getAlertingRule(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) { if (filteredAlerts.length) {
// We intentionally don't set alerts to filteredAlerts // We intentionally don't set alerts to filteredAlerts
// because later we couldn't display that some alerts are hidden (ref AlertInstances filtering) // because later we couldn't display that some alerts are hidden (ref AlertInstances filtering)

@ -1,14 +1,26 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { byRole, byText } from 'testing-library-selector';
import { getDefaultTimeRange, LoadingState, PanelProps, FieldConfigSource } from '@grafana/data'; import { getDefaultTimeRange, LoadingState, PanelProps, FieldConfigSource } from '@grafana/data';
import { TimeRangeUpdatedEvent } from '@grafana/runtime'; import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; 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 { UnifiedAlertList } from './UnifiedAlertList';
import { UnifiedAlertListOptions, SortOrder, GroupMode, ViewMode } from './types'; import { UnifiedAlertListOptions, SortOrder, GroupMode, ViewMode } from './types';
import * as utils from './util';
jest.mock('app/features/alerting/unified/api/alertmanager'); jest.mock('app/features/alerting/unified/api/alertmanager');
@ -60,14 +72,36 @@ const dashboard = {
}, },
}; };
const renderPanel = (options: UnifiedAlertListOptions = defaultOptions) => { const renderPanel = (options: Partial<UnifiedAlertListOptions> = defaultOptions) => {
const store = configureStore(); 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 }; const dashSrv: unknown = { getCurrent: () => dashboard };
setDashboardSrv(dashSrv as DashboardSrv); setDashboardSrv(dashSrv as DashboardSrv);
defaultProps.options = options; const props = { ...defaultProps, options: { ...defaultOptions, ...options } };
const props = { ...defaultProps };
return render( return render(
<Provider store={store}> <Provider store={store}>
@ -82,4 +116,38 @@ describe('UnifiedAlertList', () => {
expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1); expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1);
expect(dashboard.events.subscribe.mock.calls[0][0]).toEqual(TimeRangeUpdatedEvent); 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()
);
});
}); });

@ -1,20 +1,21 @@
import { isEmpty } from 'lodash'; 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 { 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 { Alert, hasAlertState } from 'app/types/unified-alerting';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { UnifiedAlertListOptions } from './types'; import { UnifiedAlertListOptions } from './types';
function hasLabelFilter(alertInstanceLabelFilter: string, labels: Labels) { function hasLabelFilter(alertInstanceLabelFilter: string, labels: Labels) {
const replacedLabelFilter = replaceVariables(alertInstanceLabelFilter); const matchers = parseMatchers(alertInstanceLabelFilter);
const matchers = parseMatchers(replacedLabelFilter);
return labelsMatchMatchers(labels, matchers); return labelsMatchMatchers(labels, matchers);
} }
export function filterAlerts(options: PanelProps<UnifiedAlertListOptions>['options'], alerts: Alert[]): Alert[] { export function filterAlerts(
options: Pick<UnifiedAlertListOptions, 'stateFilter' | 'alertInstanceLabelFilter'>,
alerts: Alert[]
): Alert[] {
const { stateFilter, alertInstanceLabelFilter } = options; const { stateFilter, alertInstanceLabelFilter } = options;
if (isEmpty(stateFilter)) { if (isEmpty(stateFilter)) {
@ -31,8 +32,7 @@ export function filterAlerts(options: PanelProps<UnifiedAlertListOptions>['optio
(stateFilter.normal && hasAlertState(alert, GrafanaAlertState.Normal)) || (stateFilter.normal && hasAlertState(alert, GrafanaAlertState.Normal)) ||
(stateFilter.error && hasAlertState(alert, GrafanaAlertState.Error)) || (stateFilter.error && hasAlertState(alert, GrafanaAlertState.Error)) ||
(stateFilter.inactive && hasAlertState(alert, PromAlertingRuleState.Inactive))) && (stateFilter.inactive && hasAlertState(alert, PromAlertingRuleState.Inactive))) &&
((alertInstanceLabelFilter && hasLabelFilter(options.alertInstanceLabelFilter, alert.labels)) || (alertInstanceLabelFilter ? hasLabelFilter(options.alertInstanceLabelFilter, alert.labels) : true)
!alertInstanceLabelFilter)
); );
}); });
} }

Loading…
Cancel
Save