= ({ silence, className, silencedAlerts, alertM
{status.state}
|
- {matchers?.map(({ name, value, isRegex }) => {
- return ;
- })}
+
|
{silencedAlerts.length} |
@@ -56,13 +54,17 @@ const SilenceTableRow: FC = ({ silence, className, silencedAlerts, alertM
|
{status.state === 'expired' ? (
- Recreate
+
+ Recreate
+
) : (
Unsilence
)}
-
+ {status.state !== 'expired' && (
+
+ )}
|
{!isCollapsed && (
diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
new file mode 100644
index 00000000000..0b57a5bf015
--- /dev/null
+++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
@@ -0,0 +1,156 @@
+import { Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
+import React, { FC } from 'react';
+import { Alert, Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles } from '@grafana/ui';
+import { DefaultTimeZone, GrafanaTheme } from '@grafana/data';
+import { config } from '@grafana/runtime';
+import { pickBy } from 'lodash';
+import MatchersField from './MatchersField';
+import { useForm, FormProvider } from 'react-hook-form';
+import { SilenceFormFields } from '../../types/silence-form';
+import { useDispatch } from 'react-redux';
+import { createOrUpdateSilenceAction } from '../../state/actions';
+import { SilencePeriod } from './SilencePeriod';
+import { css, cx } from '@emotion/css';
+import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
+import { makeAMLink } from '../../utils/misc';
+
+interface Props {
+ silence?: Silence;
+ alertManagerSourceName: string;
+}
+
+const getDefaultFormValues = (silence?: Silence): SilenceFormFields => {
+ if (silence) {
+ return {
+ id: silence.id,
+ startsAt: new Date().toISOString(),
+ endsAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(), // Default time period is now + 2h
+ comment: silence.comment,
+ createdBy: silence.createdBy,
+ duration: `2h`,
+ isRegex: false,
+ matchers: silence.matchers || [],
+ matcherName: '',
+ matcherValue: '',
+ timeZone: DefaultTimeZone,
+ };
+ } else {
+ return {
+ id: '',
+ startsAt: new Date().toISOString(),
+ endsAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(), // Default time period is now + 2h
+ comment: '',
+ createdBy: config.bootData.user.name,
+ duration: '2h',
+ isRegex: false,
+ matchers: [{ name: '', value: '', isRegex: false }],
+ matcherName: '',
+ matcherValue: '',
+ timeZone: DefaultTimeZone,
+ };
+ }
+};
+
+export const SilencesEditor: FC = ({ silence, alertManagerSourceName }) => {
+ const formAPI = useForm({ defaultValues: getDefaultFormValues(silence) });
+ const dispatch = useDispatch();
+ const styles = useStyles(getStyles);
+
+ const { loading, error } = useUnifiedAlertingSelector((state) => state.updateSilence);
+
+ const { register, handleSubmit, formState } = formAPI;
+
+ const onSubmit = (data: SilenceFormFields) => {
+ const { id, startsAt, endsAt, comment, createdBy, matchers } = data;
+ const payload = pickBy(
+ {
+ id,
+ startsAt,
+ endsAt,
+ comment,
+ createdBy,
+ matchers,
+ },
+ (value) => !!value
+ ) as SilenceCreatePayload;
+ dispatch(
+ createOrUpdateSilenceAction({
+ alertManagerSourceName,
+ payload,
+ exitOnSave: true,
+ successMessage: `Silence ${payload.id ? 'updated' : 'created'}`,
+ })
+ );
+ };
+ return (
+
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme) => ({
+ field: css`
+ margin: ${theme.spacing.sm} 0;
+ `,
+ textArea: css`
+ width: 600px;
+ `,
+ createdBy: css`
+ width: 200px;
+ `,
+ flexRow: css`
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+
+ & > * {
+ margin-right: ${theme.spacing.sm};
+ }
+ `,
+});
+
+export default SilencesEditor;
diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
index 020a7f0039c..98ea0337136 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
@@ -1,74 +1,117 @@
import React, { FC } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2 } from '@grafana/ui';
+import { Icon, useStyles2, Link, Button, Field } from '@grafana/ui';
import { css } from '@emotion/css';
import { AlertmanagerAlert, Silence } from 'app/plugins/datasource/alertmanager/types';
import SilenceTableRow from './SilenceTableRow';
import { getAlertTableStyles } from '../../styles/table';
import { NoSilencesSplash } from './NoSilencesCTA';
-
+import { AlertManagerPicker } from '../AlertManagerPicker';
+import { makeAMLink } from '../../utils/misc';
interface Props {
silences: Silence[];
alertManagerAlerts: AlertmanagerAlert[];
alertManagerSourceName: string;
+ setAlertManagerSourceName(name: string): void;
}
-const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSourceName }) => {
+const SilencesTable: FC = ({
+ silences,
+ alertManagerAlerts,
+ alertManagerSourceName,
+ setAlertManagerSourceName,
+}) => {
const styles = useStyles2(getStyles);
const tableStyles = useStyles2(getAlertTableStyles);
const findSilencedAlerts = (id: string) => {
return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id));
};
- if (!!silences.length) {
- return (
-
-
-
-
-
-
-
-
-
-
-
- |
- State |
- Matchers |
- Alerts |
- Schedule |
- Action |
-
-
-
- {silences.map((silence, index) => {
- const silencedAlerts = findSilencedAlerts(silence.id);
- return (
-
- );
- })}
-
-
- );
- } else {
- return ;
- }
+
+ return (
+ <>
+
+
+
+ {!!silences.length && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ State |
+ Matchers |
+ Alerts |
+ Schedule |
+ Action |
+
+
+
+ {silences.map((silence, index) => {
+ const silencedAlerts = findSilencedAlerts(silence.id);
+ return (
+
+ );
+ })}
+
+
+
+
+ Expired silences are automatically deleted after 5 days.
+
+ >
+ )}
+ {!silences.length && }
+ >
+ );
};
const getStyles = (theme: GrafanaTheme2) => ({
+ addNewSilence: css`
+ margin-bottom: ${theme.spacing(1)};
+ `,
colState: css`
width: 110px;
`,
colMatchers: css`
width: 50%;
`,
+ callout: css`
+ background-color: ${theme.colors.background.secondary};
+ border-top: 3px solid ${theme.colors.info.border};
+ border-radius: 2px;
+ height: 62px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-top: ${theme.spacing(2)};
+
+ & > * {
+ margin-left: ${theme.spacing(1)};
+ }
+ `,
+ calloutIcon: css`
+ color: ${theme.colors.info.text};
+ `,
});
export default SilencesTable;
diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts
index 592eb13208c..b7586dbb9f1 100644
--- a/public/app/features/alerting/unified/state/actions.ts
+++ b/public/app/features/alerting/unified/state/actions.ts
@@ -2,7 +2,12 @@ import { AppEvents } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { appEvents } from 'app/core/core';
-import { AlertmanagerAlert, AlertManagerCortexConfig, Silence } from 'app/plugins/datasource/alertmanager/types';
+import {
+ AlertmanagerAlert,
+ AlertManagerCortexConfig,
+ Silence,
+ SilenceCreatePayload,
+} from 'app/plugins/datasource/alertmanager/types';
import { NotifierDTO, ThunkResult } from 'app/types';
import { RuleIdentifier, RuleNamespace, RuleWithLocation } from 'app/types/unified-alerting';
import {
@@ -17,6 +22,7 @@ import {
fetchAlertManagerConfig,
fetchAlerts,
fetchSilences,
+ createOrUpdateSilence,
updateAlertmanagerConfig,
} from '../api/alertmanager';
import { fetchRules } from '../api/prometheus';
@@ -366,3 +372,26 @@ export const expireSilenceAction = (alertManagerSourceName: string, silenceId: s
dispatch(fetchAmAlertsAction(alertManagerSourceName));
};
};
+
+type UpdateSilenceActionOptions = {
+ alertManagerSourceName: string;
+ payload: SilenceCreatePayload;
+ exitOnSave: boolean;
+ successMessage?: string;
+};
+
+export const createOrUpdateSilenceAction = createAsyncThunk(
+ 'unifiedalerting/updateSilence',
+ ({ alertManagerSourceName, payload, exitOnSave, successMessage }): Promise =>
+ withSerializedError(
+ (async () => {
+ await createOrUpdateSilence(alertManagerSourceName, payload);
+ if (successMessage) {
+ appEvents.emit(AppEvents.alertSuccess, [successMessage]);
+ }
+ if (exitOnSave) {
+ locationService.push('/alerting/silences');
+ }
+ })()
+ )
+);
diff --git a/public/app/features/alerting/unified/state/reducers.ts b/public/app/features/alerting/unified/state/reducers.ts
index 1967064859f..73d8554105f 100644
--- a/public/app/features/alerting/unified/state/reducers.ts
+++ b/public/app/features/alerting/unified/state/reducers.ts
@@ -10,6 +10,7 @@ import {
fetchSilencesAction,
saveRuleFormAction,
updateAlertManagerConfigAction,
+ createOrUpdateSilenceAction,
} from './actions';
export const reducer = combineReducers({
@@ -28,6 +29,7 @@ export const reducer = combineReducers({
}),
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
+ updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer,
amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName)
.reducer,
});
diff --git a/public/app/features/alerting/unified/types/silence-form.ts b/public/app/features/alerting/unified/types/silence-form.ts
new file mode 100644
index 00000000000..cefd466dbab
--- /dev/null
+++ b/public/app/features/alerting/unified/types/silence-form.ts
@@ -0,0 +1,16 @@
+import { SilenceMatcher } from 'app/plugins/datasource/alertmanager/types';
+import { TimeZone } from '@grafana/data';
+
+export type SilenceFormFields = {
+ id: string;
+ startsAt: string;
+ endsAt: string;
+ timeZone: TimeZone;
+ duration: string;
+ comment: string;
+ matchers: SilenceMatcher[];
+ createdBy: string;
+ matcherName: string;
+ matcherValue: string;
+ isRegex: boolean;
+};
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index 04c2190235a..72c2130957a 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -370,6 +370,18 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
),
},
+ {
+ path: '/alerting/silence/new',
+ component: SafeDynamicImport(
+ () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
+ ),
+ },
+ {
+ path: '/alerting/silence/:id/edit',
+ component: SafeDynamicImport(
+ () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
+ ),
+ },
{
path: '/alerting/notifications',
component: SafeDynamicImport(