diff --git a/public/app/features/alerting/unified/api/alertmanager.ts b/public/app/features/alerting/unified/api/alertmanager.ts index 2f75264468f..ba565453d42 100644 --- a/public/app/features/alerting/unified/api/alertmanager.ts +++ b/public/app/features/alerting/unified/api/alertmanager.ts @@ -34,6 +34,8 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P template_files: result.data.template_files ?? {}, template_file_provenances: result.data.template_file_provenances ?? {}, alertmanager_config: result.data.alertmanager_config ?? {}, + last_applied: result.data.last_applied, + id: result.data.id, }; } catch (e) { // if no config has been uploaded to grafana, it returns error instead of latest config diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index be7f2634b44..66f6d760422 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -1,12 +1,16 @@ import { AlertmanagerChoice, + AlertManagerCortexConfig, ExternalAlertmanagerConfig, ExternalAlertmanagers, ExternalAlertmanagersResponse, } from '../../../../plugins/datasource/alertmanager/types'; +import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { alertingApi } from './alertingApi'; +const LIMIT_TO_SUCCESSFULLY_APPLIED_AMS = 10; + export interface AlertmanagersChoiceResponse { alertmanagersChoice: AlertmanagerChoice; numExternalAlertmanagers: number; @@ -33,5 +37,24 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ query: (config) => ({ url: '/api/v1/ngalert/admin_config', method: 'POST', data: config }), invalidatesTags: ['AlertmanagerChoice'], }), + + getValidAlertManagersConfig: build.query({ + //this is only available for the "grafana" alert manager + query: () => ({ + url: `/api/alertmanager/${getDatasourceAPIUid( + GRAFANA_RULES_SOURCE_NAME + )}/config/history?limit=${LIMIT_TO_SUCCESSFULLY_APPLIED_AMS}`, + }), + }), + + resetAlertManagerConfigToOldVersion: build.mutation<{ message: string }, { id: number }>({ + //this is only available for the "grafana" alert manager + query: (config) => ({ + url: `/api/alertmanager/${getDatasourceAPIUid(GRAFANA_RULES_SOURCE_NAME)}/config/history/${ + config.id + }/_activate`, + method: 'POST', + }), + }), }), }); diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx index a9bebf53196..5b6df00d2f9 100644 --- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx +++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx @@ -100,9 +100,7 @@ describe('Admin config', () => { alertmanager_config: {}, }); mocks.api.deleteAlertManagerConfig.mockResolvedValue(); - - await renderAdminPage(dataSources.alertManager.name); - + renderAdminPage(dataSources.alertManager.name); await userEvent.click(await ui.resetButton.find()); await userEvent.click(ui.confirmButton.get()); await waitFor(() => expect(mocks.api.deleteAlertManagerConfig).toHaveBeenCalled()); @@ -128,7 +126,7 @@ describe('Admin config', () => { mocks.api.fetchConfig.mockImplementation(() => Promise.resolve(savedConfig ?? defaultConfig)); mocks.api.updateAlertManagerConfig.mockResolvedValue(); - await renderAdminPage(dataSources.alertManager.name); + renderAdminPage(dataSources.alertManager.name); const input = await ui.configInput.find(); expect(input.value).toEqual(JSON.stringify(defaultConfig, null, 2)); await userEvent.clear(input); @@ -147,7 +145,7 @@ describe('Admin config', () => { ...someCloudAlertManagerStatus, config: someCloudAlertManagerConfig.alertmanager_config, }); - await renderAdminPage(dataSources.promAlertManager.name); + renderAdminPage(dataSources.promAlertManager.name); await ui.readOnlyConfig.find(); expect(ui.configInput.query()).not.toBeInTheDocument(); diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx index 959e55d4d3a..b5d4261ba13 100644 --- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React, { useEffect, useState, useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form, useStyles2 } from '@grafana/ui'; +import { Alert, useStyles2 } from '@grafana/ui'; import { useDispatch } from 'app/types'; import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName'; @@ -17,7 +17,10 @@ import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } import { initialAsyncRequestState } from '../../utils/redux'; import { AlertManagerPicker } from '../AlertManagerPicker'; -interface FormValues { +import AlertmanagerConfigSelector, { ValidAmConfigOption } from './AlertmanagerConfigSelector'; +import { ConfigEditor } from './ConfigEditor'; + +export interface FormValues { configJSON: string; } @@ -29,11 +32,14 @@ export default function AlertmanagerConfig(): JSX.Element { const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false); const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig); const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig); + const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : false; const styles = useStyles2(getStyles); const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs); + const [selectedAmConfig, setSelectedAmConfig] = useState(); + const { result: config, loading: isLoadingConfig, @@ -60,6 +66,13 @@ export default function AlertmanagerConfig(): JSX.Element { [config] ); + const defaultValidValues = useMemo( + (): FormValues => ({ + configJSON: selectedAmConfig ? JSON.stringify(selectedAmConfig.value, null, 2) : '', + }), + [selectedAmConfig] + ); + const loading = isDeleting || isLoadingConfig || isSaving; const onSubmit = (values: FormValues) => { @@ -84,9 +97,25 @@ export default function AlertmanagerConfig(): JSX.Element { dataSources={alertManagers} /> {loadingError && !loading && ( - - {loadingError.message || 'Unknown error.'} - + <> + + {loadingError.message || 'Unknown error.'} + + + {alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && ( + + )} + )} {isDeleting && alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME && ( @@ -94,70 +123,17 @@ export default function AlertmanagerConfig(): JSX.Element { )} {alertManagerSourceName && config && ( -
- {({ register, errors }) => ( - <> - {!readOnly && ( - -