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:
@@ -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{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Alerting",
|
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 = {
|
const dataSources = {
|
||||||
am: mockDataSource({
|
am: mockDataSource({
|
||||||
name: 'Alert Manager',
|
name: 'Alertmanager',
|
||||||
type: DataSourceType.Alertmanager,
|
type: DataSourceType.Alertmanager,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { initialAsyncRequestState } from './utils/redux';
|
|||||||
|
|
||||||
import { AmNotificationsGroup } from './components/amnotifications/AmNotificationsGroup';
|
import { AmNotificationsGroup } from './components/amnotifications/AmNotificationsGroup';
|
||||||
import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants';
|
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 AlertManagerNotifications = () => {
|
||||||
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const renderAmRoutes = () => {
|
|||||||
|
|
||||||
const dataSources = {
|
const dataSources = {
|
||||||
am: mockDataSource({
|
am: mockDataSource({
|
||||||
name: 'Alert Manager',
|
name: 'Alertmanager',
|
||||||
type: DataSourceType.Alertmanager,
|
type: DataSourceType.Alertmanager,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ const AmRoutes: FC = () => {
|
|||||||
<AlertingPageWrapper pageId="am-routes">
|
<AlertingPageWrapper pageId="am-routes">
|
||||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||||
{resultError && !resultLoading && (
|
{resultError && !resultLoading && (
|
||||||
<Alert severity="error" title="Error loading alert manager config">
|
<Alert severity="error" title="Error loading Alertmanager config">
|
||||||
{resultError.message || 'Unknown error.'}
|
{resultError.message || 'Unknown error.'}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{resultLoading && <LoadingPlaceholder text="Loading alert manager config..." />}
|
{resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
|
||||||
{result && !resultLoading && !resultError && (
|
{result && !resultLoading && !resultError && (
|
||||||
<>
|
<>
|
||||||
<AmRootRoute
|
<AmRootRoute
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const Receivers: FC = () => {
|
|||||||
onChange={setAlertManagerSourceName}
|
onChange={setAlertManagerSourceName}
|
||||||
/>
|
/>
|
||||||
{error && !loading && (
|
{error && !loading && (
|
||||||
<Alert severity="error" title="Error loading alert manager config">
|
<Alert severity="error" title="Error loading Alertmanager config">
|
||||||
{error.message || 'Unknown error.'}
|
{error.message || 'Unknown error.'}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -55,6 +55,17 @@ export async function updateAlertManagerConfig(
|
|||||||
.toPromise();
|
.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[]> {
|
export async function fetchSilences(alertManagerSourceName: string): Promise<Silence[]> {
|
||||||
const result = await getBackendSrv()
|
const result = await getBackendSrv()
|
||||||
.fetch<Silence[]>({
|
.fetch<Silence[]>({
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const AlertManagerPicker: FC<Props> = ({ onChange, current, disabled = fa
|
|||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
className={styles.field}
|
className={styles.field}
|
||||||
label={disabled ? 'Alert manager' : 'Choose alert manager'}
|
label={disabled ? 'Alertmanager' : 'Choose Alertmanager'}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
data-testid="alertmanager-picker"
|
data-testid="alertmanager-picker"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const ReceiversAndTemplatesView: FC<Props> = ({ config, alertManagerName
|
|||||||
{isCloud && (
|
{isCloud && (
|
||||||
<Alert className={styles.section} severity="info" title="Global config for contact points">
|
<Alert className={styles.section} severity="info" title="Global config for contact points">
|
||||||
<p>
|
<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.
|
password, for all the supported contact points.
|
||||||
</p>
|
</p>
|
||||||
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
|
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
createOrUpdateSilence,
|
createOrUpdateSilence,
|
||||||
updateAlertManagerConfig,
|
updateAlertManagerConfig,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
|
deleteAlertManagerConfig,
|
||||||
} from '../api/alertmanager';
|
} from '../api/alertmanager';
|
||||||
import { fetchRules } from '../api/prometheus';
|
import { fetchRules } from '../api/prometheus';
|
||||||
import {
|
import {
|
||||||
@@ -62,7 +63,11 @@ export const fetchAlertManagerConfigAction = createAsyncThunk(
|
|||||||
withSerializedError(
|
withSerializedError(
|
||||||
fetchAlertManagerConfig(alertManagerSourceName).then((result) => {
|
fetchAlertManagerConfig(alertManagerSourceName).then((result) => {
|
||||||
// if user config is empty for cortex alertmanager, try to get config from status endpoint
|
// 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) => ({
|
return fetchStatus(alertManagerSourceName).then((status) => ({
|
||||||
alertmanager_config: status.config,
|
alertmanager_config: status.config,
|
||||||
template_files: {},
|
template_files: {},
|
||||||
@@ -404,7 +409,10 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
|
|||||||
withSerializedError(
|
withSerializedError(
|
||||||
(async () => {
|
(async () => {
|
||||||
const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName);
|
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(
|
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.'
|
'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,
|
fetchFolderAction,
|
||||||
fetchAlertGroupsAction,
|
fetchAlertGroupsAction,
|
||||||
checkIfLotexSupportsEditingRulesAction,
|
checkIfLotexSupportsEditingRulesAction,
|
||||||
|
deleteAlertManagerConfigAction,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
export const reducer = combineReducers({
|
export const reducer = combineReducers({
|
||||||
@@ -32,6 +33,7 @@ export const reducer = combineReducers({
|
|||||||
}),
|
}),
|
||||||
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
|
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
|
||||||
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
|
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
|
||||||
|
deleteAMConfig: createAsyncSlice('deleteAMConfig', deleteAlertManagerConfigAction).reducer,
|
||||||
updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer,
|
updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer,
|
||||||
amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName)
|
amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName)
|
||||||
.reducer,
|
.reducer,
|
||||||
|
|||||||
@@ -466,6 +466,13 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
import(/* webpackChunkName: "AlertingRedirectToRule"*/ 'app/features/alerting/unified/RedirectToRuleViewer')
|
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',
|
path: '/playlists',
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
|
|||||||
Reference in New Issue
Block a user