diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index c87a92581a5..6e361744962 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -60,7 +60,6 @@ const ui = { matcherName: byPlaceholderText('label'), matcherValue: byPlaceholderText('value'), comment: byPlaceholderText('Details about the silence'), - createdBy: byPlaceholderText('User'), matcherOperatorSelect: byLabelText('operator'), matcherOperator: (operator: MatcherOperator) => byText(operator, { exact: true }), addMatcherButton: byRole('button', { name: 'Add matcher' }), @@ -210,6 +209,7 @@ describe('Silence edit', () => { const start = new Date(); const end = new Date(start.getTime() + 24 * 60 * 60 * 1000); + const now = dateTime().format('YYYY-MM-DD HH:mm'); const startDateString = dateTime(start).format('YYYY-MM-DD'); const endDateString = dateTime(end).format('YYYY-MM-DD'); @@ -247,17 +247,13 @@ describe('Silence edit', () => { userEvent.tab(); userEvent.type(ui.editor.matcherValue.getAll()[3], 'dev|staging'); - userEvent.type(ui.editor.comment.get(), 'Test'); - userEvent.type(ui.editor.createdBy.get(), 'Homer Simpson'); - userEvent.click(ui.editor.submit.get()); await waitFor(() => expect(mocks.api.createOrUpdateSilence).toHaveBeenCalledWith( 'grafana', expect.objectContaining({ - comment: 'Test', - createdBy: 'Homer Simpson', + comment: `created ${now}`, matchers: [ { isEqual: true, isRegex: false, name: 'foo', value: 'bar' }, { isEqual: false, isRegex: false, name: 'bar', value: 'buzz' }, diff --git a/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx b/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx new file mode 100644 index 00000000000..e09479f3d2a --- /dev/null +++ b/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useDebounce } from 'react-use'; +import { useDispatch } from 'react-redux'; +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Badge, useStyles2 } from '@grafana/ui'; +import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; +import { RuleState } from '../rules/RuleState'; +import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces'; +import { Annotation } from '../../utils/constants'; +import { findAlertRulesWithMatchers } from '../../utils/matchers'; +import { fetchAllPromAndRulerRulesAction } from '../../state/actions'; +import { CombinedRule } from 'app/types/unified-alerting'; +import { MatcherFieldValue, SilenceFormFields } from '../../types/silence-form'; + +type MatchedRulesTableItemProps = DynamicTableItemProps<{ + matchedRule: CombinedRule; +}>; +type MatchedRulesTableColumnProps = DynamicTableColumnProps<{ matchedRule: CombinedRule }>; + +export const MatchedSilencedRules = () => { + const [matchedAlertRules, setMatchedAlertRules] = useState([]); + const formApi = useFormContext(); + const dispatch = useDispatch(); + const { watch } = formApi; + const matchers: MatcherFieldValue[] = watch('matchers'); + const styles = useStyles2(getStyles); + const columns = useColumns(); + + useEffect(() => { + dispatch(fetchAllPromAndRulerRulesAction()); + }, [dispatch]); + + const combinedNamespaces = useCombinedRuleNamespaces(); + useDebounce( + () => { + const matchedRules = combinedNamespaces.flatMap((namespace) => { + return namespace.groups.flatMap((group) => { + return findAlertRulesWithMatchers(group.rules, matchers); + }); + }); + setMatchedAlertRules(matchedRules); + }, + 500, + [combinedNamespaces, matchers] + ); + + return ( +
+

+ Affected alerts + {matchedAlertRules.length > 0 ? ( + + ) : null} +

+
+ {matchers.every((matcher) => !matcher.value && !matcher.name) ? ( + Add a valid matcher to see affected alerts + ) : ( + <> + + {matchedAlertRules.length > 5 && ( +
and {matchedAlertRules.length - 5} more
+ )} + + )} +
+
+ ); +}; + +function useColumns(): MatchedRulesTableColumnProps[] { + return [ + { + id: 'state', + label: 'State', + renderCell: function renderStateTag({ data: { matchedRule } }) { + return ; + }, + size: '160px', + }, + { + id: 'name', + label: 'Name', + renderCell: function renderName({ data: { matchedRule } }) { + return matchedRule.name; + }, + size: '250px', + }, + { + id: 'summary', + label: 'Summary', + renderCell: function renderSummary({ data: { matchedRule } }) { + return matchedRule.annotations[Annotation.summary] ?? ''; + }, + size: '400px', + }, + ]; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + table: css` + max-width: ${theme.breakpoints.values.lg}px; + `, + moreMatches: css` + margin-top: ${theme.spacing(1)}; + `, + title: css` + display: flex; + align-items: center; + `, + badge: css` + margin-left: ${theme.spacing(1)}; + `, +}); diff --git a/public/app/features/alerting/unified/components/silences/MatchersField.tsx b/public/app/features/alerting/unified/components/silences/MatchersField.tsx index 96ee773c510..7464af9ef2f 100644 --- a/public/app/features/alerting/unified/components/silences/MatchersField.tsx +++ b/public/app/features/alerting/unified/components/silences/MatchersField.tsx @@ -128,7 +128,7 @@ const getStyles = (theme: GrafanaTheme2) => { min-width: 140px; `, matchers: css` - max-width: 585px; + max-width: ${theme.breakpoints.values.sm}px; margin: ${theme.spacing(1)} 0; padding-top: ${theme.spacing(0.5)}; `, diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index e3d7b46499e..dd2bcdadf64 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -14,6 +14,7 @@ import { useDebounce } from 'react-use'; import { config } from '@grafana/runtime'; import { pickBy } from 'lodash'; import MatchersField from './MatchersField'; +import { MatchedSilencedRules } from './MatchedSilencedRules'; import { useForm, FormProvider } from 'react-hook-form'; import { SilenceFormFields } from '../../types/silence-form'; import { useDispatch } from 'react-redux'; @@ -79,7 +80,7 @@ const getDefaultFormValues = (searchParams: URLSearchParams, silence?: Silence): id: '', startsAt: now.toISOString(), endsAt: endsAt.toISOString(), - comment: '', + comment: `created ${dateTime().format('YYYY-MM-DD HH:mm')}`, createdBy: config.bootData.user.name, duration: '2h', isRegex: false, @@ -164,7 +165,7 @@ export const SilencesEditor: FC = ({ silence, alertManagerSourceName }) =
-
+
= ({ silence, alertManagerSourceName }) = >