From 69dff96c1bfac41673a988cff07f87b2622cbab9 Mon Sep 17 00:00:00 2001 From: Domas Date: Wed, 28 Jul 2021 09:21:42 +0300 Subject: [PATCH] Alerting: ability to edit alertmanager config as json via UI (#37268) --- .../features/alerting/unified/Admin.test.tsx | 40 +++++- .../app/features/alerting/unified/Admin.tsx | 136 +++++++++++++++--- .../alerting/unified/state/actions.ts | 31 +++- .../features/alerting/unified/utils/misc.ts | 18 +++ 4 files changed, 192 insertions(+), 33 deletions(-) diff --git a/public/app/features/alerting/unified/Admin.test.tsx b/public/app/features/alerting/unified/Admin.test.tsx index d27b4ebe750..fde78a79634 100644 --- a/public/app/features/alerting/unified/Admin.test.tsx +++ b/public/app/features/alerting/unified/Admin.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { typeAsJestMock } from 'test/helpers/typeAsJestMock'; import { getAllDataSources } from './utils/config'; -import { fetchAlertManagerConfig, deleteAlertManagerConfig } from './api/alertmanager'; +import { fetchAlertManagerConfig, deleteAlertManagerConfig, updateAlertManagerConfig } from './api/alertmanager'; import { configureStore } from 'app/store/configureStore'; import { locationService, setDataSourceSrv } from '@grafana/runtime'; import Admin from './Admin'; @@ -9,12 +9,13 @@ import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; import { render, waitFor } from '@testing-library/react'; -import { byRole } from 'testing-library-selector'; +import { byLabelText, byRole } from 'testing-library-selector'; import { mockDataSource, MockDataSourceSrv } from './mocks'; import { DataSourceType } from './utils/datasource'; import { contextSrv } from 'app/core/services/context_srv'; import store from 'app/core/store'; import userEvent from '@testing-library/user-event'; +import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; jest.mock('./api/alertmanager'); jest.mock('./api/grafana'); @@ -26,6 +27,7 @@ const mocks = { api: { fetchConfig: typeAsJestMock(fetchAlertManagerConfig), deleteAlertManagerConfig: typeAsJestMock(deleteAlertManagerConfig), + updateAlertManagerConfig: typeAsJestMock(updateAlertManagerConfig), }, }; @@ -55,7 +57,9 @@ const dataSources = { const ui = { confirmButton: byRole('button', { name: /Confirm Modal Danger Button/ }), - resetButton: byRole('button', { name: /Reset Alertmanager configuration/ }), + resetButton: byRole('button', { name: /Reset configuration/ }), + saveButton: byRole('button', { name: /Save/ }), + configInput: byLabelText(/Configuration/), }; describe('Alerting Admin', () => { @@ -83,4 +87,34 @@ describe('Alerting Admin', () => { await waitFor(() => expect(mocks.api.deleteAlertManagerConfig).toHaveBeenCalled()); expect(ui.confirmButton.query()).not.toBeInTheDocument(); }); + + it('Edit and save alertmanager config', async () => { + let savedConfig: AlertManagerCortexConfig | undefined = undefined; + + const defaultConfig = { + template_files: { + foo: 'bar', + }, + alertmanager_config: {}, + }; + + const newConfig = { + template_files: { + bar: 'baz', + }, + alertmanager_config: {}, + }; + + mocks.api.fetchConfig.mockImplementation(() => Promise.resolve(savedConfig ?? defaultConfig)); + mocks.api.updateAlertManagerConfig.mockResolvedValue(); + await renderAdminPage(dataSources.alertManager.name); + const input = await ui.configInput.find(); + expect(input.value).toEqual(JSON.stringify(defaultConfig, null, 2)); + userEvent.clear(input); + await userEvent.type(input, JSON.stringify(newConfig, null, 2)); + userEvent.click(ui.saveButton.get()); + await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled()); + await waitFor(() => expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3)); + expect(input.value).toEqual(JSON.stringify(newConfig, null, 2)); + }); }); diff --git a/public/app/features/alerting/unified/Admin.tsx b/public/app/features/alerting/unified/Admin.tsx index 5a98d876bee..1eb52de6cf1 100644 --- a/public/app/features/alerting/unified/Admin.tsx +++ b/public/app/features/alerting/unified/Admin.tsx @@ -1,18 +1,39 @@ -import React, { useState } from 'react'; -import { Button, ConfirmModal } from '@grafana/ui'; +import React, { useEffect, useState, useMemo } from 'react'; +import { Alert, Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form } from '@grafana/ui'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { useDispatch } from 'react-redux'; -import { deleteAlertManagerConfigAction } from './state/actions'; +import { + deleteAlertManagerConfigAction, + fetchAlertManagerConfigAction, + updateAlertManagerConfigAction, +} from './state/actions'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; +import { initialAsyncRequestState } from './utils/redux'; + +interface FormValues { + configJSON: string; +} export default function Admin(): JSX.Element { const dispatch = useDispatch(); const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false); - const { loading } = useUnifiedAlertingSelector((state) => state.deleteAMConfig); + const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig); + const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig); + + const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs); + + const { result: config, loading: isLoadingConfig, error: loadingError } = + (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState; + + useEffect(() => { + if (alertManagerSourceName) { + dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); + } + }, [alertManagerSourceName, dispatch]); const resetConfig = () => { if (alertManagerSourceName) { @@ -21,29 +42,98 @@ export default function Admin(): JSX.Element { setShowConfirmDeleteAMConfig(false); }; + const defaultValues = useMemo( + (): FormValues => ({ + configJSON: config ? JSON.stringify(config, null, 2) : '', + }), + [config] + ); + + const loading = isDeleting || isLoadingConfig || isSaving; + + const onSubmit = (values: FormValues) => { + if (alertManagerSourceName) { + dispatch( + updateAlertManagerConfigAction({ + newConfig: JSON.parse(values.configJSON), + oldConfig: config, + alertManagerSourceName, + successMessage: 'Alertmanager configuration updated.', + refetch: true, + }) + ); + } + }; + return ( - {alertManagerSourceName && ( - <> - - {!!showConfirmDeleteAMConfig && ( - setShowConfirmDeleteAMConfig(false)} - /> + {loadingError && !loading && ( + + {loadingError.message || 'Unknown error.'} + + )} + {isDeleting && alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME && ( + + It might take a while... + + )} + {alertManagerSourceName && config && ( +
+ {({ register, errors }) => ( + <> + +