Alerting: Stop allowing manual editing/restore of internal AM config via settings (#103884)

pull/104489/head
Tom Ratcliffe 1 month ago committed by GitHub
parent 9e933882ed
commit e71acd9ec3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 33
      public/app/features/alerting/unified/Settings.test.tsx
  2. 6
      public/app/features/alerting/unified/components/settings/AlertmanagerCard.tsx
  3. 29
      public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx
  4. 26
      public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx
  5. 27
      public/app/features/alerting/unified/components/settings/ConfigurationDrawer.tsx
  6. 1
      public/app/features/alerting/unified/components/settings/InternalAlertmanager.tsx
  7. 8
      public/app/features/alerting/unified/components/settings/VersionManager.tsx
  8. 14
      public/locales/en-US/grafana.json

@ -1,5 +1,4 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
@ -26,8 +25,9 @@ const ui = {
statusReceiving: byText(/receiving grafana-managed alerts/i),
statusNotReceiving: byText(/not receiving/i),
configurationDrawer: byRole('dialog', { name: 'Drawer title Internal Grafana Alertmanager' }),
configurationDrawer: byRole('dialog', { name: 'Drawer title Grafana built-in Alertmanager' }),
editConfigurationButton: byRole('button', { name: /edit configuration/i }),
viewConfigurationButton: byRole('button', { name: /view configuration/i }),
saveConfigurationButton: byRole('button', { name: /save/i }),
enableButton: byRole('button', { name: 'Enable' }),
@ -54,7 +54,7 @@ describe('Alerting settings', () => {
expect(ui.statusReceiving.get(ui.builtInAlertmanagerCard.get())).toBeInTheDocument();
// check external altermanagers
// check external alertmanagers
DataSourcesResponse.forEach((ds) => {
// get the card for datasource
const card = ui.alertmanagerCard(ds.name).get();
@ -74,45 +74,36 @@ describe('Alerting settings', () => {
});
it('should be able to view configuration', async () => {
render(<SettingsPage />);
const { user } = render(<SettingsPage />);
// wait for loading to be done
await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument());
// open configuration drawer
const internalAMCard = ui.builtInAlertmanagerCard.get();
const editInternal = ui.editConfigurationButton.get(internalAMCard);
await userEvent.click(editInternal);
await waitFor(() => {
expect(ui.configurationDrawer.get()).toBeInTheDocument();
});
await userEvent.click(ui.saveConfigurationButton.get());
expect(ui.saveConfigurationButton.get()).toBeDisabled();
await user.click(ui.viewConfigurationButton.get(internalAMCard));
expect(await ui.configurationDrawer.find()).toBeInTheDocument();
await waitFor(() => {
expect(ui.saveConfigurationButton.get()).toBeEnabled();
});
expect(ui.saveConfigurationButton.query()).not.toBeInTheDocument();
});
it('should be able to view versions', async () => {
render(<SettingsPage />);
const { user } = render(<SettingsPage />);
// wait for loading to be done
await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument());
expect(await ui.builtInAlertmanagerSection.find()).toBeInTheDocument();
// open configuration drawer
const internalAMCard = ui.builtInAlertmanagerCard.get();
const editInternal = ui.editConfigurationButton.get(internalAMCard);
await userEvent.click(editInternal);
await user.click(ui.viewConfigurationButton.get(internalAMCard));
expect(await ui.configurationDrawer.find()).toBeInTheDocument();
await waitFor(() => {
expect(ui.configurationDrawer.get()).toBeInTheDocument();
});
// click versions tab
await userEvent.click(ui.versionsTab.get());
await user.click(ui.versionsTab.get());
await waitFor(() => {
expect(screen.getByText(/last applied/i)).toBeInTheDocument();

@ -121,7 +121,11 @@ export function AlertmanagerCard({
<Stack direction="row" gap={1}>
{/* ⚠ provisioned Data sources cannot have their "enable" / "disable" actions but we should still allow editing of the configuration */}
<Button onClick={onEditConfiguration} icon={readOnly ? 'eye' : 'edit'} variant="secondary" fill="outline">
{readOnly ? 'View configuration' : 'Edit configuration'}
{readOnly ? (
<Trans i18nKey="alerting.alertmanager-card.view-configuration">View configuration</Trans>
) : (
<Trans i18nKey="alerting.alertmanager-card.edit-configuration">Edit configuration</Trans>
)}
</Button>
{showActions ? (
<>

@ -47,20 +47,11 @@ describe('Alerting Settings', () => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingInstanceRead]);
});
it('should be able to reset alertmanager config', async () => {
it('should not be able to reset alertmanager config', async () => {
const onReset = jest.fn();
renderConfiguration('grafana', { onReset });
await userEvent.click(await ui.resetButton.find());
await waitFor(() => {
expect(ui.resetConfirmButton.query()).toBeInTheDocument();
});
await userEvent.click(ui.resetConfirmButton.get());
await waitFor(() => expect(onReset).toHaveBeenCalled());
expect(onReset).toHaveBeenLastCalledWith('grafana');
expect(ui.resetButton.query()).not.toBeInTheDocument();
});
it('should be able to cancel', async () => {
@ -100,4 +91,20 @@ describe('vanilla Alertmanager', () => {
expect(ui.saveButton.get()).toBeInTheDocument();
expect(ui.resetButton.get()).toBeInTheDocument();
});
it('should be able to reset non-Grafana alertmanager config', async () => {
const onReset = jest.fn();
renderConfiguration(PROVISIONED_MIMIR_ALERTMANAGER_UID, { onReset });
expect(ui.cancelButton.get()).toBeInTheDocument();
expect(ui.saveButton.get()).toBeInTheDocument();
expect(ui.resetButton.get()).toBeInTheDocument();
await userEvent.click(ui.resetButton.get());
await userEvent.click(ui.resetConfirmButton.get());
await waitFor(() => expect(onReset).toHaveBeenCalled());
expect(onReset).toHaveBeenLastCalledWith(PROVISIONED_MIMIR_ALERTMANAGER_UID);
});
});

@ -28,12 +28,12 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave
const { loading: isDeleting, error: deletingError } = useUnifiedAlertingSelector((state) => state.deleteAMConfig);
const { loading: isSaving, error: savingError } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
const [showResetConfirmation, setShowResetConfirmation] = useState(false);
const isGrafanaManagedAlertmanager = alertmanagerName === GRAFANA_RULES_SOURCE_NAME;
// ⚠ provisioned data sources should not prevent the configuration from being edited
const immutableDataSource = alertmanagerName ? isVanillaPrometheusAlertManagerDataSource(alertmanagerName) : false;
const readOnly = immutableDataSource;
const readOnly = immutableDataSource || isGrafanaManagedAlertmanager;
const isGrafanaManagedAlertmanager = alertmanagerName === GRAFANA_RULES_SOURCE_NAME;
const styles = useStyles2(getStyles);
const {
@ -128,12 +128,28 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave
);
}
const confirmationText = isGrafanaManagedAlertmanager
? `Are you sure you want to reset configuration for the Grafana Alertmanager? Contact points and notification policies will be reset to their defaults.`
: `Are you sure you want to reset configuration for "${alertmanagerName}"? Contact points and notification policies will be reset to their defaults.`;
const confirmationText = t(
'alerting.alertmanager-config.reset-confirmation',
'Are you sure you want to reset configuration for "{{alertmanagerName}}"? Contact points and notification policies will be reset to their defaults.',
{ alertmanagerName }
);
return (
<div className={styles.container}>
{isGrafanaManagedAlertmanager && (
<Alert
severity="info"
title={t(
'alerting.alertmanager-config.gma-manual-configuration-is-not-supported',
'Manual configuration changes not supported'
)}
>
<Trans i18nKey="alerting.alertmanager-config.gma-manual-configuration-description">
The internal Grafana Alertmanager configuration cannot be manually changed. To change this configuration,
edit the individual resources through the UI.
</Trans>
</Alert>
)}
{/* form error state */}
{errors.configJSON && (
<Alert

@ -3,7 +3,7 @@ import { useCallback, useMemo, useState } from 'react';
import { Drawer, Tab, TabsBar } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import AlertmanagerConfig from './AlertmanagerConfig';
import { useSettings } from './SettingsContext';
@ -17,6 +17,10 @@ export function useEditConfigurationDrawer() {
const [open, setOpen] = useState(false);
const { updateAlertmanagerSettings, resetAlertmanagerSettings } = useSettings();
const isGrafanaManagedAlertmanager = dataSourceName === GRAFANA_RULES_SOURCE_NAME;
const immutableDataSource = dataSourceName ? isVanillaPrometheusAlertManagerDataSource(dataSourceName) : false;
const readOnly = immutableDataSource || isGrafanaManagedAlertmanager;
const showConfiguration = (dataSourceName: string) => {
setDataSourceName(dataSourceName);
setOpen(true);
@ -33,14 +37,29 @@ export function useEditConfigurationDrawer() {
}
const isGrafanaAlertmanager = dataSourceName === GRAFANA_RULES_SOURCE_NAME;
const title = isGrafanaAlertmanager ? 'Internal Grafana Alertmanager' : dataSourceName;
const title = isGrafanaAlertmanager
? t(
'alerting.use-edit-configuration-drawer.drawer.internal-grafana-alertmanager-title',
'Grafana built-in Alertmanager'
)
: dataSourceName;
const subtitle = readOnly
? t(
'alerting.use-edit-configuration-drawer.drawer.title-view-the-alertmanager-configuration',
'View Alertmanager configuration'
)
: t(
'alerting.use-edit-configuration-drawer.drawer.title-edit-the-alertmanager-configuration',
'Edit Alertmanager configuration'
);
// @todo check copy
return (
<Drawer
onClose={handleDismiss}
title={title}
subtitle="Edit the Alertmanager configuration"
subtitle={subtitle}
tabs={
<TabsBar>
<Tab
@ -74,7 +93,7 @@ export function useEditConfigurationDrawer() {
)}
</Drawer>
);
}, [open, dataSourceName, handleDismiss, activeTab, updateAlertmanagerSettings, resetAlertmanagerSettings]);
}, [open, dataSourceName, readOnly, handleDismiss, activeTab, updateAlertmanagerSettings, resetAlertmanagerSettings]);
return [drawer, showConfiguration, handleDismiss] as const;
}

@ -30,6 +30,7 @@ export default function InternalAlertmanager({ onEditConfiguration }: Props) {
onEditConfiguration={handleEditConfiguration}
onEnable={handleEnable}
onDisable={handleDisable}
readOnly
/>
);
}

@ -94,11 +94,15 @@ const AlertmanagerConfigurationVersionManager = ({
}
if (isLoading) {
return 'Loading...';
return <Trans i18nKey="alerting.alertmanager-configuration-version-manager.loading">Loading...</Trans>;
}
if (!historicalConfigs.length) {
return 'No previous configurations';
return (
<Trans i18nKey="alerting.alertmanager-configuration-version-manager.no-previous-configurations">
No previous configurations
</Trans>
);
}
// with this function we'll compute the diff with the previous version; that way the user can get some idea of how many lines where changed in each update that was applied

@ -413,16 +413,21 @@
},
"alertmanager-card": {
"disable": "Disable",
"edit-configuration": "Edit configuration",
"enable": "Enable",
"not-receiving-grafana-managed-alerts": "Not receiving Grafana managed alerts",
"text-activation-in-progress": "Activation in progress",
"text-failed-to-adopt-alertmanager": "Failed to adopt Alertmanager",
"text-inconclusive": "Inconclusive",
"text-receiving-grafanamanaged-alerts": "Receiving Grafana-managed alerts",
"title-alerting-settings": "Alerting settings"
"title-alerting-settings": "Alerting settings",
"view-configuration": "View configuration"
},
"alertmanager-config": {
"gma-manual-configuration-description": "The internal Grafana Alertmanager configuration cannot be manually changed. To change this configuration, edit the individual resources through the UI.",
"gma-manual-configuration-is-not-supported": "Manual configuration changes not supported",
"reset": "Reset",
"reset-confirmation": "Are you sure you want to reset configuration for \"{{alertmanagerName}}\"? Contact points and notification policies will be reset to their defaults.",
"resetting-configuration-might-while": "Resetting configuration, this might take a while.",
"title-failed-to-load-alertmanager-configuration": "Failed to load Alertmanager configuration",
"title-oops-something-went-wrong": "Oops, something went wrong",
@ -435,6 +440,8 @@
"restore": "Restore",
"text-latest": "Latest"
},
"loading": "Loading...",
"no-previous-configurations": "No previous configurations",
"this-might-take-a-while": "This might take a while...",
"title-failed-to-load-configuration-history": "Failed to load configuration history",
"title-restore-version": "Restore version",
@ -2075,8 +2082,11 @@
},
"use-edit-configuration-drawer": {
"drawer": {
"internal-grafana-alertmanager-title": "Grafana built-in Alertmanager",
"label-json-model": "JSON Model",
"label-versions": "Versions"
"label-versions": "Versions",
"title-edit-the-alertmanager-configuration": "Edit Alertmanager configuration",
"title-view-the-alertmanager-configuration": "View Alertmanager configuration"
}
},
"use-edit-policy-modal": {

Loading…
Cancel
Save