From f27326f7d9b89c654f915de3496c2ab3e533f29d Mon Sep 17 00:00:00 2001 From: Virginia Cepeda Date: Wed, 5 Apr 2023 15:13:33 -0300 Subject: [PATCH] Alerting: Choose a previous valid AM configuration in case of error (#65746) * Add new property to AlertmanagerConfig type * Implement fetching successfully applied configurations Added method to fetch them from the API and its corresponding action and reducer * Extract ConfigEditor as component to avoid code duplication * Display dropdown with valid configs upon error and allow to save them * Fix tests * Refactor to call new endpoint using RTK * Improve texts * Apply suggested refactor * Change constant casing * Only show config selector for Grafana AM * Remove ts-ignore * Move code together for simplicity * Remove invalid mock * Update endpoint and types based on backend changes * Rename property * Rename alermanager config property from backend changes * Disable editing old configurations Due to the latest backend changes, we no longer will provide the option to edit previous AM configurations in a textearea. Instead users will only be allowed to reset to a specific one with the same content. For this reason the textearea for old conf igurations is disabled and a different form action (not submit) is executed on the "reset config" button. The updateAlertManage rConfigAction is reset to its old functionality due to these changes. * Add id to AlertManagerCortexConfig type We'll need it to pass as a parameter to the new reset endpoint * Add new endpoint for resetting AM configs to an old version * Move the "Reset to selected configuration" button next to the drop-down * Add relative offset to configurations --- .../alerting/unified/api/alertmanager.ts | 2 + .../alerting/unified/api/alertmanagerApi.ts | 23 ++++ .../admin/AlertmanagerConfig.test.tsx | 8 +- .../components/admin/AlertmanagerConfig.tsx | 114 +++++++----------- .../admin/AlertmanagerConfigSelector.tsx | 110 +++++++++++++++++ .../unified/components/admin/ConfigEditor.tsx | 96 +++++++++++++++ .../alerting/unified/state/actions.ts | 10 +- .../plugins/datasource/alertmanager/types.ts | 3 + 8 files changed, 288 insertions(+), 78 deletions(-) create mode 100644 public/app/features/alerting/unified/components/admin/AlertmanagerConfigSelector.tsx create mode 100644 public/app/features/alerting/unified/components/admin/ConfigEditor.tsx 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 && ( - -