(cherry picked from commit bf3422740b)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -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<FormAmRoute>;
|
||||
type RouteTableItemProps = DynamicTableItemProps<FormAmRoute>;
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AmSpecificRoutingProps> = ({
|
||||
<div>
|
||||
{!isAddMode && (
|
||||
<div className={styles.searchContainer}>
|
||||
<MatcherFilter
|
||||
<LabelMatcherFilter
|
||||
onFilterChange={(filter) =>
|
||||
setFilters((currentFilters) => ({ ...currentFilters, queryString: filter }))
|
||||
}
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
logInfo(LogMessages.filterPoliciesByMatchers);
|
||||
onFilterChange(e.target.value);
|
||||
}, 600),
|
||||
[onFilterChange]
|
||||
);
|
||||
|
||||
useEffect(() => onSearchInputChanged.cancel(), [onSearchInputChanged]);
|
||||
|
||||
const searchIcon = <Icon name={'search'} />;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Label>
|
||||
<Stack gap={0.5}>
|
||||
<span>Search by matcher</span>
|
||||
<HoverCard
|
||||
content={
|
||||
<div className={styles.hoverContent}>
|
||||
Filter notification policies by <span className={styles.bold}>matchers</span>
|
||||
<div className={styles.textBlock}>
|
||||
Notification policies are characterized by labels matchers rathen than labels. <br />
|
||||
Filtering by matchers means we compare if matchers are equal contrary to checking if a label matches a
|
||||
matcher.
|
||||
</div>
|
||||
<div className={styles.textBlock}>
|
||||
According to that, e.g. <code>severity=critical</code> equals only <code>severity=critical</code>
|
||||
<br />
|
||||
and <span className={styles.bold}>is not equal</span> <code>severity=~critical</code>
|
||||
<br />
|
||||
This is an important distinction from how filtering of alert rules works
|
||||
</div>
|
||||
<hr />
|
||||
Filter policies using matchers querying, e.g.:
|
||||
<pre>{`{severity="critical", instance=~"cluster-us-.+"}`}</pre>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon className={styles.icon} name="info-circle" size="sm" />
|
||||
</HoverCard>
|
||||
</Stack>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
defaultValue={defaultQueryString}
|
||||
onChange={onSearchInputChanged}
|
||||
data-testid="search-query-input"
|
||||
prefix={searchIcon}
|
||||
className={styles.inputWidth}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
`,
|
||||
});
|
||||
Reference in New Issue
Block a user