From fa21dba89e1d2e2cf950268b4bfacaf27d0f8e55 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Tue, 21 Mar 2023 13:36:06 +0100 Subject: [PATCH] Alerting: Fix notification policies matcher filter (#64967) (#65031) (cherry picked from commit bf3422740be959aa7b58ea848a0c725c36eece21) --- .../create-notification-policy.md | 17 ++++ .../features/alerting/unified/Analytics.ts | 1 + .../components/amroutes/AmRoutesTable.test.ts | 26 ++++- .../components/amroutes/AmRoutesTable.tsx | 9 +- .../components/amroutes/AmSpecificRouting.tsx | 4 +- .../amroutes/LabelMatcherFilter.tsx | 95 +++++++++++++++++++ 6 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 public/app/features/alerting/unified/components/amroutes/LabelMatcherFilter.tsx diff --git a/docs/sources/alerting/manage-notifications/create-notification-policy.md b/docs/sources/alerting/manage-notifications/create-notification-policy.md index f43045e858b..2dcf6dde19b 100644 --- a/docs/sources/alerting/manage-notifications/create-notification-policy.md +++ b/docs/sources/alerting/manage-notifications/create-notification-policy.md @@ -76,6 +76,23 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. Make any changes using instructions in [Add new specific policy](#add-new-specific-policy). 1. Click **Save policy**. +## Searching for policies + +Grafana allows you to search within the tree of policies by the following: + +- **Label matchers** +- **Contact Points** + +To search by contact point, simply enter a part or full name you are looking for. + +To search by label matchers simply enter a valid matcher in the **Search by matchers** input field. Multiple matchers can be combined with a comma (`,`). + +An example of a valid matchers search input is: + +`severity=high, region=~EMEA|NASA` + +> All matched policies will be **exact** matches, we currently do not support regex-style or partial matching. + ## Example An example of an alert configuration. diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 62eb16a5628..3bc1266cd0b 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -10,6 +10,7 @@ export const LogMessages = { clickingAlertStateFilters: 'clicking alert state filters', cancelSavingAlertRule: 'user canceled alert rule creation', successSavingAlertRule: 'alert rule saved successfully', + filterPoliciesByMatchers: 'filtering notification policies by matchers', }; // logInfo from '@grafana/runtime' should be used, but it doesn't handle Grafana JS Agent and Sentry correctly diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts index a0ced048f8f..3af98341c40 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts @@ -46,7 +46,7 @@ describe('getFilteredRoutes', () => { expect(filteredRoutes).toContain(routes[2]); }); - it('Should only return entries matching provided label query', () => { + it('Should only return entries matching provided matcher query', () => { // Arrange const routes: FormAmRoute[] = [ buildAmRoute({ id: '1' }), @@ -62,6 +62,28 @@ describe('getFilteredRoutes', () => { expect(filteredRoutes).toContain(routes[1]); }); + it('Should only return entries matching all provided matchers', () => { + // Arrange + const routes: FormAmRoute[] = [ + buildAmRoute({ id: '1' }), + buildAmRoute({ + id: '2', + object_matchers: [ + buildMatcher('severity', 'critical', MatcherOperator.regex), + buildMatcher('cloud', 'aws', MatcherOperator.regex), + ], + }), + buildAmRoute({ id: '3', object_matchers: [buildMatcher('severity', 'critical', MatcherOperator.regex)] }), + ]; + + // Act + const filteredRoutes = getFilteredRoutes(routes, 'severity=~critical, cloud=~aws', undefined); + + // Assert + expect(filteredRoutes).toHaveLength(1); + expect(filteredRoutes).toContain(routes[1]); + }); + it('Should only return entries matching provided contact query', () => { // Arrange const routes: FormAmRoute[] = [ @@ -78,7 +100,7 @@ describe('getFilteredRoutes', () => { expect(filteredRoutes).toContain(routes[1]); }); - it('Should only return entries matching provided label and contact query', () => { + it('Should only return entries matching provided matcher and contact query', () => { // Arrange const routes: FormAmRoute[] = [ buildAmRoute({ id: '1' }), diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx index 741d4f4a266..1a0b2060fc4 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx @@ -1,4 +1,4 @@ -import { intersectionWith, isEqual } from 'lodash'; +import { differenceWith, isEqual } from 'lodash'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Button, ConfirmModal, HorizontalGroup, IconButton } from '@grafana/ui'; @@ -30,14 +30,15 @@ type RouteTableColumnProps = DynamicTableColumnProps; type RouteTableItemProps = DynamicTableItemProps; export const getFilteredRoutes = (routes: FormAmRoute[], labelMatcherQuery?: string, contactPointQuery?: string) => { - const matchers = parseMatchers(labelMatcherQuery ?? ''); + const filterMatchers = parseMatchers(labelMatcherQuery ?? ''); let filteredRoutes = routes; - if (matchers.length) { + if (filterMatchers.length) { filteredRoutes = routes.filter((route) => { const routeMatchers = route.object_matchers.map(matcherFieldToMatcher); - return intersectionWith(routeMatchers, matchers, isEqual).length > 0; + // Route matchers needs to include all filter matchers + return differenceWith(filterMatchers, routeMatchers, isEqual).length === 0; }); } diff --git a/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx b/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx index bfee8e38bc6..04d59f0f2f8 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx @@ -14,9 +14,9 @@ import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes'; import { getNotificationPoliciesFilters } from '../../utils/misc'; import { EmptyArea } from '../EmptyArea'; import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA'; -import { MatcherFilter } from '../alert-groups/MatcherFilter'; import { AmRoutesTable } from './AmRoutesTable'; +import { LabelMatcherFilter } from './LabelMatcherFilter'; export interface AmSpecificRoutingProps { alertManagerSourceName: string; @@ -115,7 +115,7 @@ export const AmSpecificRouting: FC = ({
{!isAddMode && (
- setFilters((currentFilters) => ({ ...currentFilters, queryString: filter })) } diff --git a/public/app/features/alerting/unified/components/amroutes/LabelMatcherFilter.tsx b/public/app/features/alerting/unified/components/amroutes/LabelMatcherFilter.tsx new file mode 100644 index 00000000000..5a9f38d2714 --- /dev/null +++ b/public/app/features/alerting/unified/components/amroutes/LabelMatcherFilter.tsx @@ -0,0 +1,95 @@ +import { css } from '@emotion/css'; +import { debounce } from 'lodash'; +import React, { ChangeEvent, useEffect, useMemo } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Stack } from '@grafana/experimental'; +import { logInfo } from '@grafana/runtime'; +import { Label, Input, Icon, useStyles2 } from '@grafana/ui'; + +import { LogMessages } from '../../Analytics'; +import { HoverCard } from '../HoverCard'; + +interface Props { + className?: string; + defaultQueryString?: string; + onFilterChange: (filterString: string) => void; +} + +export const LabelMatcherFilter = ({ className, onFilterChange, defaultQueryString }: Props) => { + const styles = useStyles2(getStyles); + + const onSearchInputChanged = useMemo( + () => + debounce((e: ChangeEvent) => { + logInfo(LogMessages.filterPoliciesByMatchers); + onFilterChange(e.target.value); + }, 600), + [onFilterChange] + ); + + useEffect(() => onSearchInputChanged.cancel(), [onSearchInputChanged]); + + const searchIcon = ; + + return ( +
+
+ } + > + + + + + +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + icon: css` + margin-right: ${theme.spacing(0.5)}; + `, + inputWidth: css` + width: 340px; + flex-grow: 0; + `, + bold: css` + font-weight: ${theme.typography.fontWeightBold}; + `, + textBlock: css` + padding: ${theme.spacing(1, 0)}; + `, + hoverContent: css` + max-width: 600px; + `, +});