= ({ silence, className, silencedAlerts, alertM
|
- {silencedAlerts.length} |
+ {silencedAlerts.length} |
{startsAtDate?.format(dateDisplayFormat)} {'-'}
diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
index e5a46123ca1..c49ac6fe094 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
@@ -1,15 +1,15 @@
import { MatcherOperator, Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
import React, { FC, useMemo, useState } from 'react';
-import { Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles } from '@grafana/ui';
+import { Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles2 } from '@grafana/ui';
import {
DefaultTimeZone,
- GrafanaTheme,
parseDuration,
intervalToAbbreviatedDurationString,
addDurationToDate,
dateTime,
isValidDate,
UrlQueryMap,
+ GrafanaTheme2,
} from '@grafana/data';
import { useDebounce } from 'react-use';
import { config } from '@grafana/runtime';
@@ -99,7 +99,7 @@ export const SilencesEditor: FC = ({ silence, alertManagerSourceName }) =
const defaultValues = useMemo(() => getDefaultFormValues(queryParams, silence), [silence, queryParams]);
const formAPI = useForm({ defaultValues });
const dispatch = useDispatch();
- const styles = useStyles(getStyles);
+ const styles = useStyles2(getStyles);
const { loading } = useUnifiedAlertingSelector((state) => state.updateSilence);
@@ -196,7 +196,10 @@ export const SilencesEditor: FC = ({ silence, alertManagerSourceName }) =
error={formState.errors.comment?.message}
invalid={!!formState.errors.comment}
>
-
+
= ({ silence, alertManagerSourceName }) =
error={formState.errors.createdBy?.message}
invalid={!!formState.errors.createdBy}
>
-
+
@@ -228,9 +234,9 @@ export const SilencesEditor: FC = ({ silence, alertManagerSourceName }) =
);
};
-const getStyles = (theme: GrafanaTheme) => ({
+const getStyles = (theme: GrafanaTheme2) => ({
field: css`
- margin: ${theme.spacing.sm} 0;
+ margin: ${theme.spacing(1, 0)};
`,
textArea: css`
width: 600px;
@@ -244,7 +250,7 @@ const getStyles = (theme: GrafanaTheme) => ({
justify-content: flex-start;
& > * {
- margin-right: ${theme.spacing.sm};
+ margin-right: ${theme.spacing(1)};
}
`,
});
diff --git a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
new file mode 100644
index 00000000000..13442048a43
--- /dev/null
+++ b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
@@ -0,0 +1,109 @@
+import React, { FormEvent, useState } from 'react';
+import { css } from '@emotion/css';
+import { Label, Icon, Input, Tooltip, RadioButtonGroup, useStyles2, Button, Field } from '@grafana/ui';
+import { GrafanaTheme2, SelectableValue } from '@grafana/data';
+import { useQueryParams } from 'app/core/hooks/useQueryParams';
+import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
+import { SilenceState } from 'app/plugins/datasource/alertmanager/types';
+import { parseMatchers } from '../../utils/alertmanager';
+import { debounce } from 'lodash';
+
+const stateOptions: SelectableValue[] = Object.entries(SilenceState).map(([key, value]) => ({
+ label: key,
+ value,
+}));
+
+export const SilencesFilter = () => {
+ const [queryStringKey, setQueryStringKey] = useState(`queryString-${Math.random() * 100}`);
+ const [queryParams, setQueryParams] = useQueryParams();
+ const { queryString, silenceState } = getSilenceFiltersFromUrlParams(queryParams);
+ const styles = useStyles2(getStyles);
+
+ const handleQueryStringChange = debounce((e: FormEvent) => {
+ const target = e.target as HTMLInputElement;
+ setQueryParams({ queryString: target.value || null });
+ }, 400);
+
+ const handleSilenceStateChange = (state: string) => {
+ setQueryParams({ silenceState: state });
+ };
+
+ const clearFilters = () => {
+ setQueryParams({
+ queryString: null,
+ silenceState: null,
+ });
+ setTimeout(() => setQueryStringKey(''));
+ };
+
+ const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
+
+ return (
+
+
+
+ Filter silences by matchers using a comma separated list of matchers, ie:
+ {`severity=critical, instance=~cluster-us-.+`}
+
+ }
+ >
+
+ {' '}
+ Search by matchers
+
+ }
+ invalid={inputInvalid}
+ error={inputInvalid ? 'Query must use valid matcher syntax' : null}
+ >
+ }
+ onChange={handleQueryStringChange}
+ defaultValue={queryString ?? ''}
+ placeholder="Search"
+ data-testid="search-query-input"
+ />
+
+
+
+
+
+
+ {(queryString || silenceState) && (
+
+
+
+ )}
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ searchInput: css`
+ width: 360px;
+ `,
+ flexRow: css`
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ padding-bottom: ${theme.spacing(2)};
+ border-bottom: 1px solid ${theme.colors.border.strong};
+ `,
+ rowChild: css`
+ margin-right: ${theme.spacing(1)};
+ margin-bottom: 0;
+ max-height: 52px;
+ `,
+ fieldLabel: css`
+ font-size: 12px;
+ font-weight: 500;
+ `,
+});
diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
index 4832d5ffa89..e8fafe6f1c8 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
@@ -2,13 +2,15 @@ import React, { FC, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, useStyles2, Link, Button } from '@grafana/ui';
import { css } from '@emotion/css';
-import { AlertmanagerAlert, Silence } from 'app/plugins/datasource/alertmanager/types';
+import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types';
import SilenceTableRow from './SilenceTableRow';
import { getAlertTableStyles } from '../../styles/table';
import { NoSilencesSplash } from './NoSilencesCTA';
-import { makeAMLink } from '../../utils/misc';
+import { getFiltersFromUrlParams, makeAMLink } from '../../utils/misc';
import { contextSrv } from 'app/core/services/context_srv';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
+import { SilencesFilter } from './SilencesFilter';
+import { parseMatchers } from '../../utils/alertmanager';
interface Props {
silences: Silence[];
alertManagerAlerts: AlertmanagerAlert[];
@@ -19,23 +21,22 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo
const styles = useStyles2(getStyles);
const tableStyles = useStyles2(getAlertTableStyles);
const [queryParams] = useQueryParams();
+ const filteredSilences = useFilteredSilences(silences);
- const filteredSilences = useMemo(() => {
- const silenceIdsString = queryParams?.silenceIds;
- if (typeof silenceIdsString === 'string') {
- return silences.filter((silence) => silenceIdsString.split(',').includes(silence.id));
- }
- return silences;
- }, [queryParams, silences]);
+ const { silenceState } = getFiltersFromUrlParams(queryParams);
+
+ const showExpiredSilencesBanner =
+ !!filteredSilences.length && (silenceState === undefined || silenceState === SilenceState.Expired);
const findSilencedAlerts = (id: string) => {
return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id));
};
return (
- <>
+
{!!silences.length && (
<>
+
{contextSrv.isEditor && (
@@ -45,51 +46,99 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo
)}
-
-
-
-
-
-
-
- {contextSrv.isEditor && }
-
-
-
- |
- State |
- Matching labels |
- Alerts |
- Schedule |
- {contextSrv.isEditor && Action | }
-
-
-
- {filteredSilences.map((silence, index) => {
- const silencedAlerts = findSilencedAlerts(silence.id);
- return (
-
- );
- })}
-
-
-
-
- Expired silences are automatically deleted after 5 days.
-
+ {!!filteredSilences.length ? (
+
+
+
+
+
+
+
+ {contextSrv.isEditor && }
+
+
+
+ |
+ State |
+ Matching labels |
+ Alerts |
+ Schedule |
+ {contextSrv.isEditor && Action | }
+
+
+
+ {filteredSilences.map((silence, index) => {
+ const silencedAlerts = findSilencedAlerts(silence.id);
+ return (
+
+ );
+ })}
+
+
+ ) : (
+
+
+ No silences match your filters
+
+ )}
+
+ {showExpiredSilencesBanner && (
+
+
+ Expired silences are automatically deleted after 5 days.
+
+ )}
>
)}
{!silences.length && }
- >
+
);
};
+const useFilteredSilences = (silences: Silence[]) => {
+ const [queryParams] = useQueryParams();
+ return useMemo(() => {
+ const { queryString, silenceState } = getFiltersFromUrlParams(queryParams);
+ const silenceIdsString = queryParams?.silenceIds;
+ return silences.filter((silence) => {
+ if (typeof silenceIdsString === 'string') {
+ const idsIncluded = silenceIdsString.split(',').includes(silence.id);
+ if (!idsIncluded) {
+ return false;
+ }
+ }
+ if (queryString) {
+ const matchers = parseMatchers(queryString);
+ const matchersMatch = matchers.every((matcher) =>
+ silence.matchers?.some(
+ ({ name, value, isEqual, isRegex }) =>
+ matcher.name === name &&
+ matcher.value === value &&
+ matcher.isEqual === isEqual &&
+ matcher.isRegex === isRegex
+ )
+ );
+ if (!matchersMatch) {
+ return false;
+ }
+ }
+ if (silenceState) {
+ const stateMatches = silence.status.state === silenceState;
+ if (!stateMatches) {
+ return false;
+ }
+ }
+ return true;
+ });
+ }, [queryParams, silences]);
+};
+
const getStyles = (theme: GrafanaTheme2) => ({
topButtonContainer: css`
display: flex;
@@ -97,7 +146,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
justify-content: flex-end;
`,
addNewSilence: css`
- margin-bottom: ${theme.spacing(1)};
+ margin: ${theme.spacing(2, 0)};
`,
colState: css`
width: 110px;
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 98bb65c6d93..9a87a2ab177 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -11,7 +11,7 @@ import {
} from 'app/types/unified-alerting-dto';
import { AlertingRule, Alert, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting';
import DatasourceSrv from 'app/features/plugins/datasource_srv';
-import { DataSourceSrv, GetDataSourceListFilters } from '@grafana/runtime';
+import { DataSourceSrv, GetDataSourceListFilters, config } from '@grafana/runtime';
import {
AlertmanagerAlert,
AlertManagerCortexConfig,
@@ -19,6 +19,8 @@ import {
AlertmanagerStatus,
AlertState,
GrafanaManagedReceiverConfig,
+ Silence,
+ SilenceState,
} from 'app/plugins/datasource/alertmanager/types';
let nextDataSourceId = 1;
@@ -204,6 +206,21 @@ export const mockAlertGroup = (partial: Partial = {}): Alertm
};
};
+export const mockSilence = (partial: Partial = {}): Silence => {
+ return {
+ id: '1a2b3c4d5e6f',
+ matchers: [{ name: 'foo', value: 'bar', isEqual: true, isRegex: false }],
+ startsAt: new Date().toISOString(),
+ endsAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
+ updatedAt: new Date().toISOString(),
+ createdBy: config.bootData.user.name || 'admin',
+ comment: 'Silence noisy alerts',
+ status: {
+ state: SilenceState.Active,
+ },
+ ...partial,
+ };
+};
export class MockDataSourceSrv implements DataSourceSrv {
datasources: Record = {};
// @ts-ignore
diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts
index e8dd42c4b00..803d34c1348 100644
--- a/public/app/features/alerting/unified/utils/misc.ts
+++ b/public/app/features/alerting/unified/utils/misc.ts
@@ -1,6 +1,6 @@
import { urlUtil, UrlQueryMap } from '@grafana/data';
import { config } from '@grafana/runtime';
-import { CombinedRule, FilterState, RulesSource } from 'app/types/unified-alerting';
+import { CombinedRule, FilterState, RulesSource, SilenceFilterState } from 'app/types/unified-alerting';
import { ALERTMANAGER_NAME_QUERY_KEY } from './constants';
import { getRulesSourceName } from './datasource';
import * as ruleId from './rule-id';
@@ -38,7 +38,15 @@ export const getFiltersFromUrlParams = (queryParams: UrlQueryMap): FilterState =
const alertState = queryParams['alertState'] === undefined ? undefined : String(queryParams['alertState']);
const dataSource = queryParams['dataSource'] === undefined ? undefined : String(queryParams['dataSource']);
const groupBy = queryParams['groupBy'] === undefined ? undefined : String(queryParams['groupBy']).split(',');
- return { queryString, alertState, dataSource, groupBy };
+ const silenceState = queryParams['silenceState'] === undefined ? undefined : String(queryParams['silenceState']);
+ return { queryString, alertState, dataSource, groupBy, silenceState };
+};
+
+export const getSilenceFiltersFromUrlParams = (queryParams: UrlQueryMap): SilenceFilterState => {
+ const queryString = queryParams['queryString'] === undefined ? undefined : String(queryParams['queryString']);
+ const silenceState = queryParams['silenceState'] === undefined ? undefined : String(queryParams['silenceState']);
+
+ return { queryString, silenceState };
};
export function recordToArray(record: Record): Array<{ key: string; value: string }> {
diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts
index b5d98cf7ac2..c937ff4b8f7 100644
--- a/public/app/types/unified-alerting.ts
+++ b/public/app/types/unified-alerting.ts
@@ -133,4 +133,10 @@ export interface FilterState {
dataSource?: string;
alertState?: string;
groupBy?: string[];
+ silenceState?: string;
+}
+
+export interface SilenceFilterState {
+ queryString?: string;
+ silenceState?: string;
}
|