mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: add button to deactivate current alertmanager configuration (#36951)
* reset alert manager config button for admins * "alert manager" -> "Alertmanager"
This commit is contained in:
parent
8d06aaaf09
commit
1881de8236
@ -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",
|
||||
|
86
public/app/features/alerting/unified/Admin.test.tsx
Normal file
86
public/app/features/alerting/unified/Admin.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
50
public/app/features/alerting/unified/Admin.tsx
Normal file
50
public/app/features/alerting/unified/Admin.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -34,7 +34,7 @@ const renderAmNotifications = () => {
|
||||
|
||||
const dataSources = {
|
||||
am: mockDataSource({
|
||||
name: 'Alert Manager',
|
||||
name: 'Alertmanager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
}),
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -55,7 +55,7 @@ const renderAmRoutes = () => {
|
||||
|
||||
const dataSources = {
|
||||
am: mockDataSource({
|
||||
name: 'Alert Manager',
|
||||
name: 'Alertmanager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
}),
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -108,7 +108,7 @@ describe('Receivers', () => {
|
||||
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
|
||||
});
|
||||
|
||||
it('Template and receiver tables are rendered, alert manager can be selected', async () => {
|
||||
it('Template and receiver tables are rendered, alertmanager can be selected', async () => {
|
||||
mocks.api.fetchConfig.mockImplementation((name) =>
|
||||
Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig)
|
||||
);
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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[]>({
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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">
|
||||
|
@ -81,7 +81,7 @@ describe('ReceiversTable', () => {
|
||||
expect(rows[1].querySelectorAll('td')[1].textContent).toEqual('');
|
||||
});
|
||||
|
||||
it('render receivers with alert manager notifers', async () => {
|
||||
it('render receivers with alertmanager notifers', async () => {
|
||||
const receivers: Receiver[] = [
|
||||
{
|
||||
name: 'with receivers',
|
||||
|
@ -40,7 +40,7 @@ export function useAlertManagerSourceName(): [string | undefined, (alertManagerS
|
||||
if (isAlertManagerSource(querySource)) {
|
||||
return [querySource, update];
|
||||
} else {
|
||||
// non existing alert manager
|
||||
// non existing alertmanager
|
||||
return [undefined, update];
|
||||
}
|
||||
}
|
||||
|
@ -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.',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -16,7 +16,7 @@ export function extractNotifierTypeCounts(receiver: Receiver, grafanaNotifiers:
|
||||
|
||||
function getCortexAlertManagerNotifierTypeCounts(receiver: Receiver): NotifierTypeCounts {
|
||||
return Object.entries(receiver)
|
||||
.filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) // filter out only properties that are alert manager notifier
|
||||
.filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) // filter out only properties that are alertmanager notifier
|
||||
.filter(([_, value]) => Array.isArray(value) && !!value.length) // check that there are actually notifiers of this type configured
|
||||
.reduce<NotifierTypeCounts>((acc, [key, value]) => {
|
||||
const type = key.replace('_configs', ''); // remove the `_config` part from the key, making it intto a notifier name
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user