diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index c583d45599a..6e3eba6611d 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -7,10 +7,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { config } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { - setAlertmanagerConfig, - setGrafanaAlertmanagerConfig, -} from 'app/features/alerting/unified/mocks/server/configure'; +import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { MOCK_DATASOURCE_EXTERNAL_VANILLA_ALERTMANAGER_UID } from 'app/features/alerting/unified/mocks/server/handlers/datasources'; import { @@ -206,8 +203,12 @@ describe('Mute timings', () => { // FIXME: scope down grantUserPermissions(Object.values(AccessControlAction)); - setGrafanaAlertmanagerConfig(defaultConfig); - setAlertmanagerConfig(defaultConfig); + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig); + + // TODO: Add this at a higher level to ensure that no tests depend on others running first + // Without this, the selected alertmanager in a previous test can affect the next, meaning tests + // pass/fail depending on the order they are run/if they are focused + window.localStorage.clear(); }); it('creates a new mute timing, with mute_time_intervals in config', async () => { @@ -238,9 +239,9 @@ describe('Mute timings', () => { it('creates a new mute timing, with time_intervals in config', async () => { const capture = captureRequests(); - setAlertmanagerConfig(defaultConfigWithNewTimeIntervalsField); + setAlertmanagerConfig(dataSources.am.uid, defaultConfigWithNewTimeIntervalsField); renderMuteTimings(, { - search: `?alertmanager=${alertmanagerName}`, + search: `?alertmanager=${dataSources.am.name}`, }); await fillOutForm({ @@ -262,9 +263,9 @@ describe('Mute timings', () => { }); it('creates a new mute timing, with time_intervals and mute_time_intervals in config', async () => { - setGrafanaAlertmanagerConfig(defaultConfigWithBothTimeIntervalsField); + setAlertmanagerConfig(dataSources.am.uid, defaultConfigWithBothTimeIntervalsField); renderMuteTimings(, { - search: `?alertmanager=${alertmanagerName}`, + search: `?alertmanager=${dataSources.am.name}`, }); expect(ui.nameField.get()).toBeInTheDocument(); diff --git a/public/app/features/alerting/unified/NotificationPolicies.test.tsx b/public/app/features/alerting/unified/NotificationPolicies.test.tsx index f9cc962e05e..9cbc98ccf37 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.test.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.test.tsx @@ -1,64 +1,85 @@ -import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; -import { render, userEvent, waitFor, within } from 'test/test-utils'; +import 'core-js/stable/structured-clone'; +import { clickSelectOption } from 'test/helpers/selectOptionInTest'; +import { render, screen, userEvent } from 'test/test-utils'; import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'; -import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; -import { contextSrv } from 'app/core/services/context_srv'; +import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { + getErrorResponse, + makeAllAlertmanagerConfigFetchFail, +} from 'app/features/alerting/unified/mocks/server/configure'; +import { + getAlertmanagerConfig, + setAlertmanagerConfig, + setAlertmanagerStatus, +} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; +import { + TIME_INTERVAL_NAME_FILE_PROVISIONED, + TIME_INTERVAL_NAME_HAPPY_PATH, +} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s'; +import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { AlertManagerCortexConfig, AlertManagerDataSourceJsonData, AlertManagerImplementation, MatcherOperator, - MuteTimeInterval, - Route, RouteWithID, } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import NotificationPolicies, { findRoutesMatchingFilters } from './NotificationPolicies'; -import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager'; -import { alertmanagerApi } from './api/alertmanagerApi'; -import { discoverAlertmanagerFeatures } from './api/buildInfo'; -import { MockDataSourceSrv, mockDataSource, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks'; -import { defaultGroupBy } from './utils/amroutes'; -import { getAllDataSources } from './utils/config'; +import { + grantUserPermissions, + mockDataSource, + someCloudAlertManagerConfig, + someCloudAlertManagerStatus, +} from './mocks'; import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; -import 'core-js/stable/structured-clone'; - -jest.mock('./api/alertmanager'); -jest.mock('./utils/config'); -jest.mock('app/core/services/context_srv'); -jest.mock('./api/buildInfo'); jest.mock('./useRouteGroupsMatcher'); -const mocks = { - getAllDataSourcesMock: jest.mocked(getAllDataSources), - - api: { - fetchAlertManagerConfig: jest.mocked(fetchAlertManagerConfig), - updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig), - fetchStatus: jest.mocked(fetchStatus), - discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures), - }, - contextSrv: jest.mocked(contextSrv), -}; - setupMswServer(); -const renderNotificationPolicies = (alertManagerSourceName?: string) => { - return render(, { - historyOptions: { - initialEntries: [ - '/alerting/routes' + - (alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : ''), - ], - }, - }); +const updateTiming = async (selectElement: HTMLElement, value: string): Promise => { + const user = userEvent.setup(); + const input = byRole('textbox').get(selectElement); + await user.clear(input); + await user.type(input, value); }; +const openDefaultPolicyEditModal = async () => { + const user = userEvent.setup(); + await user.click(await ui.moreActionsDefaultPolicy.find()); + await user.click(await ui.editButton.find()); +}; + +const openEditModal = async ( + /** (zero-based) Index of the policy in the list to open the edit modal for */ + index: number +) => { + const user = userEvent.setup(); + await user.click((await ui.moreActions.findAll())[index]); + await user.click(await ui.editButton.find()); +}; + +const renderNotificationPolicies = (alertManagerSourceName: string = GRAFANA_RULES_SOURCE_NAME) => + render( + <> + + + , + { + historyOptions: { + initialEntries: [ + '/alerting/routes' + + (alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : ''), + ], + }, + } + ); + const dataSources = { am: mockDataSource({ name: 'Alertmanager', @@ -67,30 +88,36 @@ const dataSources = { promAlertManager: mockDataSource({ name: 'PromManager', type: DataSourceType.Alertmanager, + uid: 'prometheusAlertManager', jsonData: { implementation: AlertManagerImplementation.prometheus, }, }), + mimir: mockDataSource({ + name: 'mimir', + type: DataSourceType.Alertmanager, + uid: 'mimir', + jsonData: { + implementation: AlertManagerImplementation.mimir, + }, + }), }; const ui = { - rootReceiver: byTestId('am-routes-root-receiver'), - rootGroupBy: byTestId('am-routes-root-group-by'), - rootTimings: byTestId('am-routes-root-timings'), - row: byTestId('am-routes-row'), - + /** Row of policy tree containing default policy */ rootRouteContainer: byTestId('am-root-route-container'), + /** (deeply) Nested rows of policies under the default/root policy */ + row: byTestId('am-route-container'), - editButton: byRole('button', { name: 'Edit' }), - saveButton: byRole('button', { name: 'Save' }), + newChildPolicyButton: byRole('button', { name: /New child policy/ }), + newSiblingPolicyButton: byRole('button', { name: /Add new policy/ }), - setDefaultReceiverCTA: byRole('button', { name: 'Set a default contact point' }), + moreActionsDefaultPolicy: byLabelText(/more actions for default policy/i), + moreActions: byLabelText(/more actions for policy/i), + editButton: byRole('menuitem', { name: 'Edit' }), - editRouteButton: byLabelText('Edit route'), - deleteRouteButton: byLabelText('Delete route'), - newPolicyButton: byRole('button', { name: /Add policy/ }), - newPolicyCTAButton: byRole('button', { name: /Add specific policy/ }), - savePolicyButton: byRole('button', { name: /save policy/i }), + saveButton: byRole('button', { name: /update (default )?policy/i }), + deleteRouteButton: byRole('menuitem', { name: 'Delete' }), receiverSelect: byTestId('am-receiver-select'), groupSelect: byTestId('am-group-select'), @@ -101,451 +128,217 @@ const ui = { groupRepeatContainer: byTestId('am-repeat-interval'), confirmDeleteModal: byRole('dialog'), - confirmDeleteButton: byLabelText('Confirm Modal Danger Button'), + confirmDeleteButton: byRole('button', { name: /yes, delete policy/i }), +}; + +const getRootRoute = async () => { + return ui.rootRouteContainer.find(); }; describe('NotificationPolicies', () => { - const subroutes: Route[] = [ - { - match: { - sub1matcher1: 'sub1value1', - sub1matcher2: 'sub1value2', - }, - match_re: { - sub1matcher3: 'sub1value3', - sub1matcher4: 'sub1value4', - }, - group_by: ['sub1group1', 'sub1group2'], - receiver: 'a-receiver', - continue: true, - group_wait: '3s', - group_interval: '2m', - repeat_interval: '1s', - routes: [ - { - match: { - sub1sub1matcher1: 'sub1sub1value1', - sub1sub1matcher2: 'sub1sub1value2', - }, - match_re: { - sub1sub1matcher3: 'sub1sub1value3', - sub1sub1matcher4: 'sub1sub1value4', - }, - group_by: ['sub1sub1group1', 'sub1sub1group2'], - receiver: 'another-receiver', - }, - { - match: { - sub1sub2matcher1: 'sub1sub2value1', - sub1sub2matcher2: 'sub1sub2value2', - }, - match_re: { - sub1sub2matcher3: 'sub1sub2value3', - sub1sub2matcher4: 'sub1sub2value4', - }, - group_by: ['sub1sub2group1', 'sub1sub2group2'], - receiver: 'another-receiver', - }, - ], - }, - { - match: { - sub2matcher1: 'sub2value1', - sub2matcher2: 'sub2value2', - }, - match_re: { - sub2matcher3: 'sub2value3', - sub2matcher4: 'sub2value4', - }, - receiver: 'another-receiver', - }, - ]; - - const emptyRoute: Route = {}; - - const simpleRoute: Route = { - receiver: 'simple-receiver', - matchers: ['hello=world', 'foo!=bar'], - }; - - const rootRoute: Route = { - receiver: 'default-receiver', - group_by: ['a-group', 'another-group'], - group_wait: '1s', - group_interval: '2m', - repeat_interval: '3d', - routes: subroutes, - }; - - const muteInterval: MuteTimeInterval = { - name: 'default-mute', - time_intervals: [ - { - times: [{ start_time: '12:00', end_time: '24:00' }], - weekdays: ['monday:friday'], - days_of_month: ['1:7', '-1:-7'], - months: ['january:june'], - years: ['2020:2022'], - }, - ], - }; - beforeEach(() => { - mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources)); - mocks.contextSrv.hasPermission.mockImplementation(() => true); - mocks.contextSrv.evaluatePermission.mockImplementation(() => []); - mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false }); - setDataSourceSrv(new MockDataSourceSrv(dataSources)); + setupDataSources(...Object.values(dataSources)); + grantUserPermissions([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstanceCreate, + AccessControlAction.AlertingInstanceUpdate, + AccessControlAction.AlertingInstancesExternalRead, + AccessControlAction.AlertingInstancesExternalWrite, + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalRead, + AccessControlAction.AlertingNotificationsExternalWrite, + ]); }); - afterEach(() => { - jest.resetAllMocks(); + it('loads and shows routes', async () => { + const { alertmanager_config: testConfig } = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); - setDataSourceSrv(undefined as unknown as DataSourceSrv); - }); - - it.skip('loads and shows routes', async () => { - mocks.api.fetchAlertManagerConfig.mockResolvedValue({ - alertmanager_config: { - route: rootRoute, - receivers: [ - { - name: 'default-receiver', - }, - { - name: 'a-receiver', - }, - { - name: 'another-receiver', - }, - ], - }, - template_files: {}, - }); + const { route: defaultRoute } = testConfig; renderNotificationPolicies(); + const rootRouteEl = await getRootRoute(); - await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1)); - - expect(ui.rootReceiver.get()).toHaveTextContent(rootRoute.receiver!); - expect(ui.rootGroupBy.get()).toHaveTextContent(rootRoute.group_by!.join(', ')); - const rootTimings = ui.rootTimings.get(); - expect(rootTimings).toHaveTextContent(rootRoute.group_wait!); - expect(rootTimings).toHaveTextContent(rootRoute.group_interval!); - expect(rootTimings).toHaveTextContent(rootRoute.repeat_interval!); + expect(rootRouteEl).toHaveTextContent(new RegExp(`delivered to ${defaultRoute?.receiver}`, 'i')); + expect(rootRouteEl).toHaveTextContent(new RegExp(`grouped by ${defaultRoute?.group_by?.join(', ')}`, 'i')); + expect(rootRouteEl).toHaveTextContent(/wait 30s to group/i); + expect(rootRouteEl).toHaveTextContent(/wait 5m before sending/i); + expect(rootRouteEl).toHaveTextContent(/repeated every 4h/i); const rows = await ui.row.findAll(); - expect(rows).toHaveLength(2); + expect(rows).toHaveLength(5); - subroutes.forEach((route, index) => { + defaultRoute?.routes?.forEach((route) => { Object.entries(route.match ?? {}).forEach(([label, value]) => { - expect(rows[index]).toHaveTextContent(`${label}=${value}`); + expect(screen.getByText(`${label} = ${value}`)).toBeInTheDocument(); }); Object.entries(route.match_re ?? {}).forEach(([label, value]) => { - expect(rows[index]).toHaveTextContent(`${label}=~${value}`); + expect(screen.getByText(`${label} =~ ${value}`)).toBeInTheDocument(); }); if (route.group_by) { - expect(rows[index]).toHaveTextContent(route.group_by.join(', ')); + expect(rows.some((row) => row?.textContent?.includes(`Grouped by ${route.group_by?.join(', ')}`))).toBe(true); } if (route.receiver) { - expect(rows[index]).toHaveTextContent(route.receiver); + expect(rows.some((row) => row?.textContent?.includes(`Delivered to ${route.receiver}`))).toBe(true); } }); }); - it.skip('can edit root route if one is already defined', async () => { - const defaultConfig: AlertManagerCortexConfig = { - alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: { - receiver: 'default', - group_by: ['alertname'], - }, - templates: [], - }, - template_files: {}, - }; - const currentConfig = { current: defaultConfig }; - mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => { - currentConfig.current = newConfig; - return Promise.resolve(); - }); - - mocks.api.fetchAlertManagerConfig.mockImplementation(() => { - return Promise.resolve(currentConfig.current); - }); - + it('can edit root route if one is already defined', async () => { const { user } = renderNotificationPolicies(); - expect(await ui.rootReceiver.find()).toHaveTextContent('default'); - expect(ui.rootGroupBy.get()).toHaveTextContent('alertname'); + let rootRoute = await getRootRoute(); - // open root route for editing - const rootRouteContainer = await ui.rootRouteContainer.find(); - await user.click(ui.editButton.get(rootRouteContainer)); + expect(rootRoute).toHaveTextContent('default policy'); + expect(rootRoute).toHaveTextContent(/delivered to grafana-default-email/i); + expect(rootRoute).toHaveTextContent(/grouped by alertname/i); + + await openDefaultPolicyEditModal(); // configure receiver & group by const receiverSelect = await ui.receiverSelect.find(); - await clickSelectOption(receiverSelect, 'critical'); + + // The contact points are fetched from the k8s API, which we aren't overriding here + // when we use a different + await clickSelectOption(receiverSelect, 'lotsa-emails'); const groupSelect = ui.groupSelect.get(); await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}'); // configure timing intervals - await user.click(byText('Timing options').get(rootRouteContainer)); + await user.click(screen.getByText(/timing options/i)); - await updateTiming(ui.groupWaitContainer.get(), '1', 'Minutes'); - await updateTiming(ui.groupIntervalContainer.get(), '4', 'Minutes'); - await updateTiming(ui.groupRepeatContainer.get(), '5', 'Hours'); + await updateTiming(ui.groupWaitContainer.get(), '1m'); + await updateTiming(ui.groupIntervalContainer.get(), '4m'); + await updateTiming(ui.groupRepeatContainer.get(), '5h'); //save - await user.click(ui.saveButton.get(rootRouteContainer)); + await user.click(await screen.findByRole('button', { name: /update default policy/i })); // wait for it to go out of edit mode - await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); - - // check that appropriate api calls were made - expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, { - alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: { - continue: false, - group_by: ['alertname', 'namespace'], - receiver: 'critical', - routes: [], - group_interval: '4m', - group_wait: '1m', - repeat_interval: '5h', - mute_time_intervals: [], - }, - templates: [], - }, - template_files: {}, - }); + expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument(); // check that new config values are rendered - await waitFor(() => expect(ui.rootReceiver.query()).toHaveTextContent('critical')); - expect(ui.rootGroupBy.get()).toHaveTextContent('alertname, namespace'); + rootRoute = await getRootRoute(); + expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i); + expect(rootRoute).toHaveTextContent(/grouped by alertname, namespace/i); }); - it.skip('can edit root route if one is not defined yet', async () => { - mocks.api.fetchAlertManagerConfig.mockResolvedValue({ + it('can edit root route if one is not defined yet', async () => { + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, { alertmanager_config: { - receivers: [{ name: 'default' }], + route: {}, + receivers: [{ name: 'grafana-default-email' }], }, template_files: {}, }); - const { user } = renderNotificationPolicies(); - // open root route for editing - const rootRouteContainer = await ui.rootRouteContainer.find(); - await user.click(ui.editButton.get(rootRouteContainer)); + await openDefaultPolicyEditModal(); // configure receiver & group by const receiverSelect = await ui.receiverSelect.find(); - await clickSelectOption(receiverSelect, 'default'); + await clickSelectOption(receiverSelect, 'lotsa-emails'); const groupSelect = ui.groupSelect.get(); await user.type(byRole('combobox').get(groupSelect), 'severity{enter}'); await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}'); //save - await user.click(ui.saveButton.get(rootRouteContainer)); + await user.click(await screen.findByRole('button', { name: /update default policy/i })); - // wait for it to go out of edit mode - await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); + expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument(); - // check that appropriate api calls were made - expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, { - alertmanager_config: { - receivers: [{ name: 'default' }], - route: { - continue: false, - group_by: defaultGroupBy.concat(['severity', 'namespace']), - receiver: 'default', - routes: [], - mute_time_intervals: [], - }, - }, - template_files: {}, - }); + const rootRoute = await getRootRoute(); + expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i); + expect(rootRoute).toHaveTextContent(/grouped by severity, namespace/i); }); it('hides create and edit button if user does not have permission', async () => { - mocks.contextSrv.hasPermission.mockImplementation((action) => - [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsRead].includes( - action as AccessControlAction - ) - ); + grantUserPermissions([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstancesExternalRead, + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsExternalRead, + ]); - renderNotificationPolicies(); - await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1)); + const { user } = renderNotificationPolicies(); - expect(ui.newPolicyButton.query()).not.toBeInTheDocument(); + expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument(); + expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument(); + + await user.click(await ui.moreActionsDefaultPolicy.find()); expect(ui.editButton.query()).not.toBeInTheDocument(); }); it('Show error message if loading Alertmanager config fails', async () => { - mocks.api.fetchAlertManagerConfig.mockRejectedValue({ - status: 500, - data: { - message: "Alertmanager has exploded. it's gone. Forget about it.", - }, - }); - - jest.spyOn(alertmanagerApi, 'useGetAlertmanagerAlertGroupsQuery').mockImplementation(() => ({ - currentData: [], - refetch: jest.fn(), - })); + makeAllAlertmanagerConfigFetchFail(getErrorResponse("Alertmanager has exploded. it's gone. Forget about it.")); renderNotificationPolicies(); - await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1)); + await screen.findByText(/error loading alertmanager config/i); expect(await byText("Alertmanager has exploded. it's gone. Forget about it.").find()).toBeInTheDocument(); - expect(ui.rootReceiver.query()).not.toBeInTheDocument(); - expect(ui.editButton.query()).not.toBeInTheDocument(); + expect(ui.rootRouteContainer.query()).not.toBeInTheDocument(); }); - it.skip('Converts matchers to object_matchers for grafana alertmanager', async () => { - const defaultConfig: AlertManagerCortexConfig = { - alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: { - continue: false, - receiver: 'default', - group_by: ['alertname'], - routes: [simpleRoute], - group_interval: '4m', - group_wait: '1m', - repeat_interval: '5h', - }, - templates: [], - }, - template_files: {}, - }; - - const currentConfig = { current: defaultConfig }; - mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => { - currentConfig.current = newConfig; - return Promise.resolve(); - }); - - mocks.api.fetchAlertManagerConfig.mockImplementation(() => { - return Promise.resolve(currentConfig.current); - }); - + it('Converts matchers to object_matchers for grafana alertmanager', async () => { const { user } = renderNotificationPolicies(GRAFANA_RULES_SOURCE_NAME); - expect(await ui.rootReceiver.find()).toHaveTextContent('default'); - expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled(); - // Toggle a save to test new object_matchers - const rootRouteContainer = await ui.rootRouteContainer.find(); - await user.click(ui.editButton.get(rootRouteContainer)); - await user.click(ui.saveButton.get(rootRouteContainer)); + const policyIndex = 0; + await openEditModal(policyIndex); - await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); + // Save policy to test that format is converted to object_matchers + await user.click(await ui.saveButton.find()); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled(); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, { - alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: { - continue: false, - group_by: ['alertname'], - group_interval: '4m', - group_wait: '1m', - receiver: 'default', - repeat_interval: '5h', - mute_time_intervals: [], - routes: [ - { - continue: false, - group_by: [], - object_matchers: [ - ['hello', '=', 'world'], - ['foo', '!=', 'bar'], - ], - receiver: 'simple-receiver', - mute_time_intervals: [], - routes: [], - }, - ], - }, - templates: [], - }, - template_files: {}, - }); + expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i); + + const updatedConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); + expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].object_matchers).toMatchSnapshot(); }); - it.skip('Should be able to delete an empty route', async () => { - const routeConfig = { - continue: false, - receiver: 'default', - group_by: ['alertname'], - routes: [emptyRoute], - group_interval: '4m', - group_wait: '1m', - repeat_interval: '5h', - mute_time_intervals: [], - }; - + it('Should be able to delete an empty route', async () => { const defaultConfig: AlertManagerCortexConfig = { alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: routeConfig, - templates: [], + route: { + routes: [{}], + }, }, template_files: {}, }; - mocks.api.fetchAlertManagerConfig.mockImplementation(() => { - return Promise.resolve(defaultConfig); - }); - - mocks.api.updateAlertManagerConfig.mockResolvedValue(Promise.resolve()); + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig); const { user } = renderNotificationPolicies(GRAFANA_RULES_SOURCE_NAME); - await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); - const deleteButtons = await ui.deleteRouteButton.findAll(); - expect(deleteButtons).toHaveLength(1); + await user.click(await ui.moreActions.find()); + const deleteButtons = await ui.deleteRouteButton.find(); - await user.click(deleteButtons[0]); + await user.click(deleteButtons); const confirmDeleteButton = ui.confirmDeleteButton.get(ui.confirmDeleteModal.get()); expect(confirmDeleteButton).toBeInTheDocument(); await user.click(confirmDeleteButton); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith<[string, AlertManagerCortexConfig]>( - GRAFANA_RULES_SOURCE_NAME, - { - ...defaultConfig, - alertmanager_config: { - ...defaultConfig.alertmanager_config, - route: { - ...routeConfig, - routes: [], - }, - }, - } - ); + expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i); + + expect(ui.row.query()).not.toBeInTheDocument(); }); - it.skip('Keeps matchers for non-grafana alertmanager sources', async () => { - const defaultConfig: AlertManagerCortexConfig = { + it('Keeps matchers for non-grafana alertmanager sources', async () => { + setAlertmanagerConfig(dataSources.am.uid, { alertmanager_config: { receivers: [{ name: 'default' }, { name: 'critical' }], route: { continue: false, receiver: 'default', group_by: ['alertname'], - routes: [simpleRoute], + routes: [ + { + receiver: 'simple-receiver', + matchers: ['hello=world', 'foo!=bar'], + }, + ], group_interval: '4m', group_wait: '1m', repeat_interval: '5h', @@ -553,79 +346,38 @@ describe('NotificationPolicies', () => { templates: [], }, template_files: {}, - }; - - const currentConfig = { current: defaultConfig }; - mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => { - currentConfig.current = newConfig; - return Promise.resolve(); - }); - - mocks.api.fetchAlertManagerConfig.mockImplementation(() => { - return Promise.resolve(currentConfig.current); }); const { user } = renderNotificationPolicies(dataSources.am.name); - expect(await ui.rootReceiver.find()).toHaveTextContent('default'); - expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled(); - // Toggle a save to test new object_matchers - const rootRouteContainer = await ui.rootRouteContainer.find(); - await user.click(ui.editButton.get(rootRouteContainer)); - await user.click(ui.saveButton.get(rootRouteContainer)); + const policyIndex = 0; + await openEditModal(policyIndex); - await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); + // Save policy to test that format is NOT converted + await user.click(await ui.saveButton.find()); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled(); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(dataSources.am.name, { - alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: { - continue: false, - group_by: ['alertname'], - group_interval: '4m', - group_wait: '1m', - matchers: [], - receiver: 'default', - repeat_interval: '5h', - mute_time_intervals: [], - routes: [ - { - continue: false, - group_by: [], - matchers: ['hello=world', 'foo!=bar'], - receiver: 'simple-receiver', - routes: [], - mute_time_intervals: [], - }, - ], - }, - templates: [], - }, - template_files: {}, - }); + const updatedConfig = getAlertmanagerConfig(dataSources.am.uid); + expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].matchers).toMatchSnapshot(); }); - it.skip('Prometheus Alertmanager routes cannot be edited', async () => { - mocks.api.fetchStatus.mockResolvedValue({ + it('Prometheus Alertmanager routes cannot be edited', async () => { + setAlertmanagerStatus(dataSources.promAlertManager.uid, { ...someCloudAlertManagerStatus, config: someCloudAlertManagerConfig.alertmanager_config, }); renderNotificationPolicies(dataSources.promAlertManager.name); - const rootRouteContainer = await ui.rootRouteContainer.find(); - expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument(); + + expect(await ui.rootRouteContainer.find()).toBeInTheDocument(); + const rows = await ui.row.findAll(); expect(rows).toHaveLength(2); - expect(ui.editRouteButton.query()).not.toBeInTheDocument(); - expect(ui.deleteRouteButton.query()).not.toBeInTheDocument(); - expect(ui.saveButton.query()).not.toBeInTheDocument(); - expect(mocks.api.fetchAlertManagerConfig).not.toHaveBeenCalled(); - expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); + expect(ui.moreActions.query()).not.toBeInTheDocument(); + expect(ui.moreActionsDefaultPolicy.query()).not.toBeInTheDocument(); }); it('Prometheus Alertmanager has no CTA button if there are no specific policies', async () => { - mocks.api.fetchStatus.mockResolvedValue({ + setAlertmanagerStatus(dataSources.promAlertManager.uid, { ...someCloudAlertManagerStatus, config: { ...someCloudAlertManagerConfig.alertmanager_config, @@ -636,102 +388,39 @@ describe('NotificationPolicies', () => { }, }); - jest.spyOn(alertmanagerApi, 'useGetAlertmanagerAlertGroupsQuery').mockImplementation(() => ({ - currentData: [], - refetch: jest.fn(), - })); - renderNotificationPolicies(dataSources.promAlertManager.name); - const rootRouteContainer = await ui.rootRouteContainer.find(); - await waitFor(() => - expect(within(rootRouteContainer).getByTestId('matching-instances')).toHaveTextContent('0instance') - ); - expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument(); - expect(ui.newPolicyCTAButton.query()).not.toBeInTheDocument(); - expect(mocks.api.fetchAlertManagerConfig).not.toHaveBeenCalled(); - expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); + expect(await ui.rootRouteContainer.find()).toBeInTheDocument(); + + expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument(); + expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument(); }); - it.skip('Can add a mute timing to a route', async () => { - const defaultConfig: AlertManagerCortexConfig = { - alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: { - continue: false, - receiver: 'default', - group_by: ['alertname'], - routes: [simpleRoute], - group_interval: '4m', - group_wait: '1m', - repeat_interval: '5h', - }, - templates: [], - mute_time_intervals: [muteInterval], - }, - template_files: {}, - }; + it('Can add a mute timing to a route', async () => { + const { user } = renderNotificationPolicies(); - const currentConfig = { current: defaultConfig }; - mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => { - currentConfig.current = newConfig; - return Promise.resolve(); - }); - - mocks.api.fetchAlertManagerConfig.mockResolvedValue(defaultConfig); - - const { user } = renderNotificationPolicies(dataSources.am.name); - const rows = await ui.row.findAll(); - expect(rows).toHaveLength(1); - await user.click(ui.editRouteButton.get(rows[0])); + await openEditModal(0); const muteTimingSelect = ui.muteTimingSelect.get(); - await clickSelectOption(muteTimingSelect, 'default-mute'); - expect(muteTimingSelect).toHaveTextContent('default-mute'); + await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_HAPPY_PATH); + await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_FILE_PROVISIONED); - const savePolicyButton = ui.savePolicyButton.get(); - expect(savePolicyButton).toBeInTheDocument(); + await user.click(ui.saveButton.get()); - await user.click(savePolicyButton); + expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i); - await waitFor(() => expect(savePolicyButton).not.toBeInTheDocument()); - - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled(); - expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(dataSources.am.name, { - ...defaultConfig, - alertmanager_config: { - ...defaultConfig.alertmanager_config, - route: { - ...defaultConfig.alertmanager_config.route, - mute_time_intervals: [], - matchers: [], - routes: [ - { - ...simpleRoute, - mute_time_intervals: [muteInterval.name], - routes: [], - continue: false, - group_by: [], - }, - ], - }, - }, - }); + const policy = (await ui.row.findAll())[0]; + expect(policy).toHaveTextContent( + `Muted when ${TIME_INTERVAL_NAME_HAPPY_PATH}, ${TIME_INTERVAL_NAME_FILE_PROVISIONED}` + ); }); it.skip('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => { - mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true }); + makeAllAlertmanagerConfigFetchFail(getErrorResponse('alertmanager storage object not found')); + setAlertmanagerStatus(dataSources.mimir.uid, someCloudAlertManagerStatus); + renderNotificationPolicies(dataSources.mimir.name); - mocks.api.fetchAlertManagerConfig.mockRejectedValue({ - message: 'alertmanager storage object not found', - }); - - renderNotificationPolicies(); - - await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1)); - - expect(ui.rootReceiver.query()).toBeInTheDocument(); - expect(ui.setDefaultReceiverCTA.query()).toBeInTheDocument(); + expect(await ui.rootRouteContainer.find()).toBeInTheDocument(); }); }); @@ -806,19 +495,3 @@ describe('findRoutesMatchingFilters', () => { expect(matchingRoutes).toMatchSnapshot(); }); }); - -const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise => { - const user = userEvent.setup(); - await user.click(byRole('combobox').get(selectElement)); - await selectOptionInTest(selectElement, optionText); -}; - -const updateTiming = async (selectElement: HTMLElement, value: string, timeUnit: string): Promise => { - const user = userEvent.setup(); - const input = byRole('textbox').get(selectElement); - const select = byRole('combobox').get(selectElement); - await user.clear(input); - await user.type(input, value); - await user.click(select); - await selectOptionInTest(selectElement, timeUnit); -}; diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx index ba1f5025330..beb1b908abe 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.tsx @@ -4,6 +4,7 @@ import { useAsyncFn } from 'react-use'; import { GrafanaTheme2, UrlQueryMap } from '@grafana/data'; import { Alert, LoadingPlaceholder, Stack, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui'; +import { useAppNotification } from 'app/core/copy/appNotification'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings'; import { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; @@ -52,6 +53,7 @@ enum ActiveTab { const AmRoutes = () => { const dispatch = useDispatch(); const styles = useStyles2(getStyles); + const appNotification = useAppNotification(); const { useGetAlertmanagerAlertGroupsQuery } = alertmanagerApi; @@ -175,11 +177,11 @@ const AmRoutes = () => { }, oldConfig: result, alertManagerSourceName: selectedAlertmanager!, - successMessage: 'Updated notification policies', }) ) .unwrap() .then(() => { + appNotification.success('Updated notification policies'); if (selectedAlertmanager) { refetchAlertGroups(); } diff --git a/public/app/features/alerting/unified/__snapshots__/NotificationPolicies.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/NotificationPolicies.test.tsx.snap index 9011ec04694..17c630e4c21 100644 --- a/public/app/features/alerting/unified/__snapshots__/NotificationPolicies.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/NotificationPolicies.test.tsx.snap @@ -1,5 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`NotificationPolicies Converts matchers to object_matchers for grafana alertmanager 1`] = ` +[ + [ + "sub1matcher1", + "=", + "sub1value1", + ], + [ + "sub1matcher2", + "=", + "sub1value2", + ], + [ + "sub1matcher3", + "=~", + "sub1value3", + ], + [ + "sub1matcher4", + "=~", + "sub1value4", + ], +] +`; + +exports[`NotificationPolicies Keeps matchers for non-grafana alertmanager sources 1`] = ` +[ + "hello="world"", + "foo!="bar"", +] +`; + exports[`findRoutesMatchingFilters should not match non-existing 1`] = ` { "filtersApplied": true, diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json deleted file mode 100644 index 0077f89f3a0..00000000000 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "template_files": { - "slack-template": "{{ define \"slack-template\" }} Custom slack template {{ end }}", - "custom-email": "{{ define \"custom-email\" }} Custom email template {{ end }}", - "provisioned-template": "{{ define \"provisioned-template\" }} Custom provisioned template {{ end }}", - "template with spaces": "{{ define \"template with spaces\" }} Custom template with spaces in the name {{ end }}", - "misconfigured-template": "{{ define \"misconfigured template\" }} Template that is defined in template_files but not templates {{ end }}", - "misconfigured and provisioned": "{{ define \"misconfigured and provisioned template\" }} Provisioned template that is defined in template_files but not templates {{ end }}" - }, - "template_file_provenances": { - "provisioned-template": "api", - "misconfigured and provisioned": "api" - }, - "alertmanager_config": { - "route": { - "receiver": "grafana-default-email", - "routes": [ - { - "receiver": "provisioned-contact-point" - } - ] - }, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "xeKQrBrnk", - "name": "grafana-default-email", - "type": "email", - "disableResolveMessage": false, - "settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false }, - "secureFields": {} - } - ] - }, - { - "name": "provisioned-contact-point", - "grafana_managed_receiver_configs": [ - { - "uid": "s8SdCVjnk", - "name": "provisioned-contact-point", - "type": "email", - "disableResolveMessage": false, - "settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false }, - "secureFields": {}, - "provenance": "api" - } - ] - }, - { - "name": "lotsa-emails", - "grafana_managed_receiver_configs": [ - { - "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", - "name": "lotsa-emails", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", - "singleEmail": false - }, - "secureFields": {} - } - ] - }, - { - "name": "Slack with multiple channels", - "grafana_managed_receiver_configs": [ - { - "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", - "name": "Slack with multiple channels", - "type": "slack", - "disableResolveMessage": false, - "settings": { "recipient": "test-alerts" }, - "secureFields": { "token": true } - }, - { - "uid": "b286a3be-f690-49e2-8605-b075cbace2df", - "name": "Slack with multiple channels", - "type": "slack", - "disableResolveMessage": false, - "settings": { "recipient": "test-alerts2" }, - "secureFields": { "token": true } - } - ] - }, - { - "name": "OnCall Conctact point", - "grafana_managed_receiver_configs": [ - { - "name": "Oncall-integration", - "type": "oncall", - "settings": { - "url": "https://oncall-endpoint.example.com" - }, - "disableResolveMessage": false - } - ] - } - ], - "templates": ["slack-template", "custom-email", "provisioned-template", "template with spaces"], - "time_intervals": [], - "mute_time_intervals": [] - } -} diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx index cac835fad21..eb63d6cbec6 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -3,10 +3,8 @@ import { render, screen, userEvent, within } from 'test/test-utils'; import { config } from '@grafana/runtime'; import { defaultConfig } from 'app/features/alerting/unified/MuteTimings.test'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { - setGrafanaAlertmanagerConfig, - setMuteTimingsListError, -} from 'app/features/alerting/unified/mocks/server/configure'; +import { setMuteTimingsListError } from 'app/features/alerting/unified/mocks/server/configure'; +import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; @@ -31,7 +29,7 @@ setupMswServer(); describe('MuteTimingsTable', () => { describe('with necessary permissions', () => { beforeEach(() => { - setGrafanaAlertmanagerConfig(defaultConfig); + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig); config.featureToggles.alertingApiServer = false; grantUserPermissions([ AccessControlAction.AlertingNotificationsRead, diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx index 1e3a0af1583..2985c7e20a6 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx @@ -133,8 +133,8 @@ describe('Policy', () => { expect(within(firstPolicy).getByTestId('continue-matching')).toBeInTheDocument(); // expect(within(firstPolicy).getByTestId('matching-instances')).toHaveTextContent('0instances'); expect(within(firstPolicy).getByTestId('contact-point')).toHaveTextContent('provisioned-contact-point'); - expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted whenmt-1'); - expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active whenmt-2'); + expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted when mt-1'); + expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active when mt-2'); expect(within(firstPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited2 properties'); // second custom policy should be correct diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 0dbbc61072e..d5e4f4c8b04 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -23,6 +23,7 @@ import { } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import ConditionalWrap from 'app/features/alerting/unified/components/ConditionalWrap'; +import MoreButton from 'app/features/alerting/unified/components/MoreButton'; import { PrimaryText } from 'app/features/alerting/unified/components/common/TextVariants'; import { AlertmanagerGroup, @@ -308,12 +309,8 @@ const Policy = (props: PolicyComponentProps) => { )} {dropdownMenuActions.length > 0 && ( {dropdownMenuActions}}> -