diff --git a/.betterer.results b/.betterer.results index 515f8d40135..bed4d33a06e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1830,7 +1830,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx:5381": [ + "public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/alerting/unified/components/export/FileExportPreview.tsx:5381": [ diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 0fdd2893cf0..979ac9330ff 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -74,7 +74,6 @@ Some features are enabled by default. You can disable these feature by setting t | `splitScopes` | Support faster dashboard and folder search by splitting permission scopes into parts | | `dashgpt` | Enable AI powered features in dashboards | | `reportingRetries` | Enables rendering retries for the reporting feature | -| `alertingContactPointsV2` | Show the new contacpoints list view | | `transformationsVariableSupport` | Allows using variables in transformations | | `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 33da24411d5..6eb6c218fb4 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -124,7 +124,6 @@ export interface FeatureToggles { lokiRunQueriesInParallel?: boolean; wargamesTesting?: boolean; alertingInsights?: boolean; - alertingContactPointsV2?: boolean; externalCorePlugins?: boolean; pluginsAPIMetrics?: boolean; httpSLOLevels?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d10f921d735..22004e59248 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -791,13 +791,6 @@ var ( AllowSelfServe: falsePtr, HideFromAdminPage: true, // This is moving away from being a feature toggle. }, - { - Name: "alertingContactPointsV2", - Description: "Show the new contacpoints list view", - FrontendOnly: true, - Stage: FeatureStagePublicPreview, - Owner: grafanaAlertingSquad, - }, { Name: "externalCorePlugins", Description: "Allow core plugins to be loaded as external", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 956556ac376..34fd3bc6048 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -105,7 +105,6 @@ libraryPanelRBAC,experimental,@grafana/dashboards-squad,false,false,true,false lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false,false wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false,false alertingInsights,GA,@grafana/alerting-squad,false,false,false,true -alertingContactPointsV2,preview,@grafana/alerting-squad,false,false,false,true externalCorePlugins,experimental,@grafana/plugins-platform-backend,false,false,false,false pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,false,true httpSLOLevels,experimental,@grafana/hosted-grafana-team,false,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a37c72933dc..900645c168a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -431,10 +431,6 @@ const ( // Show the new alerting insights landing page FlagAlertingInsights = "alertingInsights" - // FlagAlertingContactPointsV2 - // Show the new contacpoints list view - FlagAlertingContactPointsV2 = "alertingContactPointsV2" - // FlagExternalCorePlugins // Allow core plugins to be loaded as external FlagExternalCorePlugins = "externalCorePlugins" diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index ccaa9128c96..6de84d52b62 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -1,15 +1,13 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { config } from '@grafana/runtime'; import { withErrorBoundary } from '@grafana/ui'; -const ContactPointsV1 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v1')); -const ContactPointsV2 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v2')); +const ContactPointsV2 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints')); const EditContactPoint = SafeDynamicImport(() => import('./components/contact-points/EditContactPoint')); const NewContactPoint = SafeDynamicImport(() => import('./components/contact-points/NewContactPoint')); const EditMessageTemplate = SafeDynamicImport(() => import('./components/contact-points/EditMessageTemplate')); const NewMessageTemplate = SafeDynamicImport(() => import('./components/contact-points/NewMessageTemplate')); -const GlobalConfig = SafeDynamicImport(() => import('./components/contact-points/GlobalConfig')); +const GlobalConfig = SafeDynamicImport(() => import('./components/contact-points/components/GlobalConfig')); const DuplicateMessageTemplate = SafeDynamicImport( () => import('./components/contact-points/DuplicateMessageTemplate') ); @@ -18,29 +16,21 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; -const newContactPointsListView = config.featureToggles.alertingContactPointsV2 ?? false; - -// TODO add pagenav back in – that way we have correct breadcrumbs and page title -const ContactPoints = (props: GrafanaRouteComponentProps): JSX.Element => ( +const ContactPoints = (_props: GrafanaRouteComponentProps): JSX.Element => ( - {/* TODO do we want a "routes" component for each Alerting entity? */} - {newContactPointsListView ? ( - - - - - - - - - - ) : ( - - )} + + + + + + + + + ); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx similarity index 57% rename from public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx rename to public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index 28420a80172..fde20c0fadc 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -5,6 +5,7 @@ import React, { PropsWithChildren } from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; +import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { setupMswServer } from '../../mockApi'; @@ -13,9 +14,12 @@ import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; import { setupDataSources } from '../../testSetup/datasources'; import { DataSourceType } from '../../utils/datasource'; -import ContactPoints, { ContactPoint } from './ContactPoints.v2'; +import ContactPoints, { ContactPoint } from './ContactPoints'; import setupGrafanaManagedServer from './__mocks__/grafanaManagedServer'; import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimirFlavoredServer'; +import setupVanillaAlertmanagerFlavoredServer, { + VANILLA_ALERTMANAGER_DATASOURCE_UID, +} from './__mocks__/vanillaAlertmanagerServer'; /** * There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them. @@ -38,17 +42,15 @@ const server = setupMswServer(); describe('contact points', () => { describe('Contact points with Grafana managed alertmanager', () => { beforeEach(() => { - setupGrafanaManagedServer(server); - }); - - beforeAll(() => { grantUserPermissions([ AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite, ]); + + setupGrafanaManagedServer(server); }); - it('should show / hide loading states', async () => { + it('should show / hide loading states, have all actions enabled', async () => { render( @@ -64,6 +66,67 @@ describe('contact points', () => { expect(screen.getByText('grafana-default-email')).toBeInTheDocument(); expect(screen.getAllByTestId('contact-point')).toHaveLength(4); + + // check for available actions – our mock 4 contact points, 1 of them is provisioned + expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'export all' })).toBeInTheDocument(); + + // 2 of them are unused by routes in the mock response + const unusedBadge = screen.getAllByLabelText('unused'); + expect(unusedBadge).toHaveLength(2); + + const viewProvisioned = screen.getByRole('link', { name: 'view-action' }); + expect(viewProvisioned).toBeInTheDocument(); + expect(viewProvisioned).not.toBeDisabled(); + + const editButtons = screen.getAllByRole('link', { name: 'edit-action' }); + expect(editButtons).toHaveLength(3); + editButtons.forEach((button) => { + expect(button).not.toBeDisabled(); + }); + + const moreActionsButtons = screen.getAllByRole('button', { name: 'more-actions' }); + expect(moreActionsButtons).toHaveLength(4); + moreActionsButtons.forEach((button) => { + expect(button).not.toBeDisabled(); + }); + }); + + it('should disable certain actions if the user has no write permissions', async () => { + grantUserPermissions([AccessControlAction.AlertingNotificationsRead]); + + render( + + + , + { wrapper: TestProvider } + ); + + // wait for loading to be done + await waitFor(async () => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + // should disable create contact point + expect(screen.getByRole('link', { name: 'add contact point' })).toHaveAttribute('aria-disabled', 'true'); + + // there should be no edit buttons + expect(screen.queryAllByRole('link', { name: 'edit-action' })).toHaveLength(0); + + // there should be view buttons though + const viewButtons = screen.getAllByRole('link', { name: 'view-action' }); + expect(viewButtons).toHaveLength(4); + + // delete should be disabled in the "more" actions + const moreButtons = screen.queryAllByRole('button', { name: 'more-actions' }); + expect(moreButtons).toHaveLength(4); + + // check if all of the delete buttons are disabled + for await (const button of moreButtons) { + await userEvent.click(button); + const deleteButton = await screen.queryByRole('menuitem', { name: 'delete' }); + expect(deleteButton).toBeDisabled(); + } }); it('should call delete when clicked and not disabled', async () => { @@ -175,7 +238,7 @@ describe('contact points', () => { ); }); - it('should show / hide loading states', async () => { + it('should show / hide loading states, have the right actions enabled', async () => { render( @@ -193,6 +256,76 @@ describe('contact points', () => { expect(screen.getByText('mixed')).toBeInTheDocument(); expect(screen.getByText('some webhook')).toBeInTheDocument(); expect(screen.getAllByTestId('contact-point')).toHaveLength(2); + + // check for available actions – export should be disabled + expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'export all' })).not.toBeInTheDocument(); + + // 1 of them is used by a route in the mock response + const unusedBadge = screen.getAllByLabelText('unused'); + expect(unusedBadge).toHaveLength(1); + + const editButtons = screen.getAllByRole('link', { name: 'edit-action' }); + expect(editButtons).toHaveLength(2); + editButtons.forEach((button) => { + expect(button).not.toBeDisabled(); + }); + + const moreActionsButtons = screen.getAllByRole('button', { name: 'more-actions' }); + expect(moreActionsButtons).toHaveLength(2); + moreActionsButtons.forEach((button) => { + expect(button).not.toBeDisabled(); + }); + }); + }); + + describe('Vanilla Alertmanager ', () => { + beforeEach(() => { + setupVanillaAlertmanagerFlavoredServer(server); + }); + + beforeAll(() => { + grantUserPermissions([ + AccessControlAction.AlertingNotificationsExternalRead, + AccessControlAction.AlertingNotificationsExternalWrite, + ]); + + const alertManager = mockDataSource({ + name: VANILLA_ALERTMANAGER_DATASOURCE_UID, + uid: VANILLA_ALERTMANAGER_DATASOURCE_UID, + type: DataSourceType.Alertmanager, + jsonData: { + implementation: AlertManagerImplementation.prometheus, + handleGrafanaManagedAlerts: true, + }, + }); + + setupDataSources(alertManager); + }); + + it("should not allow any editing because it's not supported", async () => { + render( + + + + + + ); + + await waitFor(async () => { + expect(screen.getByText('Loading...')).toBeInTheDocument(); + await waitForElementToBeRemoved(screen.getByText('Loading...')); + expect(screen.queryByTestId(selectors.components.Alert.alertV2('error'))).not.toBeInTheDocument(); + }); + + expect(screen.queryByRole('link', { name: 'add contact point' })).not.toBeInTheDocument(); + + const viewProvisioned = screen.getByRole('link', { name: 'view-action' }); + expect(viewProvisioned).toBeInTheDocument(); + expect(viewProvisioned).not.toBeDisabled(); }); }); }); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx similarity index 98% rename from public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx rename to public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index c6e8cc2fe97..368cd23aa5f 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -45,14 +45,14 @@ import { Spacer } from '../Spacer'; import { Strong } from '../Strong'; import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter'; import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter'; -import { GlobalConfigAlert } from '../receivers/ReceiversAndTemplatesView'; -import { UnusedContactPointBadge } from '../receivers/ReceiversTable'; import { ReceiverMetadataBadge } from '../receivers/grafanaAppReceivers/ReceiverMetadataBadge'; import { ReceiverPluginMetadata } from '../receivers/grafanaAppReceivers/useReceiversMetadata'; -import { ContactPointsFilter } from './ContactPointsFilter'; -import { useDeleteContactPointModal } from './Modals'; import { NotificationTemplates } from './NotificationTemplates'; +import { ContactPointsFilter } from './components/ContactPointsFilter'; +import { GlobalConfigAlert } from './components/GlobalConfigAlert'; +import { useDeleteContactPointModal } from './components/Modals'; +import { UnusedContactPointBadge } from './components/UnusedBadge'; import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY, @@ -135,6 +135,7 @@ const ContactPoints = () => { {addContactPointSupported && ( { showExportDrawer(ALL_CONTACT_POINTS)} > @@ -364,6 +366,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { openExportDrawer(name)} @@ -386,6 +389,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { > ({ - name: 'PromManager', - type: DataSourceType.Alertmanager, - jsonData: { - implementation: AlertManagerImplementation.prometheus, - }, - }), -}; - -const renderReceivers = (alertManagerSourceName?: string) => { - locationService.push('/alerting/notifications'); - - return render( - - - - - - ); -}; - -const ui = { - newContactPointButton: byRole('link', { name: /add contact point/i }), - saveContactButton: byRole('button', { name: /save contact point/i }), - newContactPointIntegrationButton: byRole('button', { name: /add contact point integration/i }), - testContactPointButton: byRole('button', { name: /Test/ }), - testContactPointModal: byRole('heading', { name: /test contact point/i }), - customContactPointOption: byRole('radio', { name: /custom/i }), - contactPointAnnotationSelect: (idx: number) => byTestId(`annotation-key-${idx}`), - contactPointAnnotationValue: (idx: number) => byTestId(`annotation-value-${idx}`), - contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`), - contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`), - testContactPoint: byRole('button', { name: /send test notification/i }), - cancelButton: byTestId('cancel-button'), - - receiversTable: byTestId('dynamic-table'), - templatesTable: byTestId('templates-table'), - alertManagerPicker: byTestId('alertmanager-picker'), - - channelFormContainer: byTestId('item-container'), - - contactPointsCollapseToggle: byTestId('collapse-toggle'), - - inputs: { - name: byPlaceholderText('Name'), - email: { - addresses: byLabelText(/Addresses/), - toEmails: byLabelText(/To/), - }, - hipchat: { - url: byLabelText('Hip Chat Url'), - apiKey: byLabelText('API Key'), - }, - slack: { - webhookURL: byLabelText(/Webhook URL/i), - }, - webhook: { - URL: byLabelText(/The endpoint to send HTTP POST requests to/i), - }, - }, -}; - -const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise => { - await userEvent.click(byRole('combobox').get(selectElement)); - await selectOptionInTest(selectElement, optionText); -}; - -document.addEventListener('click', interceptLinkClicks); -const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 }; - -const useGetGrafanaReceiverTypeCheckerMock = jest.spyOn(grafanaApp, 'useGetGrafanaReceiverTypeChecker'); - -const server = setupMswServer(); - -describe('Receivers', () => { - beforeEach(() => { - server.resetHandlers(); - jest.resetAllMocks(); - - mockApi(server).grafanaNotifiers(grafanaNotifiersMock); - mockApi(server).plugins.getPluginSettings(onCallPluginMetaMock); - - useGetGrafanaReceiverTypeCheckerMock.mockReturnValue(() => undefined); - mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); - mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); - mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false }); - mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - - store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); - - grantUserPermissions([ - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalRead, - AccessControlAction.AlertingNotificationsExternalWrite, - ]); - }); - - it('Template and receiver tables are rendered, alertmanager can be selected, no notification errors', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - mocks.api.fetchConfig.mockImplementation((name) => - Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig) - ); - renderReceivers(); - - // check that by default grafana templates & receivers are fetched rendered in appropriate tables - await ui.receiversTable.find(); - let templatesTable = await ui.templatesTable.find(); - let templateRows = templatesTable.querySelectorAll('tbody tr'); - expect(templateRows).toHaveLength(3); - expect(templateRows[0]).toHaveTextContent('first template'); - expect(templateRows[1]).toHaveTextContent('second template'); - expect(templateRows[2]).toHaveTextContent('third template'); - let receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(receiverRows[0]).toHaveTextContent('default'); - expect(receiverRows[1]).toHaveTextContent('critical'); - expect(receiverRows).toHaveLength(2); - - expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1); - expect(mocks.api.fetchConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME); - expect(mocks.api.fetchNotifiers).toHaveBeenCalledTimes(1); - expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual(undefined); - }); - - it('Grafana receiver can be tested', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - - mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); - - renderReceivers(); - - // go to new contact point page - await userEvent.click(await ui.newContactPointButton.find()); - - await byRole('heading', { name: /create contact point/i }).find(); - - expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new'); - - // type in a name for the new receiver - await userEvent.type(ui.inputs.name.get(), 'my new receiver'); - - // enter some email - const email = ui.inputs.email.addresses.get(); - await userEvent.clear(email); - await userEvent.type(email, 'tester@grafana.com'); - - // try to test the contact point - await userEvent.click(await ui.testContactPointButton.find()); - - await waitFor(() => expect(ui.testContactPointModal.get()).toBeInTheDocument(), { timeout: 1000 }); - await userEvent.click(ui.customContactPointOption.get()); - - // enter custom annotations and labels - await userEvent.type(screen.getByPlaceholderText('Enter a description...'), 'Test contact point'); - await userEvent.type(ui.contactPointLabelKey(0).get(), 'foo'); - await userEvent.type(ui.contactPointLabelValue(0).get(), 'bar'); - await userEvent.click(ui.testContactPoint.get()); - - await waitFor(() => expect(mocks.api.testReceivers).toHaveBeenCalled()); - - expect(mocks.api.testReceivers).toHaveBeenCalledWith( - 'grafana', - [ - { - grafana_managed_receiver_configs: [ - { - disableResolveMessage: false, - name: 'test', - secureSettings: {}, - settings: { addresses: 'tester@grafana.com', singleEmail: false }, - type: 'email', - }, - ], - name: 'test', - }, - ], - { annotations: { description: 'Test contact point' }, labels: { foo: 'bar' } } - ); - }); - - it('Grafana receiver can be created', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - - mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); - mocks.api.updateConfig.mockResolvedValue(); - renderReceivers(); - - // go to new contact point page - await userEvent.click(await ui.newContactPointButton.find()); - - await byRole('heading', { name: /create contact point/i }).find(); - expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new'); - - // type in a name for the new receiver - await userEvent.type(byPlaceholderText('Name').get(), 'my new receiver'); - - // check that default email form is rendered - await ui.inputs.email.addresses.find(); - - // select hipchat - await clickSelectOption(byTestId('items.0.type').get(), 'HipChat'); - - // check that email options are gone and hipchat options appear - expect(ui.inputs.email.addresses.query()).not.toBeInTheDocument(); - - const urlInput = ui.inputs.hipchat.url.get(); - const apiKeyInput = ui.inputs.hipchat.apiKey.get(); - - await userEvent.type(urlInput, 'http://hipchat'); - await userEvent.type(apiKeyInput, 'foobarbaz'); - - await userEvent.click(await ui.saveContactButton.find()); - - // see that we're back to main page and proper api calls have been made - await ui.receiversTable.find(); - expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1); - expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2); - expect(locationService.getLocation().pathname).toEqual('/alerting/notifications'); - expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, { - ...someGrafanaAlertManagerConfig, - alertmanager_config: { - ...someGrafanaAlertManagerConfig.alertmanager_config, - receivers: [ - ...(someGrafanaAlertManagerConfig.alertmanager_config.receivers ?? []), - { - name: 'my new receiver', - grafana_managed_receiver_configs: [ - { - disableResolveMessage: false, - name: 'my new receiver', - secureSettings: {}, - settings: { - apiKey: 'foobarbaz', - url: 'http://hipchat', - }, - type: 'hipchat', - }, - ], - }, - ], - }, - }); - }); - - it('Hides create contact point button for users without permission', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - - mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); - mocks.api.updateConfig.mockResolvedValue(); - grantUserPermissions([ - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsExternalRead, - ]); - mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); - renderReceivers(); - await ui.receiversTable.find(); - - expect(ui.newContactPointButton.query()).not.toBeInTheDocument(); - }); - - it('Cloud alertmanager receiver can be edited', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - - mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig); - mocks.api.updateConfig.mockResolvedValue(); - renderReceivers('CloudManager'); - - // click edit button for the receiver - await ui.receiversTable.find(); - const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(receiverRows[0]).toHaveTextContent('cloud-receiver'); - await userEvent.click(byTestId('edit').get(receiverRows[0])); - - // check that form is open - await byRole('heading', { name: /update contact point/i }).find(); - expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit'); - expect(ui.channelFormContainer.queryAll()).toHaveLength(2); - - // delete the email channel - expect(ui.channelFormContainer.queryAll()).toHaveLength(2); - await userEvent.click(byTestId('items.0.delete-button').get()); - expect(ui.channelFormContainer.queryAll()).toHaveLength(1); - - // modify webhook url - const slackContainer = ui.channelFormContainer.get(); - await userEvent.click(byText('Optional Slack settings').get(slackContainer)); - await userEvent.type(ui.inputs.slack.webhookURL.get(slackContainer), 'http://newgreaturl'); - - // add confirm button to action - await userEvent.click(byText(/Actions \(1\)/i).get(slackContainer)); - await userEvent.click(await byTestId('items.1.settings.actions.0.confirm.add-button').find()); - const confirmSubform = byTestId('items.1.settings.actions.0.confirm.container').get(); - await userEvent.type(byLabelText('Text').get(confirmSubform), 'confirm this'); - - // delete a field - await userEvent.click(byText(/Fields \(2\)/i).get(slackContainer)); - await userEvent.click(byTestId('items.1.settings.fields.0.delete-button').get()); - byText(/Fields \(1\)/i).get(slackContainer); - - // add another channel - await userEvent.click(ui.newContactPointIntegrationButton.get()); - await clickSelectOption(await byTestId('items.2.type').find(), 'Webhook'); - await userEvent.type(await ui.inputs.webhook.URL.find(), 'http://webhookurl'); - - await userEvent.click(ui.saveContactButton.get()); - - // see that we're back to main page and proper api calls have been made - await ui.receiversTable.find(); - expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1); - expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2); - - expect(locationService.getLocation().pathname).toEqual('/alerting/notifications'); - expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', { - ...someCloudAlertManagerConfig, - alertmanager_config: { - ...someCloudAlertManagerConfig.alertmanager_config, - receivers: [ - { - name: 'cloud-receiver', - slack_configs: [ - { - actions: [ - { - confirm: { - text: 'confirm this', - }, - text: 'action1text', - type: 'action1type', - url: 'http://action1', - }, - ], - api_url: 'http://slack1http://newgreaturl', - channel: '#mychannel', - fields: [ - { - short: false, - title: 'field2', - value: 'text2', - }, - ], - link_names: false, - send_resolved: false, - short_fields: false, - }, - ], - webhook_configs: [ - { - send_resolved: true, - url: 'http://webhookurl', - }, - ], - }, - ], - }, - }); - }); - - it('Prometheus Alertmanager receiver cannot be edited', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - - mocks.api.fetchStatus.mockResolvedValue({ - ...someCloudAlertManagerStatus, - config: someCloudAlertManagerConfig.alertmanager_config, - }); - renderReceivers(dataSources.promAlertManager.name); - - await ui.receiversTable.find(); - // there's no templates table for vanilla prom, API does not return templates - expect(ui.templatesTable.query()).not.toBeInTheDocument(); - - // click view button on the receiver - const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(receiverRows[0]).toHaveTextContent('cloud-receiver'); - expect(byTestId('edit').query(receiverRows[0])).not.toBeInTheDocument(); - await userEvent.click(byTestId('view').get(receiverRows[0])); - - // check that form is open - await byRole('heading', { name: /contact point/i }).find(); - expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit'); - - const channelForms = ui.channelFormContainer.queryAll(); - expect(channelForms).toHaveLength(2); - - // check that inputs are disabled and there is no save button - expect(ui.inputs.name.queryAll()[0]).toHaveAttribute('readonly'); - expect(ui.inputs.email.toEmails.get(channelForms[0])).toHaveAttribute('readonly'); - expect(ui.inputs.slack.webhookURL.get(channelForms[1])).toHaveAttribute('readonly'); - expect(ui.newContactPointButton.query()).not.toBeInTheDocument(); - expect(ui.testContactPointButton.query()).not.toBeInTheDocument(); - expect(ui.saveContactButton.query()).not.toBeInTheDocument(); - expect(ui.cancelButton.query()).toBeInTheDocument(); - - expect(mocks.api.fetchConfig).not.toHaveBeenCalled(); - expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); - }); - - it('Loads config from status endpoint if there is no user config', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - // loading an empty config with make it fetch config from status endpoint - mocks.api.fetchConfig.mockResolvedValue({ - template_files: {}, - alertmanager_config: {}, - }); - mocks.api.fetchStatus.mockResolvedValue(someCloudAlertManagerStatus); - renderReceivers('CloudManager'); - - // check that receiver from the default config is represented - await ui.receiversTable.find(); - const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(receiverRows[0]).toHaveTextContent('default-email'); - - // check that both config and status endpoints were called - expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1); - expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager'); - expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); - expect(mocks.api.fetchStatus).toHaveBeenLastCalledWith('CloudManager'); - }); - - it('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - - mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true }); - mocks.api.fetchConfig.mockRejectedValue({ message: 'alertmanager storage object not found' }); - - renderReceivers('CloudManager'); - - const templatesTable = await ui.templatesTable.find(); - const receiversTable = await ui.receiversTable.find(); - - expect(templatesTable).toBeInTheDocument(); - expect(receiversTable).toBeInTheDocument(); - }); - - describe('Contact points health', () => { - it('Should render error notifications when there are some points state ', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); - mocks.api.updateConfig.mockResolvedValue(); - - const receiversMock: ContactPointsState = { - receivers: { - default: { - active: true, - notifiers: { - email: [ - { - lastNotifyAttemptError: - 'establish connection to server: dial tcp: lookup smtp.example.org on 8.8.8.8:53: no such host', - lastNotifyAttempt: '2022-09-19T15:34:40.696Z', - lastNotifyAttemptDuration: '117.2455ms', - name: 'email[0]', - }, - ], - }, - errorCount: 1, - }, - critical: { - active: true, - notifiers: { - slack: [ - { - lastNotifyAttempt: '2022-09-19T15:34:40.696Z', - lastNotifyAttemptDuration: '117.2455ms', - name: 'slack[0]', - }, - ], - pagerduty: [ - { - lastNotifyAttempt: '2022-09-19T15:34:40.696Z', - lastNotifyAttemptDuration: '117.2455ms', - name: 'pagerduty', - }, - ], - }, - errorCount: 0, - }, - }, - errorCount: 1, - }; - - mocks.hooks.useGetContactPointsState.mockReturnValue(receiversMock); - renderReceivers(); - - // - await ui.receiversTable.find(); - - const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(receiverRows[0]).toHaveTextContent('1 error'); - expect(receiverRows[1]).not.toHaveTextContent('error'); - expect(receiverRows[1]).toHaveTextContent('OK'); - - //should show error in contact points when expanding - // expand contact point detail for default 2 emails - 2 errors - await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[0])); - const defaultDetailTable = screen.getAllByTestId('dynamic-table')[1]; - expect(byText('Error').getAll(defaultDetailTable)).toHaveLength(1); - - // expand contact point detail for slack and pagerduty - 0 errors - await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[1])); - const criticalDetailTable = screen.getAllByTestId('dynamic-table')[2]; - expect(byText('Error').query(criticalDetailTable)).toBeNull(); - expect(byText('OK').getAll(criticalDetailTable)).toHaveLength(2); - }); - it('Should render no attempt message when there are some points state with null lastNotifyAttempt, and "-" in null values', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); - mocks.api.updateConfig.mockResolvedValue(); - - const receiversMock: ContactPointsState = { - receivers: { - default: { - active: true, - notifiers: { - email: [ - { - lastNotifyAttemptError: - 'establish connection to server: dial tcp: lookup smtp.example.org on 8.8.8.8:53: no such host', - lastNotifyAttempt: '2022-09-19T15:34:40.696Z', - lastNotifyAttemptDuration: '117.2455ms', - name: 'email[0]', - }, - ], - }, - errorCount: 1, - }, - critical: { - active: true, - notifiers: { - slack: [ - { - lastNotifyAttempt: '0001-01-01T00:00:00.000Z', - lastNotifyAttemptDuration: '0s', - name: 'slack[0]', - }, - ], - pagerduty: [ - { - lastNotifyAttempt: '2022-09-19T15:34:40.696Z', - lastNotifyAttemptDuration: '117.2455ms', - name: 'pagerduty', - }, - ], - }, - errorCount: 0, - }, - }, - errorCount: 1, - }; - - mocks.hooks.useGetContactPointsState.mockReturnValue(receiversMock); - renderReceivers(); - - // - await ui.receiversTable.find(); - - const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(receiverRows[0]).toHaveTextContent('1 error'); - expect(receiverRows[1]).not.toHaveTextContent('error'); - expect(receiverRows[1]).toHaveTextContent('No attempts'); - - //should show error in contact points when expanding - // expand contact point detail for default 2 emails - 2 errors - await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[0])); - const defaultDetailTable = screen.getAllByTestId('dynamic-table')[1]; - expect(byText('Error').getAll(defaultDetailTable)).toHaveLength(1); - - // expand contact point detail for slack and pagerduty - 0 errors - await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[1])); - const criticalDetailTableRows = within(screen.getAllByTestId('dynamic-table')[2]).getAllByTestId('row'); - // should render slack item with no attempt - expect(criticalDetailTableRows[0]).toHaveTextContent('No attempt'); - expect(criticalDetailTableRows[0]).toHaveTextContent('--'); - //should render pagerduty with no attempt - expect(criticalDetailTableRows[1]).toHaveTextContent('OK'); - expect(criticalDetailTableRows[1]).toHaveTextContent('117.2455ms'); - }); - - it('Should not render error notifications when fetching contact points state raises 404 error ', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); - mocks.api.updateConfig.mockResolvedValue(); - - mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); - renderReceivers(); - - await ui.receiversTable.find(); - //contact points are not expandable - expect(ui.contactPointsCollapseToggle.query()).not.toBeInTheDocument(); - //should render receivers, only one dynamic table - let receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(receiverRows[0]).toHaveTextContent('default'); - expect(receiverRows[1]).toHaveTextContent('critical'); - expect(receiverRows).toHaveLength(2); - }); - - it('Should render "Unused" warning if a contact point is not used in route configuration', async () => { - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - mocks.api.updateConfig.mockResolvedValue(); - mocks.api.fetchConfig.mockResolvedValue({ - ...someGrafanaAlertManagerConfig, - alertmanager_config: { ...someGrafanaAlertManagerConfig.alertmanager_config, route: { receiver: 'default' } }, - }); - - mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); - renderReceivers(); - - await ui.receiversTable.find(); - //contact points are not expandable - expect(ui.contactPointsCollapseToggle.query()).not.toBeInTheDocument(); - //should render receivers, only one dynamic table - let receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - - expect(receiverRows).toHaveLength(2); - expect(receiverRows[0]).toHaveTextContent('default'); - expect(receiverRows[1]).toHaveTextContent('critical'); - expect(receiverRows[1]).toHaveTextContent('Unused'); - }); - }); -}); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx deleted file mode 100644 index 72b8b78fef8..00000000000 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useEffect } from 'react'; -import { Route, RouteChildrenProps, Switch } from 'react-router-dom'; - -import { Alert, LoadingPlaceholder } from '@grafana/ui'; -import { useDispatch } from 'app/types'; - -import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; -import { useAlertmanager } from '../../state/AlertmanagerContext'; -import { fetchGrafanaNotifiersAction } from '../../state/actions'; -import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; -import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning'; -import { DuplicateTemplateView } from '../receivers/DuplicateTemplateView'; -import { EditReceiverView } from '../receivers/EditReceiverView'; -import { EditTemplateView } from '../receivers/EditTemplateView'; -import { GlobalConfigForm } from '../receivers/GlobalConfigForm'; -import { NewReceiverView } from '../receivers/NewReceiverView'; -import { NewTemplateView } from '../receivers/NewTemplateView'; -import { ReceiversAndTemplatesView } from '../receivers/ReceiversAndTemplatesView'; - -export interface NotificationErrorProps { - errorCount: number; -} - -const Receivers = () => { - const { selectedAlertmanager: alertManagerSourceName } = useAlertmanager(); - const dispatch = useDispatch(); - const { currentData: config, isLoading: loading, error } = useAlertmanagerConfig(alertManagerSourceName); - - const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); - - useEffect(() => { - if ( - alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && - !(receiverTypes.result || receiverTypes.loading || receiverTypes.error) - ) { - dispatch(fetchGrafanaNotifiersAction()); - } - }, [alertManagerSourceName, dispatch, receiverTypes]); - - if (!alertManagerSourceName) { - return null; - } - - return ( - <> - {error && !loading && ( - - {error.message || 'Unknown error.'} - - )} - - {loading && !config && } - {config && !error && ( - - - - - - - - - {({ match }: RouteChildrenProps<{ name: string }>) => - match?.params.name && ( - - ) - } - - - {({ match }: RouteChildrenProps<{ name: string }>) => - match?.params.name && ( - - ) - } - - - - - - {({ match }: RouteChildrenProps<{ name: string }>) => - match?.params.name && ( - - ) - } - - - - - - )} - > - ); -}; - -export default Receivers; diff --git a/public/app/features/alerting/unified/components/contact-points/NewContactPoint.test.tsx b/public/app/features/alerting/unified/components/contact-points/NewContactPoint.test.tsx new file mode 100644 index 00000000000..2269eede098 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/NewContactPoint.test.tsx @@ -0,0 +1,116 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; +import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector'; + +import { AccessControlAction } from 'app/types'; + +import { setupMswServer } from '../../mockApi'; +import { grantUserPermissions } from '../../mocks'; +import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; + +import NewContactPoint from './NewContactPoint'; +import setupGrafanaManagedServer, { + setupSaveEndpointMock, + setupTestEndpointMock, +} from './__mocks__/grafanaManagedServer'; + +import 'core-js/stable/structured-clone'; + +const server = setupMswServer(); +const user = userEvent.setup(); + +beforeEach(() => { + grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]); + setupGrafanaManagedServer(server); +}); + +it('should be able to test and save a receiver', async () => { + const testMock = setupTestEndpointMock(server); + const saveMock = setupSaveEndpointMock(server); + + render( + + + , + { wrapper: TestProvider } + ); + + // wait for loading to be done + // type in a name for the new receiver + await waitFor(() => { + user.type(ui.inputs.name.get(), 'my new receiver'); + }); + + // enter some email + const email = ui.inputs.email.addresses.get(); + await user.clear(email); + await user.type(email, 'tester@grafana.com'); + + // try to test the contact point + await user.click(await ui.testContactPointButton.find()); + + await waitFor( + () => { + expect(ui.testContactPointModal.get()).toBeInTheDocument(); + }, + { timeout: 1000 } + ); + await user.click(ui.customContactPointOption.get()); + + // enter custom annotations and labels + await user.type(screen.getByPlaceholderText('Enter a description...'), 'Test contact point'); + await user.type(ui.contactPointLabelKey(0).get(), 'foo'); + await user.type(ui.contactPointLabelValue(0).get(), 'bar'); + + // click test + await user.click(ui.testContactPoint.get()); + + // we shouldn't be testing implementation details but when the request is successful + // it can't seem to assert on the success toast + await waitFor(() => { + expect(testMock).toHaveBeenCalled(); + expect(testMock.mock.lastCall).toMatchSnapshot(); + }); + + await user.click(ui.saveContactButton.get()); + await waitFor(() => { + expect(saveMock).toHaveBeenCalled(); + expect(saveMock.mock.lastCall).toMatchSnapshot(); + }); +}); + +const ui = { + saveContactButton: byRole('button', { name: /save contact point/i }), + newContactPointIntegrationButton: byRole('button', { name: /add contact point integration/i }), + testContactPointButton: byRole('button', { name: /Test/ }), + testContactPointModal: byRole('heading', { name: /test contact point/i }), + customContactPointOption: byRole('radio', { name: /custom/i }), + contactPointAnnotationSelect: (idx: number) => byTestId(`annotation-key-${idx}`), + contactPointAnnotationValue: (idx: number) => byTestId(`annotation-value-${idx}`), + contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`), + contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`), + testContactPoint: byRole('button', { name: /send test notification/i }), + cancelButton: byTestId('cancel-button'), + + channelFormContainer: byTestId('item-container'), + + inputs: { + name: byPlaceholderText('Name'), + email: { + addresses: byLabelText(/Addresses/), + toEmails: byLabelText(/To/), + }, + hipchat: { + url: byLabelText('Hip Chat Url'), + apiKey: byLabelText('API Key'), + }, + slack: { + webhookURL: byLabelText(/Webhook URL/i), + }, + webhook: { + URL: byLabelText(/The endpoint to send HTTP POST requests to/i), + }, + }, +}; diff --git a/public/app/features/alerting/unified/components/contact-points/NewContactPoint.tsx b/public/app/features/alerting/unified/components/contact-points/NewContactPoint.tsx index e1ca42cbd90..0151e262768 100644 --- a/public/app/features/alerting/unified/components/contact-points/NewContactPoint.tsx +++ b/public/app/features/alerting/unified/components/contact-points/NewContactPoint.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RouteChildrenProps } from 'react-router-dom'; import { Alert } from '@grafana/ui'; @@ -7,7 +6,7 @@ import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { NewReceiverView } from '../receivers/NewReceiverView'; -const NewContactPoint = (_props: RouteChildrenProps) => { +const NewContactPoint = () => { const { selectedAlertmanager } = useAlertmanager(); const { data, isLoading, error } = useAlertmanagerConfig(selectedAlertmanager); 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 index b5d9f3f2c82..5bfacc2cdcc 100644 --- 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 @@ -1,6 +1,14 @@ { "template_files": {}, "alertmanager_config": { + "route": { + "receiver": "grafana-default-email", + "routes": [ + { + "receiver": "provisioned-contact-point" + } + ] + }, "receivers": [ { "name": "grafana-default-email", diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.mimir.config.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.mimir.config.mock.json index 9bcdc5ce78e..46501c9a0d5 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.mimir.config.mock.json +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.mimir.config.mock.json @@ -21,13 +21,9 @@ "group_wait": "30s", "matchers": [], "mute_time_intervals": [], - "receiver": "email", + "receiver": "some webhook", "repeat_interval": "5h", - "routes": [ - { - "receiver": "mixed" - } - ] + "routes": [] }, "templates": [] } diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.vanilla.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.vanilla.mock.json new file mode 100644 index 00000000000..af6b05fd87b --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.vanilla.mock.json @@ -0,0 +1,70 @@ +{ + "cluster": { + "name": "01HGAVE6N6RG55BZW8N5ZJ12T3", + "peers": [{ "address": "172.19.0.6:9094", "name": "01HGAVE6N6RG55BZW8N5ZJ12T3" }], + "status": "settling" + }, + "config": { + "global": { + "resolve_timeout": "5m", + "http_config": { + "tls_config": { "insecure_skip_verify": false }, + "follow_redirects": true, + "enable_http2": true, + "proxy_url": null + }, + "smtp_hello": "localhost", + "smtp_smarthost": "", + "smtp_require_tls": true, + "pagerduty_url": "https://events.pagerduty.com/v2/enqueue", + "opsgenie_api_url": "https://api.opsgenie.com/", + "wechat_api_url": "https://qyapi.weixin.qq.com/cgi-bin/", + "victorops_api_url": "https://alert.victorops.com/integrations/generic/20131114/alert/", + "telegram_api_url": "https://api.telegram.org", + "webex_api_url": "https://webexapis.com/v1/messages" + }, + "route": { + "receiver": "web.hook", + "group_by": ["alertname"], + "group_wait": "30s", + "group_interval": "5m", + "repeat_interval": "1h" + }, + "inhibit_rules": [ + { + "source_match": { "severity": "critical" }, + "target_match": { "severity": "warning" }, + "equal": ["alertname", "dev", "instance"] + } + ], + "templates": [], + "receivers": [ + { + "name": "web.hook", + "webhook_configs": [ + { + "send_resolved": true, + "http_config": { + "tls_config": { "insecure_skip_verify": false }, + "follow_redirects": true, + "enable_http2": true, + "proxy_url": null + }, + "url": "\u003csecret\u003e", + "url_file": "", + "max_alerts": 0 + } + ] + } + ] + }, + "uptime": "2023-11-28T11:36:10.663Z", + "versionInfo": { + "branch": "HEAD", + "buildDate": "20230824-11:09:02", + "buildUser": "root@520df6c16a84", + "goVersion": "go1.20.7", + "revision": "d7b4f0c7322e7151d6e3b1e31cbc15361e295d8d", + "version": "0.26.0" + } +} diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts index e92e20166b8..a93b7b046c0 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/grafanaManagedServer.ts @@ -1,5 +1,5 @@ import { rest } from 'msw'; -import { SetupServer } from 'msw/lib/node'; +import { SetupServer } from 'msw/node'; import { AlertmanagerChoice, AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { ReceiversStateDTO } from 'app/types'; @@ -36,3 +36,33 @@ export default (server: SetupServer) => { return server; }; + +export const setupTestEndpointMock = (server: SetupServer) => { + const mock = jest.fn(); + + server.use( + rest.post('/api/alertmanager/grafana/config/api/v1/receivers/test', async (req, res, ctx) => { + const requestBody = await req.json(); + mock(requestBody); + + return res.once(ctx.status(200)); + }) + ); + + return mock; +}; + +export const setupSaveEndpointMock = (server: SetupServer) => { + const mock = jest.fn(); + + server.use( + rest.post('/api/alertmanager/grafana/config/api/v1/alerts', async (req, res, ctx) => { + const requestBody = await req.json(); + mock(requestBody); + + return res.once(ctx.status(200)); + }) + ); + + return mock; +}; diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts deleted file mode 100644 index 951a7bc1eff..00000000000 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { rest } from 'msw'; - -import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; -import { ReceiversStateDTO } from 'app/types'; - -import { setupMswServer } from '../../../mockApi'; - -import alertmanagerMock from './alertmanager.config.mock.json'; -import receiversMock from './receivers.mock.json'; - -const server = setupMswServer(); - -server.use( - // this endpoint is a grafana built-in alertmanager - rest.get('/api/alertmanager/grafana/config/api/v1/alerts', (_req, res, ctx) => - res(ctx.json(alertmanagerMock)) - ), - // this endpoint is only available for the built-in alertmanager - rest.get('/api/alertmanager/grafana/config/api/v1/receivers', (_req, res, ctx) => - res(ctx.json(receiversMock)) - ) -); - -export default server; diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/vanillaAlertmanagerServer.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/vanillaAlertmanagerServer.ts new file mode 100644 index 00000000000..b834a5a515f --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/vanillaAlertmanagerServer.ts @@ -0,0 +1,21 @@ +import { rest } from 'msw'; +import { SetupServer } from 'msw/lib/node'; + +import { AlertmanagerStatus } from 'app/plugins/datasource/alertmanager/types'; + +import vanillaAlertManagerConfig from './alertmanager.vanilla.mock.json'; + +// this one emulates a mimir server setup +export const VANILLA_ALERTMANAGER_DATASOURCE_UID = 'alertmanager'; + +export default (server: SetupServer) => { + server.use( + rest.get(`/api/alertmanager/${VANILLA_ALERTMANAGER_DATASOURCE_UID}/api/v2/status`, (_req, res, ctx) => + res(ctx.json(vanillaAlertManagerConfig)) + ), + // this endpoint will respond if the OnCall plugin is installed + rest.get('/api/plugins/grafana-oncall-app/settings', (_req, res, ctx) => res(ctx.status(404))) + ); + + return server; +}; diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap new file mode 100644 index 00000000000..028e88fcac9 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should be able to test and save a receiver 1`] = ` +[ + { + "alert": { + "annotations": { + "description": "Test contact point", + }, + "labels": { + "foo": "bar", + }, + }, + "receivers": [ + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "test", + "secureSettings": {}, + "settings": { + "addresses": "nteews treerc@egirvaefrana.com", + "singleEmail": false, + }, + "type": "email", + }, + ], + "name": "test", + }, + ], + }, +] +`; + +exports[`should be able to test and save a receiver 2`] = ` +[ + { + "alertmanager_config": { + "receivers": [ + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "grafana-default-email", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "xeKQrBrnk", + }, + ], + "name": "grafana-default-email", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "provisioned-contact-point", + "provenance": "api", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "s8SdCVjnk", + }, + ], + "name": "provisioned-contact-point", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "lotsa-emails", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", + }, + ], + "name": "lotsa-emails", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts", + }, + "type": "slack", + "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", + }, + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts2", + }, + "type": "slack", + "uid": "b286a3be-f690-49e2-8605-b075cbace2df", + }, + ], + "name": "Slack with multiple channels", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "my ", + "secureSettings": {}, + "settings": { + "addresses": "nteews treerc@egirvaefrana.com", + "singleEmail": false, + }, + "type": "email", + }, + ], + "name": "my ", + }, + ], + "route": { + "receiver": "grafana-default-email", + "routes": [ + { + "receiver": "provisioned-contact-point", + }, + ], + }, + }, + "template_file_provenances": {}, + "template_files": {}, + }, +] +`; diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap index ca6b3c77572..dd84a65a6ae 100644 --- a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap @@ -30,7 +30,7 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, ], "name": "grafana-default-email", - "numberOfPolicies": 0, + "numberOfPolicies": 1, }, { "grafana_managed_receiver_configs": [ @@ -87,7 +87,7 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, ], "name": "provisioned-contact-point", - "numberOfPolicies": 0, + "numberOfPolicies": 1, }, { "grafana_managed_receiver_configs": [ diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPointsFilter.tsx b/public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx similarity index 95% rename from public/app/features/alerting/unified/components/contact-points/ContactPointsFilter.tsx rename to public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx index c21ceed72b8..f1d6ee5c31a 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPointsFilter.tsx +++ b/public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx @@ -5,7 +5,7 @@ import { useDebounce } from 'react-use'; import { Stack } from '@grafana/experimental'; import { Button, Field, Icon, Input, useStyles2 } from '@grafana/ui'; -import { useURLSearchParams } from '../../hooks/useURLSearchParams'; +import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; const ContactPointsFilter = () => { const styles = useStyles2(getStyles); diff --git a/public/app/features/alerting/unified/components/contact-points/GlobalConfig.tsx b/public/app/features/alerting/unified/components/contact-points/components/GlobalConfig.tsx similarity index 74% rename from public/app/features/alerting/unified/components/contact-points/GlobalConfig.tsx rename to public/app/features/alerting/unified/components/contact-points/components/GlobalConfig.tsx index d86cec101ff..1d493119eb6 100644 --- a/public/app/features/alerting/unified/components/contact-points/GlobalConfig.tsx +++ b/public/app/features/alerting/unified/components/contact-points/components/GlobalConfig.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { Alert } from '@grafana/ui'; -import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig'; -import { useAlertmanager } from '../../state/AlertmanagerContext'; -import { GlobalConfigForm } from '../receivers/GlobalConfigForm'; +import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig'; +import { useAlertmanager } from '../../../state/AlertmanagerContext'; +import { GlobalConfigForm } from '../../receivers/GlobalConfigForm'; const NewMessageTemplate = () => { const { selectedAlertmanager } = useAlertmanager(); diff --git a/public/app/features/alerting/unified/components/contact-points/components/GlobalConfigAlert.tsx b/public/app/features/alerting/unified/components/contact-points/components/GlobalConfigAlert.tsx new file mode 100644 index 00000000000..e277b007ac6 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/components/GlobalConfigAlert.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { Alert, LinkButton } from '@grafana/ui'; + +import { AlertmanagerAction } from '../../../hooks/useAbilities'; +import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource'; +import { makeAMLink } from '../../../utils/misc'; +import { Authorize } from '../../Authorize'; + +interface GlobalConfigAlertProps { + alertManagerName: string; +} + +export const GlobalConfigAlert = ({ alertManagerName }: GlobalConfigAlertProps) => { + const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName); + + return ( + + + + For each external Alertmanager you can define global settings, like server addresses, usernames and password, + for all the supported contact points. + + + {isVanillaAM ? 'View global config' : 'Edit global config'} + + + + ); +}; diff --git a/public/app/features/alerting/unified/components/contact-points/Modals.tsx b/public/app/features/alerting/unified/components/contact-points/components/Modals.tsx similarity index 100% rename from public/app/features/alerting/unified/components/contact-points/Modals.tsx rename to public/app/features/alerting/unified/components/contact-points/components/Modals.tsx diff --git a/public/app/features/alerting/unified/components/contact-points/components/UnusedBadge.tsx b/public/app/features/alerting/unified/components/contact-points/components/UnusedBadge.tsx new file mode 100644 index 00000000000..73c11402c99 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/components/UnusedBadge.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { Badge } from '@grafana/ui'; + +export const UnusedContactPointBadge = () => ( + +); diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx deleted file mode 100644 index 532a601f7c2..00000000000 --- a/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; - -import { Alert, LinkButton, Stack } from '@grafana/ui'; -import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; - -import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; -import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; -import { makeAMLink } from '../../utils/misc'; -import { Authorize } from '../Authorize'; - -import { ReceiversSection } from './ReceiversSection'; -import { ReceiversTable } from './ReceiversTable'; -import { TemplatesTable } from './TemplatesTable'; - -interface Props { - config: AlertManagerCortexConfig; - alertManagerName: string; -} - -export const ReceiversAndTemplatesView = ({ config, alertManagerName }: Props) => { - const isGrafanaManagedAlertmanager = alertManagerName === GRAFANA_RULES_SOURCE_NAME; - const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName); - - return ( - - - {/* Vanilla flavored Alertmanager does not support editing notification templates via the UI */} - {!isVanillaAM && } - {/* Grafana manager Alertmanager does not support global config, Mimir and Cortex do */} - {!isGrafanaManagedAlertmanager && } - - ); -}; - -export const TemplatesView = ({ config, alertManagerName }: Props) => { - const [createNotificationTemplateSupported, createNotificationTemplateAllowed] = useAlertmanagerAbility( - AlertmanagerAction.CreateNotificationTemplate - ); - - return ( - - - - ); -}; - -interface GlobalConfigAlertProps { - alertManagerName: string; -} - -export const GlobalConfigAlert = ({ alertManagerName }: GlobalConfigAlertProps) => { - const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName); - - return ( - - - - For each external Alertmanager you can define global settings, like server addresses, usernames and password, - for all the supported contact points. - - - {isVanillaAM ? 'View global config' : 'Edit global config'} - - - - ); -}; diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx deleted file mode 100644 index 229b847de91..00000000000 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { render, screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import { AutoSizerProps } from 'react-virtualized-auto-sizer'; -import { TestProvider } from 'test/helpers/TestProvider'; -import { byRole, byTestId } from 'testing-library-selector'; - -import { setBackendSrv } from '@grafana/runtime'; -import { - AlertManagerCortexConfig, - GrafanaManagedReceiverConfig, - Receiver, -} from 'app/plugins/datasource/alertmanager/types'; -import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } from 'app/types'; - -import { backendSrv } from '../../../../../core/services/backend_srv'; -import * as receiversApi from '../../api/receiversApi'; -import { mockProvisioningApi, setupMswServer } from '../../mockApi'; -import { grantUserPermissions } from '../../mocks'; -import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; -import { fetchGrafanaNotifiersAction } from '../../state/actions'; -import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; - -import { ReceiversTable } from './ReceiversTable'; -import * as receiversMeta from './grafanaAppReceivers/useReceiversMetadata'; -import { ReceiverPluginMetadata } from './grafanaAppReceivers/useReceiversMetadata'; - -jest.mock('react-virtualized-auto-sizer', () => { - return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 }); -}); -jest.mock('@grafana/ui', () => ({ - ...jest.requireActual('@grafana/ui'), - CodeEditor: ({ value }: { value: string }) => , -})); - -const renderReceieversTable = async ( - receivers: Receiver[], - notifiers: NotifierDTO[], - alertmanagerName = 'alertmanager-1' -) => { - const config: AlertManagerCortexConfig = { - template_files: {}, - alertmanager_config: { - receivers, - }, - }; - - const store = configureStore(); - await store.dispatch(fetchGrafanaNotifiersAction.fulfilled(notifiers, 'initial')); - - return render( - - - - - - ); -}; - -const mockGrafanaReceiver = (type: string): GrafanaManagedReceiverConfig => ({ - type, - disableResolveMessage: false, - secureFields: {}, - settings: {}, - name: type, -}); - -const mockNotifier = (type: NotifierType, name: string): NotifierDTO => ({ - type, - name, - description: 'its a mock', - heading: 'foo', - options: [], -}); - -const useReceiversMetadata = jest.spyOn(receiversMeta, 'useReceiversMetadata'); -const useGetContactPointsStateMock = jest.spyOn(receiversApi, 'useGetContactPointsState'); - -setBackendSrv(backendSrv); - -const server = setupMswServer(); - -afterEach(() => { - server.resetHandlers(); -}); - -const ui = { - export: { - dialog: byRole('dialog', { name: 'Drawer title Export' }), - jsonTab: byRole('tab', { name: /JSON/ }), - yamlTab: byRole('tab', { name: /YAML/ }), - editor: byTestId('code-editor'), - copyCodeButton: byRole('button', { name: 'Copy code' }), - downloadButton: byRole('button', { name: 'Download' }), - }, -}; - -describe('ReceiversTable', () => { - beforeEach(() => { - jest.resetAllMocks(); - const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 }; - useGetContactPointsStateMock.mockReturnValue(emptyContactPointsState); - useReceiversMetadata.mockReturnValue(new Map()); - }); - - it('render receivers with grafana notifiers', async () => { - const receivers: Receiver[] = [ - { - name: 'with receivers', - grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')], - }, - { - name: 'without receivers', - grafana_managed_receiver_configs: [], - }, - ]; - - const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')]; - - await renderReceieversTable(receivers, notifiers); - - const rows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(rows).toHaveLength(2); - expect(rows[0]).toHaveTextContent('with receivers'); - expect(rows[0].querySelector('[data-column="Type"]')).toHaveTextContent('Google Chat, Sensu Go'); - expect(rows[1]).toHaveTextContent('without receivers'); - expect(rows[1].querySelector('[data-column="Type"]')).toHaveTextContent(''); - }); - - it('render receivers with alertmanager notifers', async () => { - const receivers: Receiver[] = [ - { - name: 'with receivers', - email_configs: [ - { - to: 'domas.lapinskas@grafana.com', - }, - ], - slack_configs: [], - webhook_configs: [ - { - url: 'http://example.com', - }, - ], - opsgenie_configs: [ - { - foo: 'bar', - }, - ], - foo_configs: [ - { - url: 'bar', - }, - ], - }, - { - name: 'without receivers', - }, - ]; - - await renderReceieversTable(receivers, []); - - const rows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row'); - expect(rows).toHaveLength(2); - expect(rows[0]).toHaveTextContent('with receivers'); - expect(rows[0].querySelector('[data-column="Type"]')).toHaveTextContent('Email, Webhook, OpsGenie, Foo'); - expect(rows[1]).toHaveTextContent('without receivers'); - expect(rows[1].querySelector('[data-column="Type"]')).toHaveTextContent(''); - }); - - describe('RBAC Enabled', () => { - describe('Export button', () => { - const receivers: Receiver[] = [ - { - name: 'with receivers', - grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')], - }, - { - name: 'no receivers', - }, - ]; - - const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')]; - - it('should be visible when user has permissions to read notifications', async () => { - grantUserPermissions([AccessControlAction.AlertingNotificationsRead]); - - await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME); - - const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export'); - expect(buttons).toHaveLength(2); - }); - }); - }); - - describe('Exporter functionality', () => { - it('Should allow exporting receiver', async () => { - // Arrange - mockProvisioningApi(server).exportReceiver({ - yaml: 'Yaml Export Content', - json: 'Json Export Content', - }); - - const user = userEvent.setup(); - - const receivers: Receiver[] = [ - { - name: 'with receivers', - grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')], - }, - { - name: 'no receivers', - }, - ]; - - const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')]; - - grantUserPermissions([AccessControlAction.AlertingNotificationsRead]); - - // Act - await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME); - - const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export'); - // click first export button - await user.click(buttons[0]); - const drawer = await ui.export.dialog.find(); - - // Assert - expect(ui.export.yamlTab.get(drawer)).toHaveAttribute('aria-selected', 'true'); - await waitFor(() => { - expect(ui.export.editor.get(drawer)).toHaveTextContent('Yaml Export Content'); - }); - await user.click(ui.export.jsonTab.get(drawer)); - await waitFor(() => { - expect(ui.export.editor.get(drawer)).toHaveTextContent('Json Export Content'); - }); - expect(ui.export.copyCodeButton.get(drawer)).toBeInTheDocument(); - expect(ui.export.downloadButton.get(drawer)).toBeInTheDocument(); - }); - }); -}); diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx deleted file mode 100644 index 67474161c8d..00000000000 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx +++ /dev/null @@ -1,522 +0,0 @@ -import pluralize from 'pluralize'; -import React, { useMemo, useState } from 'react'; -import { useToggle } from 'react-use'; - -import { dateTime, dateTimeFormat } from '@grafana/data'; -import { Badge, Button, ConfirmModal, Icon, Modal, useStyles2, Stack } from '@grafana/ui'; -import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; -import { ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types'; - -import { useGetContactPointsState } from '../../api/receiversApi'; -import { Authorize } from '../../components/Authorize'; -import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; -import { deleteReceiverAction } from '../../state/actions'; -import { getAlertTableStyles } from '../../styles/table'; -import { SupportedPlugin } from '../../types/pluginBridges'; -import { isReceiverUsed } from '../../utils/alertmanager'; -import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; -import { makeAMLink } from '../../utils/misc'; -import { extractNotifierTypeCounts } from '../../utils/receivers'; -import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; -import { ProvisioningBadge } from '../Provisioning'; -import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter'; -import { ActionIcon } from '../rules/ActionIcon'; - -import { ReceiversSection } from './ReceiversSection'; -import { ReceiverMetadataBadge } from './grafanaAppReceivers/ReceiverMetadataBadge'; -import { ReceiverPluginMetadata, useReceiversMetadata } from './grafanaAppReceivers/useReceiversMetadata'; -import { AlertmanagerConfigHealth, useAlertmanagerConfigHealth } from './useAlertmanagerConfigHealth'; - -interface UpdateActionProps extends ActionProps { - onClickDeleteReceiver: (receiverName: string) => void; -} - -function UpdateActions({ alertManagerName, receiverName, onClickDeleteReceiver }: UpdateActionProps) { - return ( - <> - - - - - onClickDeleteReceiver(receiverName)} - tooltip="Delete contact point" - icon="trash-alt" - /> - - > - ); -} - -interface ActionProps { - alertManagerName: string; - receiverName: string; - canReadSecrets?: boolean; -} - -function ViewAction({ alertManagerName, receiverName }: ActionProps) { - return ( - - - - ); -} - -function ExportAction({ receiverName, canReadSecrets = false }: ActionProps) { - const [showExportDrawer, toggleShowExportDrawer] = useToggle(false); - - return ( - - - {showExportDrawer && ( - - )} - - ); -} - -interface ReceiverErrorProps { - errorCount: number; - errorDetail?: string; - showErrorCount: boolean; - tooltip?: string; -} - -function ReceiverError({ errorCount, errorDetail, showErrorCount, tooltip }: ReceiverErrorProps) { - const text = showErrorCount ? `${errorCount} ${pluralize('error', errorCount)}` : 'Error'; - const tooltipToRender = tooltip ?? errorDetail ?? 'Error'; - - return ; -} - -interface NotifierHealthProps { - errorsByNotifier: number; - errorDetail?: string; - lastNotify: string; -} - -function NotifierHealth({ errorsByNotifier, errorDetail, lastNotify }: NotifierHealthProps) { - const hasErrors = errorsByNotifier > 0; - const noAttempts = isLastNotifyNullDate(lastNotify); - - if (hasErrors) { - return ; - } - - if (noAttempts) { - return <>No attempts>; - } - - return ; -} - -interface ReceiverHealthProps { - errorsByReceiver: number; - someWithNoAttempt: boolean; -} - -function ReceiverHealth({ errorsByReceiver, someWithNoAttempt }: ReceiverHealthProps) { - const hasErrors = errorsByReceiver > 0; - - if (hasErrors) { - return ( - - ); - } - - if (someWithNoAttempt) { - return <>No attempts>; - } - - return ; -} - -const useContactPointsState = (alertManagerName: string) => { - const contactPointsState = useGetContactPointsState(alertManagerName); - const receivers: ReceiversState = contactPointsState?.receivers ?? {}; - const errorStateAvailable = Object.keys(receivers).length > 0; - return { contactPointsState, errorStateAvailable }; -}; - -interface ReceiverItem { - name: string; - types: string[]; - provisioned?: boolean; - grafanaAppReceiverType?: SupportedPlugin; - metadata?: ReceiverPluginMetadata; -} - -interface NotifierStatus { - lastError?: null | string; - lastNotify: string; - lastNotifyDuration: string; - type: string; - sendResolved?: boolean; -} - -type RowTableColumnProps = DynamicTableColumnProps; -type RowItemTableProps = DynamicTableItemProps; - -type NotifierTableColumnProps = DynamicTableColumnProps; -type NotifierItemTableProps = DynamicTableItemProps; - -interface NotifiersTableProps { - notifiersState: NotifiersState; -} - -const isLastNotifyNullDate = (lastNotify: string) => lastNotify === '0001-01-01T00:00:00.000Z'; - -function LastNotify({ lastNotifyDate }: { lastNotifyDate: string }) { - if (isLastNotifyNullDate(lastNotifyDate)) { - return <>{'-'}>; - } else { - return ( - - {`${dateTime(lastNotifyDate).locale('en').fromNow(true)} ago`} - - {`${dateTimeFormat(lastNotifyDate, { format: 'YYYY-MM-DD HH:mm:ss' })}`} - - ); - } -} - -const possibleNullDurations = ['', '0', '0ms', '0s', '0m', '0h', '0d', '0w', '0y']; -const durationIsNull = (duration: string) => possibleNullDurations.includes(duration); - -function NotifiersTable({ notifiersState }: NotifiersTableProps) { - function getNotifierColumns(): NotifierTableColumnProps[] { - return [ - { - id: 'health', - label: 'Health', - renderCell: ({ data: { lastError, lastNotify } }) => { - return ( - - ); - }, - size: 0.5, - }, - { - id: 'name', - label: 'Name', - renderCell: ({ data: { type }, id }) => <>{`${type}[${id}]`}>, - size: 1, - }, - { - id: 'lastNotify', - label: 'Last delivery attempt', - renderCell: ({ data: { lastNotify } }) => , - size: 3, - }, - { - id: 'lastNotifyDuration', - label: 'Last duration', - renderCell: ({ data: { lastNotify, lastNotifyDuration } }) => ( - <>{isLastNotifyNullDate(lastNotify) && durationIsNull(lastNotifyDuration) ? '-' : lastNotifyDuration}> - ), - size: 1, - }, - { - id: 'sendResolved', - label: 'Send resolved', - renderCell: ({ data: { sendResolved } }) => <>{String(Boolean(sendResolved))}>, - size: 1, - }, - ]; - } - - const notifierRows: NotifierItemTableProps[] = Object.entries(notifiersState).flatMap((typeState) => - typeState[1].map((notifierStatus, index) => { - return { - id: index, - data: { - type: typeState[0], - lastError: notifierStatus.lastNotifyAttemptError, - lastNotify: notifierStatus.lastNotifyAttempt, - lastNotifyDuration: notifierStatus.lastNotifyAttemptDuration, - sendResolved: notifierStatus.sendResolved, - }, - }; - }) - ); - - return ; -} - -interface Props { - config: AlertManagerCortexConfig; - alertManagerName: string; -} - -export const ReceiversTable = ({ config, alertManagerName }: Props) => { - const dispatch = useDispatch(); - const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName); - const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); - - const configHealth = useAlertmanagerConfigHealth(config.alertmanager_config); - const { contactPointsState, errorStateAvailable } = useContactPointsState(alertManagerName); - const receiversMetadata = useReceiversMetadata(config.alertmanager_config.receivers ?? []); - - // receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted - const [receiverToDelete, setReceiverToDelete] = useState(); - const [showCannotDeleteReceiverModal, setShowCannotDeleteReceiverModal] = useState(false); - - const [supportsExport, allowedToExport] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint); - const showExport = supportsExport && allowedToExport; - - const onClickDeleteReceiver = (receiverName: string): void => { - if (isReceiverUsed(receiverName, config)) { - setShowCannotDeleteReceiverModal(true); - } else { - setReceiverToDelete(receiverName); - } - }; - - const deleteReceiver = () => { - if (receiverToDelete) { - dispatch(deleteReceiverAction(receiverToDelete, alertManagerName)); - } - setReceiverToDelete(undefined); - }; - - const rows: RowItemTableProps[] = useMemo(() => { - const receivers = config.alertmanager_config.receivers ?? []; - - return ( - receivers.map((receiver) => ({ - id: receiver.name, - data: { - name: receiver.name, - types: Object.entries(extractNotifierTypeCounts(receiver, grafanaNotifiers.result ?? [])).map( - ([type, count]) => { - if (count > 1) { - return `${type} (${count})`; - } - return type; - } - ), - provisioned: receiver.grafana_managed_receiver_configs?.some((receiver) => receiver.provenance), - metadata: receiversMetadata.get(receiver), - }, - })) ?? [] - ); - }, [grafanaNotifiers.result, config.alertmanager_config, receiversMetadata]); - - const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint); - - const [_, canReadSecrets] = useAlertmanagerAbility(AlertmanagerAction.DecryptSecrets); - - const columns = useGetColumns( - alertManagerName, - errorStateAvailable, - contactPointsState, - configHealth, - onClickDeleteReceiver, - isVanillaAM, - canReadSecrets - ); - - return ( - - ( - - ) - : undefined - } - /> - {!!showCannotDeleteReceiverModal && ( - setShowCannotDeleteReceiverModal(false)} - > - - Contact point cannot be deleted because it is used in more policies. Please update or delete these policies - first. - - - setShowCannotDeleteReceiverModal(false)} fill="outline"> - Close - - - - )} - {!!receiverToDelete && ( - setReceiverToDelete(undefined)} - /> - )} - - ); -}; -const errorsByReceiver = (contactPointsState: ContactPointsState, receiverName: string) => - contactPointsState?.receivers[receiverName]?.errorCount ?? 0; - -const someNotifiersWithNoAttempt = (contactPointsState: ContactPointsState, receiverName: string) => { - const notifiers = Object.values(contactPointsState?.receivers[receiverName]?.notifiers ?? {}); - - if (notifiers.length === 0) { - return false; - } - - const hasSomeWitNoAttempt = notifiers.flat().some((status) => isLastNotifyNullDate(status.lastNotifyAttempt)); - return hasSomeWitNoAttempt; -}; - -function useGetColumns( - alertManagerName: string, - errorStateAvailable: boolean, - contactPointsState: ContactPointsState | undefined, - configHealth: AlertmanagerConfigHealth, - onClickDeleteReceiver: (receiverName: string) => void, - isVanillaAM: boolean, - canReadSecrets: boolean -): RowTableColumnProps[] { - const tableStyles = useStyles2(getAlertTableStyles); - - const enableHealthColumn = - errorStateAvailable || Object.values(configHealth.contactPoints).some((cp) => cp.matchingRoutes === 0); - - const isGrafanaAlertManager = alertManagerName === GRAFANA_RULES_SOURCE_NAME; - - const baseColumns: RowTableColumnProps[] = [ - { - id: 'name', - label: 'Contact point name', - renderCell: ({ data: { name, provisioned } }) => ( - <> - {name} - {provisioned && } - > - ), - size: 3, - className: tableStyles.nameCell, - }, - { - id: 'type', - label: 'Type', - renderCell: ({ data: { types, metadata } }) => ( - <>{metadata ? : types.join(', ')}> - ), - size: 2, - }, - ]; - const healthColumn: RowTableColumnProps = { - id: 'health', - label: 'Health', - renderCell: ({ data: { name } }) => { - if (configHealth.contactPoints[name]?.matchingRoutes === 0) { - return ; - } - - return ( - contactPointsState && - Object.entries(contactPointsState.receivers).length > 0 && ( - - ) - ); - }, - size: '160px', - }; - - return [ - ...baseColumns, - ...(enableHealthColumn ? [healthColumn] : []), - { - id: 'actions', - label: 'Actions', - renderCell: ({ data: { provisioned, name } }) => ( - - - {!isVanillaAM && !provisioned && ( - - )} - {(isVanillaAM || provisioned) && } - {isGrafanaAlertManager && ( - - )} - - - ), - size: '100px', - }, - ]; -} - -export function UnusedContactPointBadge() { - return ( - - ); -} diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/grafanaApp.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/grafanaApp.ts index d0687b83226..eb9ec72c251 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/grafanaApp.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/grafanaApp.ts @@ -5,7 +5,7 @@ import { usePluginBridge } from '../../../hooks/usePluginBridge'; import { SupportedPlugin } from '../../../types/pluginBridges'; import { isOnCallReceiver } from './onCall/onCall'; -import { AmRouteReceiver, ReceiverWithTypes } from './types'; +import { AmRouteReceiver } from './types'; export const useGetGrafanaReceiverTypeChecker = () => { const { installed: isOnCallEnabled } = usePluginBridge(SupportedPlugin.OnCall); @@ -38,13 +38,3 @@ export const useGetAmRouteReceiverWithGrafanaAppTypes = (receivers: Receiver[]) return receivers.map(receiverToSelectableContactPointValue); }; - -export const useGetReceiversWithGrafanaAppTypes = (receivers: Receiver[]): ReceiverWithTypes[] => { - const getGrafanaReceiverType = useGetGrafanaReceiverTypeChecker(); - return receivers.map((receiver: Receiver) => { - return { - ...receiver, - grafanaAppReceiverType: getGrafanaReceiverType(receiver), - }; - }); -}; diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts index 099a73b8fc8..5da23618271 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts @@ -1,4 +1,3 @@ -import { GrafanaManagedContactPoint } from '../../../../../../plugins/datasource/alertmanager/types'; import { SupportedPlugin } from '../../../types/pluginBridges'; export interface AmRouteReceiver { @@ -7,9 +6,6 @@ export interface AmRouteReceiver { grafanaAppReceiverType?: SupportedPlugin; } -export interface ReceiverWithTypes extends GrafanaManagedContactPoint { - grafanaAppReceiverType?: SupportedPlugin; -} export const GRAFANA_APP_RECEIVERS_SOURCE_IMAGE: Record = { [SupportedPlugin.OnCall]: 'public/img/alerting/oncall_logo.svg', diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts index 13a85df9d6c..0fb9dcfaac5 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts @@ -1,12 +1,8 @@ -import { useMemo } from 'react'; - -import { GrafanaManagedReceiverConfig, Receiver } from '../../../../../../plugins/datasource/alertmanager/types'; -import { onCallApi, OnCallIntegrationDTO } from '../../../api/onCallApi'; -import { usePluginBridge } from '../../../hooks/usePluginBridge'; +import { GrafanaManagedReceiverConfig } from '../../../../../../plugins/datasource/alertmanager/types'; +import { OnCallIntegrationDTO } from '../../../api/onCallApi'; import { SupportedPlugin } from '../../../types/pluginBridges'; import { createBridgeURL } from '../../PluginBridge'; -import { ReceiverTypes } from './onCall/onCall'; import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types'; export interface ReceiverPluginMetadata { @@ -25,32 +21,6 @@ const onCallReceiverMeta: ReceiverPluginMetadata = { icon: onCallReceiverICon, }; -export const useReceiversMetadata = (receivers: Receiver[]): Map => { - const { installed: isOnCallEnabled } = usePluginBridge(SupportedPlugin.OnCall); - const { data: onCallIntegrations = [] } = onCallApi.useGrafanaOnCallIntegrationsQuery(undefined, { - skip: !isOnCallEnabled, - }); - - return useMemo(() => { - const result = new Map(); - - receivers.forEach((receiver) => { - const onCallReceiver = receiver.grafana_managed_receiver_configs?.find((c) => c.type === ReceiverTypes.OnCall); - - if (onCallReceiver) { - if (!isOnCallEnabled) { - result.set(receiver, getOnCallMetadata(null, onCallReceiver)); - return; - } - - result.set(receiver, getOnCallMetadata(onCallIntegrations, onCallReceiver)); - } - }); - - return result; - }, [receivers, isOnCallEnabled, onCallIntegrations]); -}; - export function getOnCallMetadata( onCallIntegrations: OnCallIntegrationDTO[] | undefined | null, receiver: GrafanaManagedReceiverConfig diff --git a/public/app/features/alerting/unified/components/receivers/useAlertmanagerConfigHealth.ts b/public/app/features/alerting/unified/components/receivers/useAlertmanagerConfigHealth.ts deleted file mode 100644 index 65c84e61f5c..00000000000 --- a/public/app/features/alerting/unified/components/receivers/useAlertmanagerConfigHealth.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { countBy } from 'lodash'; - -import { AlertmanagerConfig } from '../../../../../plugins/datasource/alertmanager/types'; -import { getUsedContactPoints } from '../contact-points/utils'; - -export interface ContactPointConfigHealth { - matchingRoutes: number; -} - -export interface AlertmanagerConfigHealth { - contactPoints: Record; -} - -export function useAlertmanagerConfigHealth(config: AlertmanagerConfig): AlertmanagerConfigHealth { - if (!config.receivers) { - return { contactPoints: {} }; - } - - if (!config.route) { - return { contactPoints: Object.fromEntries(config.receivers.map((r) => [r.name, { matchingRoutes: 0 }])) }; - } - - const definedContactPointNames = config.receivers?.map((receiver) => receiver.name) ?? []; - const usedContactPoints = getUsedContactPoints(config.route); - const usedContactPointCounts = countBy(usedContactPoints); - - const contactPointsHealth: AlertmanagerConfigHealth['contactPoints'] = {}; - const configHealth: AlertmanagerConfigHealth = { contactPoints: contactPointsHealth }; - - definedContactPointNames.forEach((contactPointName) => { - contactPointsHealth[contactPointName] = { matchingRoutes: usedContactPointCounts[contactPointName] ?? 0 }; - }); - - return configHealth; -} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx index 09fa89ce112..f55648c5ae0 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Alert, Field, LoadingPlaceholder, Select, Stack, useStyles2 } from '@grafana/ui'; import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource'; -import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoints.v2'; +import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoints'; import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints'; import { selectContactPoint } from './SimplifiedRouting'; diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts index 02488b99ad0..e81b9195ffc 100644 --- a/public/app/features/alerting/unified/utils/alertmanager.ts +++ b/public/app/features/alerting/unified/utils/alertmanager.ts @@ -53,18 +53,6 @@ export function renameMuteTimings(newMuteTimingName: string, oldMuteTimingName: }; } -function isReceiverUsedInRoute(receiver: string, route: Route): boolean { - return ( - (route.receiver === receiver || route.routes?.some((route) => isReceiverUsedInRoute(receiver, route))) ?? false - ); -} - -export function isReceiverUsed(receiver: string, config: AlertManagerCortexConfig): boolean { - return ( - (config.alertmanager_config.route && isReceiverUsedInRoute(receiver, config.alertmanager_config.route)) ?? false - ); -} - export function matcherToOperator(matcher: Matcher): MatcherOperator { if (matcher.isEqual) { if (matcher.isRegex) { diff --git a/public/app/features/alerting/unified/utils/receivers.ts b/public/app/features/alerting/unified/utils/receivers.ts index 11bed1bf7fc..d4147cae3d1 100644 --- a/public/app/features/alerting/unified/utils/receivers.ts +++ b/public/app/features/alerting/unified/utils/receivers.ts @@ -1,33 +1,6 @@ -import { capitalize, isEmpty, times } from 'lodash'; +import { isEmpty, times } from 'lodash'; -import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/alertmanager/types'; -import { NotifierDTO } from 'app/types'; - -// extract notifier type name to count map, eg { Slack: 1, Email: 2 } - -type NotifierTypeCounts = Record; // name : count - -export function extractNotifierTypeCounts(receiver: Receiver, grafanaNotifiers: NotifierDTO[]): NotifierTypeCounts { - if ('grafana_managed_receiver_configs' in receiver) { - return getGrafanaNotifierTypeCounts(receiver.grafana_managed_receiver_configs ?? [], grafanaNotifiers); - } - return getCortexAlertManagerNotifierTypeCounts(receiver); -} - -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 alertmanager notifier - .filter(([_, value]) => Array.isArray(value) && !!value.length) // check that there are actually notifiers of this type configured - .reduce((acc, [key, value]) => { - const type = key.replace('_configs', ''); // remove the `_config` part from the key, making it intto a notifier name - const name = receiverTypeNames[type] ?? capitalize(type); - return { - ...acc, - [name]: (acc[name] ?? 0) + (Array.isArray(value) ? value.length : 1), - }; - }, {}); -} /** * This function will extract the integrations that have been defined for either grafana managed contact point @@ -67,19 +40,3 @@ export function extractReceivers(receiver: Receiver): GrafanaManagedReceiverConf return integrations; } - -function getGrafanaNotifierTypeCounts( - configs: GrafanaManagedReceiverConfig[], - grafanaNotifiers: NotifierDTO[] -): NotifierTypeCounts { - return configs - .map((recv) => recv.type) // extract types from config - .map((type) => grafanaNotifiers.find((r) => r.type === type)?.name ?? capitalize(type)) // get readable name from notifier cofnig, or if not available, just capitalize - .reduce( - (acc, type) => ({ - ...acc, - [type]: (acc[type] ?? 0) + 1, - }), - {} - ); -} diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index 293b4850d3d..8f47c418c21 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -12,30 +12,30 @@ export type AlertManagerCortexConfig = { }; export type TLSConfig = { - ca_file: string; - cert_file: string; - key_file: string; + ca_file?: string; + cert_file?: string; + key_file?: string; server_name?: string; insecure_skip_verify?: boolean; }; export type HTTPConfigCommon = { - proxy_url?: string; + proxy_url?: string | null; tls_config?: TLSConfig; }; export type HTTPConfigBasicAuth = { - basic_auth: { + basic_auth?: { username: string; - } & ({ password: string } | { password_file: string }); + } & ({ password?: string } | { password_file?: string }); }; export type HTTPConfigBearerToken = { - bearer_token: string; + bearer_token?: string; }; export type HTTPConfigBearerTokenFile = { - bearer_token_file: string; + bearer_token_file?: string; }; export type HTTPConfig = HTTPConfigCommon & (HTTPConfigBasicAuth | HTTPConfigBearerToken | HTTPConfigBearerTokenFile); @@ -123,10 +123,10 @@ export interface RouteWithID extends Route { } export type InhibitRule = { - target_match: Record; - target_match_re: Record; - source_match: Record; - source_match_re: Record; + target_match?: Record; + target_match_re?: Record; + source_match?: Record; + source_match_re?: Record; equal?: string[]; };
+ For each external Alertmanager you can define global settings, like server addresses, usernames and password, + for all the supported contact points. +
- For each external Alertmanager you can define global settings, like server addresses, usernames and password, - for all the supported contact points. -
- Contact point cannot be deleted because it is used in more policies. Please update or delete these policies - first. -