diff --git a/public/app/features/alerting/unified/api/receiversApi.ts b/public/app/features/alerting/unified/api/receiversApi.ts index 17d32398b04..9d0778d6e8e 100644 --- a/public/app/features/alerting/unified/api/receiversApi.ts +++ b/public/app/features/alerting/unified/api/receiversApi.ts @@ -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({ + 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('; '); +} diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 499fd7bdaee..0202e7c2e52 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -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({ 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({
{isTestable && onTest && isTestAvailable && ( - )} diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index 8971c9b34d7..de17ab6c7c0 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -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(); + const [testReceivers, setTestReceivers] = useState(); // 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) } /> - setTestChannelValues(undefined)} - isOpen={!!testChannelValues} - onTest={(alert) => testNotification(alert)} - /> + {testReceivers && ( + setTestReceivers(undefined)} + isOpen={!!testReceivers} + alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME} + receivers={testReceivers} + /> + )} ); }; diff --git a/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx b/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx index a0f81586269..78cdb95fa89 100644 --- a/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx @@ -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.predefined); const styles = useStyles2(getStyles); const formMethods = useForm({ 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((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) && ( + + {stringifyErrorLike(error)} + + )} + + {isSuccess && ( + + )} +