Alerting: add button to deactivate current alertmanager configuration (#36951)

* reset alert manager config button for admins

* "alert manager" -> "Alertmanager"
This commit is contained in:
Domas
2021-07-22 09:15:39 +03:00
committed by GitHub
parent 8d06aaaf09
commit 1881de8236
18 changed files with 194 additions and 14 deletions

View File

@@ -227,6 +227,12 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
})
}
}
if c.OrgRole == models.ROLE_ADMIN {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Admin", Id: "alerting-admin", Url: hs.Cfg.AppSubURL + "/alerting/admin",
Icon: "cog",
})
}
navTree = append(navTree, &dtos.NavLink{
Text: "Alerting",

View File

@@ -0,0 +1,86 @@
import React from 'react';
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
import { getAllDataSources } from './utils/config';
import { fetchAlertManagerConfig, deleteAlertManagerConfig } from './api/alertmanager';
import { configureStore } from 'app/store/configureStore';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import Admin from './Admin';
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 { 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';
jest.mock('./api/alertmanager');
jest.mock('./api/grafana');
jest.mock('./utils/config');
const mocks = {
getAllDataSources: typeAsJestMock(getAllDataSources),
api: {
fetchConfig: typeAsJestMock(fetchAlertManagerConfig),
deleteAlertManagerConfig: typeAsJestMock(deleteAlertManagerConfig),
},
};
const renderAdminPage = (alertManagerSourceName?: string) => {
const store = configureStore();
locationService.push(
'/alerting/notifications' +
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '')
);
return render(
<Provider store={store}>
<Router history={locationService.getHistory()}>
<Admin />
</Router>
</Provider>
);
};
const dataSources = {
alertManager: mockDataSource({
name: 'CloudManager',
type: DataSourceType.Alertmanager,
}),
};
const ui = {
confirmButton: byRole('button', { name: /Confirm Modal Danger Button/ }),
resetButton: byRole('button', { name: /Reset Alertmanager configuration/ }),
};
describe('Alerting Admin', () => {
beforeEach(() => {
jest.resetAllMocks();
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
setDataSourceSrv(new MockDataSourceSrv(dataSources));
contextSrv.isGrafanaAdmin = true;
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
});
it('Reset alertmanager config', async () => {
mocks.api.fetchConfig.mockResolvedValue({
template_files: {
foo: 'bar',
},
alertmanager_config: {},
});
mocks.api.deleteAlertManagerConfig.mockResolvedValue();
await renderAdminPage(dataSources.alertManager.name);
userEvent.click(await ui.resetButton.find());
userEvent.click(ui.confirmButton.get());
await waitFor(() => expect(mocks.api.deleteAlertManagerConfig).toHaveBeenCalled());
expect(ui.confirmButton.query()).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,50 @@
import React, { useState } from 'react';
import { Button, ConfirmModal } 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 { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
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 resetConfig = () => {
if (alertManagerSourceName) {
dispatch(deleteAlertManagerConfigAction(alertManagerSourceName));
}
setShowConfirmDeleteAMConfig(false);
};
return (
<AlertingPageWrapper pageId="alerting-admin">
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
{alertManagerSourceName && (
<>
<Button disabled={loading} variant="destructive" onClick={() => setShowConfirmDeleteAMConfig(true)}>
Reset Alertmanager configuration
</Button>
{!!showConfirmDeleteAMConfig && (
<ConfirmModal
isOpen={true}
title="Reset Alertmanager configuration"
body={`Are you sure you want to reset configuration ${
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME
? 'for the Grafana Alertmanager'
: `for "${alertManagerSourceName}"`
}? Contact points and notification policies will be reset to their defaults.`}
confirmText="Yes, reset configuration"
onConfirm={resetConfig}
onDismiss={() => setShowConfirmDeleteAMConfig(false)}
/>
)}
</>
)}
</AlertingPageWrapper>
);
}

View File

@@ -34,7 +34,7 @@ const renderAmNotifications = () => {
const dataSources = {
am: mockDataSource({
name: 'Alert Manager',
name: 'Alertmanager',
type: DataSourceType.Alertmanager,
}),
};

View File

@@ -12,7 +12,7 @@ import { initialAsyncRequestState } from './utils/redux';
import { AmNotificationsGroup } from './components/amnotifications/AmNotificationsGroup';
import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants';
import { Alert, LoadingPlaceholder } from '../../../../../packages/grafana-ui/src';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
const AlertManagerNotifications = () => {
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();

View File

@@ -55,7 +55,7 @@ const renderAmRoutes = () => {
const dataSources = {
am: mockDataSource({
name: 'Alert Manager',
name: 'Alertmanager',
type: DataSourceType.Alertmanager,
}),
};

View File

@@ -92,11 +92,11 @@ const AmRoutes: FC = () => {
<AlertingPageWrapper pageId="am-routes">
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
{resultError && !resultLoading && (
<Alert severity="error" title="Error loading alert manager config">
<Alert severity="error" title="Error loading Alertmanager config">
{resultError.message || 'Unknown error.'}
</Alert>
)}
{resultLoading && <LoadingPlaceholder text="Loading alert manager config..." />}
{resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{result && !resultLoading && !resultError && (
<>
<AmRootRoute

View File

@@ -57,7 +57,7 @@ const Receivers: FC = () => {
onChange={setAlertManagerSourceName}
/>
{error && !loading && (
<Alert severity="error" title="Error loading alert manager config">
<Alert severity="error" title="Error loading Alertmanager config">
{error.message || 'Unknown error.'}
</Alert>
)}

View File

@@ -55,6 +55,17 @@ export async function updateAlertManagerConfig(
.toPromise();
}
export async function deleteAlertManagerConfig(alertManagerSourceName: string): Promise<void> {
await getBackendSrv()
.fetch({
method: 'DELETE',
url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/alerts`,
showErrorAlert: false,
showSuccessAlert: false,
})
.toPromise();
}
export async function fetchSilences(alertManagerSourceName: string): Promise<Silence[]> {
const result = await getBackendSrv()
.fetch<Silence[]>({

View File

@@ -41,7 +41,7 @@ export const AlertManagerPicker: FC<Props> = ({ onChange, current, disabled = fa
return (
<Field
className={styles.field}
label={disabled ? 'Alert manager' : 'Choose alert manager'}
label={disabled ? 'Alertmanager' : 'Choose Alertmanager'}
disabled={disabled}
data-testid="alertmanager-picker"
>

View File

@@ -23,7 +23,7 @@ export const ReceiversAndTemplatesView: FC<Props> = ({ config, alertManagerName
{isCloud && (
<Alert className={styles.section} severity="info" title="Global config for contact points">
<p>
For each external alert managers you can define global settings, like server addresses, usernames and
For each external Alertmanager you can define global settings, like server addresses, usernames and
password, for all the supported contact points.
</p>
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">

View File

@@ -25,6 +25,7 @@ import {
createOrUpdateSilence,
updateAlertManagerConfig,
fetchStatus,
deleteAlertManagerConfig,
} from '../api/alertmanager';
import { fetchRules } from '../api/prometheus';
import {
@@ -62,7 +63,11 @@ export const fetchAlertManagerConfigAction = createAsyncThunk(
withSerializedError(
fetchAlertManagerConfig(alertManagerSourceName).then((result) => {
// if user config is empty for cortex alertmanager, try to get config from status endpoint
if (isEmpty(result.alertmanager_config) && alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) {
if (
isEmpty(result.alertmanager_config) &&
isEmpty(result.template_files) &&
alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME
) {
return fetchStatus(alertManagerSourceName).then((status) => ({
alertmanager_config: status.config,
template_files: {},
@@ -404,7 +409,10 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
withSerializedError(
(async () => {
const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName);
if (JSON.stringify(latestConfig) !== JSON.stringify(oldConfig)) {
if (
!(isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files)) &&
JSON.stringify(latestConfig) !== JSON.stringify(oldConfig)
) {
throw new Error(
'It seems configuration has been recently updated. Please reload page and try again to make sure that recent changes are not overwritten.'
);
@@ -569,3 +577,13 @@ export const checkIfLotexSupportsEditingRulesAction = createAsyncThunk(
}
)
);
export const deleteAlertManagerConfigAction = createAsyncThunk(
'unifiedalerting/deleteAlertManagerConfig',
async (alertManagerSourceName: string): Promise<void> => {
return withAppEvents(withSerializedError(deleteAlertManagerConfig(alertManagerSourceName)), {
errorMessage: 'Failed to reset Alertmanager configuration',
successMessage: 'Alertmanager configuration reset.',
});
}
);

View File

@@ -14,6 +14,7 @@ import {
fetchFolderAction,
fetchAlertGroupsAction,
checkIfLotexSupportsEditingRulesAction,
deleteAlertManagerConfigAction,
} from './actions';
export const reducer = combineReducers({
@@ -32,6 +33,7 @@ export const reducer = combineReducers({
}),
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
deleteAMConfig: createAsyncSlice('deleteAMConfig', deleteAlertManagerConfigAction).reducer,
updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer,
amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName)
.reducer,

View File

@@ -466,6 +466,13 @@ export function getAppRoutes(): RouteDescriptor[] {
import(/* webpackChunkName: "AlertingRedirectToRule"*/ 'app/features/alerting/unified/RedirectToRuleViewer')
),
},
{
path: '/alerting/admin',
roles: () => ['Admin'],
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingAdmin" */ 'app/features/alerting/unified/Admin')
),
},
{
path: '/playlists',
component: SafeDynamicImport(