Alerting: Improve test notification visualization (#113228)
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
/** @deprecated To be deleted - use alertingApiServer API instead */
|
||||
|
||||
import { ContactPointsState } from 'app/features/alerting/unified/types/alerting';
|
||||
import { Receiver, TestReceiversAlert, TestReceiversResult } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { CONTACT_POINTS_STATE_INTERVAL_MS } from '../utils/constants';
|
||||
import { getDatasourceAPIUid } from '../utils/datasource';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
import { fetchContactPointsState } from './grafana';
|
||||
@@ -19,9 +21,34 @@ export const receiversApi = alertingApi.injectEndpoints({
|
||||
}
|
||||
},
|
||||
}),
|
||||
testIntegration: build.mutation<TestReceiversResult, TestReceiversOptions>({
|
||||
query: ({ alertManagerSourceName, receivers, alert }) => ({
|
||||
method: 'POST',
|
||||
data: {
|
||||
receivers,
|
||||
alert,
|
||||
},
|
||||
url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/config/api/v1/receivers/test`,
|
||||
showErrorAlert: false,
|
||||
showSuccessAlert: false,
|
||||
}),
|
||||
transformResponse: (response: TestReceiversResult) => {
|
||||
// Check if the response contains errors even though the HTTP status was 200
|
||||
if (receiversResponseContainsErrors(response)) {
|
||||
throw new Error(getReceiverResultError(response));
|
||||
}
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
interface TestReceiversOptions {
|
||||
alertManagerSourceName: string;
|
||||
receivers: Receiver[];
|
||||
alert?: TestReceiversAlert;
|
||||
}
|
||||
|
||||
export const useGetContactPointsState = (alertManagerSourceName: string) => {
|
||||
const contactPointsStateEmpty: ContactPointsState = { receivers: {}, errorCount: 0 };
|
||||
const { currentData: contactPointsState } = receiversApi.useContactPointsStateQuery(
|
||||
@@ -33,3 +60,22 @@ export const useGetContactPointsState = (alertManagerSourceName: string) => {
|
||||
);
|
||||
return contactPointsState ?? contactPointsStateEmpty;
|
||||
};
|
||||
|
||||
export const { useTestIntegrationMutation } = receiversApi;
|
||||
|
||||
// Helper functions for checking receiver test results
|
||||
function receiversResponseContainsErrors(result: TestReceiversResult): boolean {
|
||||
return result.receivers.some((receiver) =>
|
||||
receiver.grafana_managed_receiver_configs.some((config) => config.status === 'failed')
|
||||
);
|
||||
}
|
||||
|
||||
function getReceiverResultError(receiversResult: TestReceiversResult): string {
|
||||
return receiversResult.receivers
|
||||
.flatMap((receiver) =>
|
||||
receiver.grafana_managed_receiver_configs
|
||||
.filter((config) => config.status === 'failed')
|
||||
.map((config) => config.error ?? 'Unknown error.')
|
||||
)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Button, Field, Select, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { NotificationChannelOption } from 'app/features/alerting/unified/types/alerting';
|
||||
|
||||
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
||||
import {
|
||||
ChannelValues,
|
||||
CloudChannelValues,
|
||||
@@ -66,7 +65,6 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
|
||||
const selectedType = watch(typeFieldPath) ?? defaultValues.type;
|
||||
const parse_mode = watch(`${settingsFieldPath}.parse_mode`);
|
||||
const { loading: testingReceiver } = useUnifiedAlertingSelector((state) => state.testReceivers);
|
||||
|
||||
// TODO I don't like integration specific code here but other ways require a bigger refactoring
|
||||
const onCallIntegrationType = watch(`${settingsFieldPath}.integration_type`);
|
||||
@@ -232,14 +230,7 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
{isTestable && onTest && isTestAvailable && (
|
||||
<Button
|
||||
disabled={testingReceiver}
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => handleTest()}
|
||||
icon={testingReceiver ? 'spinner' : 'message'}
|
||||
>
|
||||
<Button size="xs" variant="secondary" type="button" onClick={() => handleTest()} icon="message">
|
||||
<Trans i18nKey="alerting.channel-sub-form.test">Test</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
+19
-29
@@ -13,12 +13,10 @@ import { canEditEntity } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import {
|
||||
GrafanaManagedContactPoint,
|
||||
GrafanaManagedReceiverConfig,
|
||||
TestReceiversAlert,
|
||||
Receiver,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { alertmanagerApi } from '../../../api/alertmanagerApi';
|
||||
import { testReceiversAction } from '../../../state/actions';
|
||||
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
||||
import {
|
||||
formChannelValuesToGrafanaChannelConfig,
|
||||
@@ -52,7 +50,6 @@ interface Props {
|
||||
const { useGrafanaNotifiersQuery } = alertmanagerApi;
|
||||
|
||||
export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [createContactPoint] = useCreateContactPoint({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
});
|
||||
@@ -71,7 +68,7 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
|
||||
const { data: grafanaNotifiers = [], isLoading: isLoadingNotifiers } = useGrafanaNotifiersQuery();
|
||||
|
||||
const [testChannelValues, setTestChannelValues] = useState<GrafanaChannelValues>();
|
||||
const [testReceivers, setTestReceivers] = useState<Receiver[]>();
|
||||
|
||||
// transform receiver DTO to form values
|
||||
const [existingValue, id2original] = useMemo((): [
|
||||
@@ -112,27 +109,17 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
};
|
||||
|
||||
const onTestChannel = (values: GrafanaChannelValues) => {
|
||||
setTestChannelValues(values);
|
||||
};
|
||||
const existing: GrafanaManagedReceiverConfig | undefined = id2original[values.__id];
|
||||
const chan = formChannelValuesToGrafanaChannelConfig(values, defaultChannelValues, 'test', existing);
|
||||
|
||||
const testNotification = (alert?: TestReceiversAlert) => {
|
||||
if (testChannelValues) {
|
||||
const existing: GrafanaManagedReceiverConfig | undefined = id2original[testChannelValues.__id];
|
||||
const chan = formChannelValuesToGrafanaChannelConfig(testChannelValues, defaultChannelValues, 'test', existing);
|
||||
const receivers: Receiver[] = [
|
||||
{
|
||||
name: 'test',
|
||||
grafana_managed_receiver_configs: [chan],
|
||||
},
|
||||
];
|
||||
|
||||
const payload = {
|
||||
alertManagerSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
receivers: [
|
||||
{
|
||||
name: 'test',
|
||||
grafana_managed_receiver_configs: [chan],
|
||||
},
|
||||
],
|
||||
alert,
|
||||
};
|
||||
|
||||
dispatch(testReceiversAction(payload));
|
||||
}
|
||||
setTestReceivers(receivers);
|
||||
};
|
||||
|
||||
// If there is no contact point it means we're creating a new one, so scoped permissions doesn't exist yet
|
||||
@@ -192,11 +179,14 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
editMode && contactPoint && showManageContactPointPermissions(GRAFANA_RULES_SOURCE_NAME, contactPoint)
|
||||
}
|
||||
/>
|
||||
<TestContactPointModal
|
||||
onDismiss={() => setTestChannelValues(undefined)}
|
||||
isOpen={!!testChannelValues}
|
||||
onTest={(alert) => testNotification(alert)}
|
||||
/>
|
||||
{testReceivers && (
|
||||
<TestContactPointModal
|
||||
onDismiss={() => setTestReceivers(undefined)}
|
||||
isOpen={!!testReceivers}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
receivers={testReceivers}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+32
-10
@@ -4,18 +4,21 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { Alert, Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Receiver, TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { Annotations, Labels } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { useTestIntegrationMutation } from '../../../api/receiversApi';
|
||||
import { defaultAnnotations } from '../../../utils/constants';
|
||||
import { stringifyErrorLike } from '../../../utils/misc';
|
||||
import AnnotationsStep from '../../rule-editor/AnnotationsStep';
|
||||
import LabelsField from '../../rule-editor/labels/LabelsField';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onDismiss: () => void;
|
||||
onTest: (alert?: TestReceiversAlert) => void;
|
||||
alertManagerSourceName: string;
|
||||
receivers: Receiver[];
|
||||
}
|
||||
|
||||
type AnnoField = {
|
||||
@@ -40,14 +43,17 @@ const defaultValues: FormFields = {
|
||||
labels: [{ key: '', value: '' }],
|
||||
};
|
||||
|
||||
export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
|
||||
export const TestContactPointModal = ({ isOpen, onDismiss, alertManagerSourceName, receivers }: Props) => {
|
||||
const [notificationType, setNotificationType] = useState<NotificationType>(NotificationType.predefined);
|
||||
const styles = useStyles2(getStyles);
|
||||
const formMethods = useForm<FormFields>({ defaultValues, mode: 'onBlur' });
|
||||
const [testIntegration, { isLoading, error, isSuccess }] = useTestIntegrationMutation();
|
||||
|
||||
const onSubmit = async (data: FormFields) => {
|
||||
let alert: TestReceiversAlert | undefined;
|
||||
|
||||
const onSubmit = (data: FormFields) => {
|
||||
if (notificationType === NotificationType.custom) {
|
||||
const alert = {
|
||||
alert = {
|
||||
annotations: data.annotations
|
||||
.filter(({ key, value }) => !!key && !!value)
|
||||
.reduce<Annotations>((acc, { key, value }) => {
|
||||
@@ -59,10 +65,13 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
|
||||
return { ...acc, [key]: value };
|
||||
}, {}),
|
||||
};
|
||||
onTest(alert);
|
||||
} else {
|
||||
onTest();
|
||||
}
|
||||
|
||||
await testIntegration({
|
||||
alertManagerSourceName,
|
||||
receivers,
|
||||
alert,
|
||||
}).unwrap();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -71,6 +80,19 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
|
||||
isOpen={isOpen}
|
||||
title={t('alerting.test-contact-point-modal.title-test-contact-point', 'Test contact point')}
|
||||
>
|
||||
{Boolean(error) && (
|
||||
<Alert title={t('alerting.test-contact-point-modal.test-failed', 'Test notification failed')} severity="error">
|
||||
{stringifyErrorLike(error)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
<Alert
|
||||
title={t('alerting.test-contact-point-modal.test-successful', 'Test notification sent successfully')}
|
||||
severity="success"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={styles.section}>
|
||||
<Label>
|
||||
<Trans i18nKey="alerting.test-contact-point-modal.notification-message">Notification message</Trans>
|
||||
@@ -110,7 +132,7 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
|
||||
)}
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button type="submit">
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
<Trans i18nKey="alerting.test-contact-point-modal.send-test-notification">Send test notification</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
|
||||
import { MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER } from 'app/features/alerting/unified/mocks/server/handlers/datasources';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { AlertManagerCortexConfig, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AlertManagerCortexConfig, AlertState, TestReceiversPayload } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
export const grafanaAlertingConfigurationStatusHandler = (
|
||||
response = defaultGrafanaAlertingConfigurationStatusResponse
|
||||
@@ -160,9 +160,24 @@ const getReceiversHandler = () =>
|
||||
});
|
||||
|
||||
const testReceiversHandler = () =>
|
||||
http.post('/api/alertmanager/grafana/config/api/v1/receivers/test', () => {
|
||||
// TODO: scaffold out response as needed by tests
|
||||
return HttpResponse.json({});
|
||||
http.post('/api/alertmanager/grafana/config/api/v1/receivers/test', async ({ request }) => {
|
||||
const body: TestReceiversPayload = await request.clone().json();
|
||||
const { receivers = [] } = body;
|
||||
|
||||
// Build response with successful test results for each receiver
|
||||
const testResults = receivers.map((receiver) => ({
|
||||
name: receiver.name,
|
||||
grafana_managed_receiver_configs: (receiver.grafana_managed_receiver_configs || []).map((config) => ({
|
||||
name: config.name || config.type,
|
||||
uid: config.uid,
|
||||
status: 'ok' as const,
|
||||
})),
|
||||
}));
|
||||
|
||||
return HttpResponse.json({
|
||||
notified_at: new Date().toISOString(),
|
||||
receivers: testResults,
|
||||
});
|
||||
});
|
||||
|
||||
const getGroupsHandler = () =>
|
||||
|
||||
@@ -2,24 +2,13 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { locationService, logMeasurement } from '@grafana/runtime';
|
||||
import {
|
||||
AlertManagerCortexConfig,
|
||||
AlertmanagerGroup,
|
||||
Matcher,
|
||||
Receiver,
|
||||
TestReceiversAlert,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AlertManagerCortexConfig, AlertmanagerGroup, Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ThunkResult } from 'app/types/store';
|
||||
import { RuleIdentifier, RuleNamespace, StateHistoryItem } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { withPromRulesMetadataLogging, withRulerRulesMetadataLogging } from '../Analytics';
|
||||
import {
|
||||
deleteAlertManagerConfig,
|
||||
fetchAlertGroups,
|
||||
testReceivers,
|
||||
updateAlertManagerConfig,
|
||||
} from '../api/alertmanager';
|
||||
import { deleteAlertManagerConfig, fetchAlertGroups, updateAlertManagerConfig } from '../api/alertmanager';
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { fetchAnnotations } from '../api/annotations';
|
||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||
@@ -264,22 +253,6 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
interface TestReceiversOptions {
|
||||
alertManagerSourceName: string;
|
||||
receivers: Receiver[];
|
||||
alert?: TestReceiversAlert;
|
||||
}
|
||||
|
||||
export const testReceiversAction = createAsyncThunk(
|
||||
'unifiedalerting/testReceivers',
|
||||
({ alertManagerSourceName, receivers, alert }: TestReceiversOptions): Promise<void> => {
|
||||
return withAppEvents(withSerializedError(testReceivers(alertManagerSourceName, receivers, alert)), {
|
||||
errorMessage: 'Failed to send test alert.',
|
||||
successMessage: 'Test alert sent.',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const rulesInSameGroupHaveInvalidFor = (rules: RulerRuleDTO[], everyDuration: string) => {
|
||||
return rules.filter((rule: RulerRuleDTO) => {
|
||||
const { forDuration } = getAlertInfo(rule, everyDuration);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
fetchGrafanaAnnotationsAction,
|
||||
fetchPromRulesAction,
|
||||
fetchRulerRulesAction,
|
||||
testReceiversAction,
|
||||
updateAlertManagerConfigAction,
|
||||
} from './actions';
|
||||
|
||||
@@ -23,7 +22,6 @@ export const reducer = combineReducers({
|
||||
fetchAlertGroupsAction,
|
||||
(alertManagerSourceName) => alertManagerSourceName
|
||||
).reducer,
|
||||
testReceivers: createAsyncSlice('testReceivers', testReceiversAction).reducer,
|
||||
managedAlertStateHistory: createAsyncSlice('managedAlertStateHistory', fetchGrafanaAnnotationsAction).reducer,
|
||||
});
|
||||
|
||||
|
||||
@@ -2885,6 +2885,8 @@
|
||||
"notification-message": "Notification message",
|
||||
"predefined-notification-message": "You will send a test notification that uses a predefined alert. If you have defined a custom template or message, for better results switch to <strong>custom</strong> notification message, from above.",
|
||||
"send-test-notification": "Send test notification",
|
||||
"test-failed": "Test notification failed",
|
||||
"test-successful": "Test notification sent successfully",
|
||||
"title-test-contact-point": "Test contact point"
|
||||
},
|
||||
"threshold": {
|
||||
|
||||
Reference in New Issue
Block a user