diff --git a/pkg/services/ngalert/api/api_configuration.go b/pkg/services/ngalert/api/api_configuration.go index 4d78029bd64..c4f092c638b 100644 --- a/pkg/services/ngalert/api/api_configuration.go +++ b/pkg/services/ngalert/api/api_configuration.go @@ -148,8 +148,15 @@ func (srv ConfigSrv) RouteGetAlertingStatus(c *models.ReqContext) response.Respo sendsAlertsTo = cfg.SendAlertsTo } + // handle errors + externalAlertManagers, err := srv.externalAlertmanagers(c.Req.Context(), c.OrgID) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + resp := apimodels.AlertingStatus{ - AlertmanagersChoice: apimodels.AlertmanagersChoice(sendsAlertsTo.String()), + AlertmanagersChoice: apimodels.AlertmanagersChoice(sendsAlertsTo.String()), + NumExternalAlertmanagers: len(externalAlertManagers), } return response.JSON(http.StatusOK, resp) } diff --git a/pkg/services/ngalert/api/tooling/definitions/admin.go b/pkg/services/ngalert/api/tooling/definitions/admin.go index 8d635e90373..25488d1fb6e 100644 --- a/pkg/services/ngalert/api/tooling/definitions/admin.go +++ b/pkg/services/ngalert/api/tooling/definitions/admin.go @@ -92,5 +92,6 @@ type GettableAlertmanagers struct { // swagger:model type AlertingStatus struct { - AlertmanagersChoice AlertmanagersChoice `json:"alertmanagersChoice"` + AlertmanagersChoice AlertmanagersChoice `json:"alertmanagersChoice"` + NumExternalAlertmanagers int `json:"numExternalAlertmanagers"` } diff --git a/public/app/features/alerting/unified/AlertGroups.tsx b/public/app/features/alerting/unified/AlertGroups.tsx index 9322973030d..920531c6f2d 100644 --- a/public/app/features/alerting/unified/AlertGroups.tsx +++ b/public/app/features/alerting/unified/AlertGroups.tsx @@ -25,7 +25,7 @@ import { getFiltersFromUrlParams } from './utils/misc'; import { initialAsyncRequestState } from './utils/redux'; const AlertGroups = () => { - const { useGetAlertmanagerChoiceQuery } = alertmanagerApi; + const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi; const alertManagers = useAlertManagersByPermission('instance'); const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers); @@ -34,7 +34,7 @@ const AlertGroups = () => { const { groupBy = [] } = getFiltersFromUrlParams(queryParams); const styles = useStyles2(getStyles); - const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery(); + const { currentData: amConfigStatus } = useGetAlertmanagerChoiceStatusQuery(); const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups); const { @@ -47,7 +47,8 @@ const AlertGroups = () => { const filteredAlertGroups = useFilteredAmGroups(groupedAlerts); const grafanaAmDeliveryDisabled = - alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && alertmanagerChoice === AlertmanagerChoice.External; + alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && + amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External; useEffect(() => { function fetchNotifications() { diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx index b69a685e23a..db81dae3d4b 100644 --- a/public/app/features/alerting/unified/AmRoutes.tsx +++ b/public/app/features/alerting/unified/AmRoutes.tsx @@ -8,7 +8,6 @@ import { useDispatch } from 'app/types'; import { useCleanup } from '../../../core/hooks/useCleanup'; -import { alertmanagerApi } from './api/alertmanagerApi'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; @@ -28,12 +27,11 @@ import { initialAsyncRequestState } from './utils/redux'; const AmRoutes = () => { const dispatch = useDispatch(); - const { useGetAlertmanagerChoiceQuery } = alertmanagerApi; + const styles = useStyles2(getStyles); const [isRootRouteEditMode, setIsRootRouteEditMode] = useState(false); const alertManagers = useAlertManagersByPermission('notification'); const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); - const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery(); const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); @@ -131,10 +129,7 @@ const AmRoutes = () => { {resultError.message || 'Unknown error.'} )} - + {isProvisioned && } {resultLoading && } {result && !resultLoading && !resultError && ( diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index 92661114386..63845e99761 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -24,6 +24,7 @@ import 'whatwg-fetch'; import Receivers from './Receivers'; import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from './api/alertmanager'; +import { AlertmanagersChoiceResponse } from './api/alertmanagerApi'; import { discoverAlertmanagerFeatures } from './api/buildInfo'; import { fetchNotifiers } from './api/grafana'; import * as receiversApi from './api/receiversApi'; @@ -62,6 +63,11 @@ const mocks = { contextSrv: jest.mocked(contextSrv), }; +const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = { + alertmanagersChoice: AlertmanagerChoice.Internal, + numExternalAlertmanagers: 0, +}; + const renderReceivers = (alertManagerSourceName?: string) => { const store = configureStore(); @@ -181,7 +187,7 @@ describe('Receivers', () => { }); it('Template and receiver tables are rendered, alertmanager can be selected, no notification errors', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockImplementation((name) => Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig) ); @@ -226,7 +232,7 @@ describe('Receivers', () => { }); it('Grafana receiver can be tested', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); @@ -284,7 +290,7 @@ describe('Receivers', () => { }); it('Grafana receiver can be created', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); @@ -348,7 +354,7 @@ describe('Receivers', () => { }); it('Hides create contact point button for users without permission', () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); @@ -364,7 +370,7 @@ describe('Receivers', () => { }); it('Cloud alertmanager receiver can be edited', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); @@ -460,7 +466,7 @@ describe('Receivers', () => { }); it('Prometheus Alertmanager receiver cannot be edited', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchStatus.mockResolvedValue({ ...someCloudAlertManagerStatus, @@ -499,7 +505,7 @@ describe('Receivers', () => { }); it('Loads config from status endpoint if there is no user config', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); // loading an empty config with make it fetch config from status endpoint mocks.api.fetchConfig.mockResolvedValue({ template_files: {}, @@ -521,7 +527,7 @@ describe('Receivers', () => { }); it('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true }); mocks.api.fetchConfig.mockRejectedValue({ message: 'alertmanager storage object not found' }); @@ -538,7 +544,7 @@ describe('Receivers', () => { describe('Contact points state', () => { it('Should render error notifications when there are some points state ', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); @@ -610,7 +616,7 @@ describe('Receivers', () => { 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, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); @@ -687,7 +693,7 @@ describe('Receivers', () => { }); it('Should not render error notifications when fetching contact points state raises 404 error ', async () => { - mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index d4934e43fa5..48d60aa09f4 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -10,7 +10,6 @@ import { useDispatch } from 'app/types'; import { ContactPointsState } from '../../../types'; -import { alertmanagerApi } from './api/alertmanagerApi'; import { useGetContactPointsState } from './api/receiversApi'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; @@ -54,8 +53,6 @@ function NotificationError({ errorCount }: NotificationErrorProps) { type PageType = 'receivers' | 'templates' | 'global-config'; const Receivers = () => { - const { useGetAlertmanagerChoiceQuery } = alertmanagerApi; - const alertManagers = useAlertManagersByPermission('notification'); const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const dispatch = useDispatch(); @@ -96,8 +93,6 @@ const Receivers = () => { const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? ''); const integrationsErrorCount = contactPointsState?.errorCount ?? 0; - const { data: alertmanagerChoice } = useGetAlertmanagerChoiceQuery(); - const disableAmSelect = !isRoot; let pageNav = getPageNavigationModel(type, id); @@ -130,10 +125,7 @@ const Receivers = () => { {error.message || 'Unknown error.'} )} - + {loading && !config && } {config && !error && ( diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index 3800c9d7f83..eaf54214b13 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -5,7 +5,6 @@ import { Alert, withErrorBoundary } from '@grafana/ui'; import { Silence } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch } from 'app/types'; -import { alertmanagerApi } from './api/alertmanagerApi'; import { featureDiscoveryApi } from './api/featureDiscoveryApi'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; @@ -26,7 +25,6 @@ const Silences = () => { const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const dispatch = useDispatch(); - const { useGetAlertmanagerChoiceQuery } = alertmanagerApi; const silences = useUnifiedAlertingSelector((state) => state.silences); const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts); const alertsRequest = alertManagerSourceName @@ -42,8 +40,6 @@ const Silences = () => { { skip: !alertManagerSourceName } ); - const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery(); - useEffect(() => { function fetchAll() { if (alertManagerSourceName) { @@ -84,10 +80,7 @@ const Silences = () => { onChange={setAlertManagerSourceName} dataSources={alertManagers} /> - + {mimirLazyInitError && ( diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index 53199140e22..be7f2634b44 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -9,14 +9,14 @@ import { alertingApi } from './alertingApi'; export interface AlertmanagersChoiceResponse { alertmanagersChoice: AlertmanagerChoice; + numExternalAlertmanagers: number; } export const alertmanagerApi = alertingApi.injectEndpoints({ endpoints: (build) => ({ - getAlertmanagerChoice: build.query({ + getAlertmanagerChoiceStatus: build.query({ query: () => ({ url: '/api/v1/ngalert' }), providesTags: ['AlertmanagerChoice'], - transformResponse: (response: AlertmanagersChoiceResponse) => response.alertmanagersChoice, }), getExternalAlertmanagerConfig: build.query({ diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx index 405dc211d09..31fe270eaff 100644 --- a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx @@ -1,47 +1,100 @@ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import { setupServer } from 'msw/node'; import React from 'react'; +import { Provider } from 'react-redux'; + +import 'whatwg-fetch'; + +import { setBackendSrv } from '@grafana/runtime'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { configureStore } from 'app/store/configureStore'; import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types'; +import { mockAlertmanagerChoiceResponse } from '../mocks/alertmanagerApi'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { GrafanaAlertmanagerDeliveryWarning } from './GrafanaAlertmanagerDeliveryWarning'; describe('GrafanaAlertmanagerDeliveryWarning', () => { - describe('When AlertmanagerChoice set to External', () => { - it('Should not render when the datasource is not Grafana', () => { - const { container } = render( - - ); + const server = setupServer(); - expect(container).toBeEmptyDOMElement(); - }); - - it('Should render warning when the datasource is Grafana', () => { - const { container } = render( - - ); - - expect(container).toHaveTextContent('Grafana alerts are not delivered to Grafana Alertmanager'); - }); + beforeAll(() => { + setBackendSrv(backendSrv); + server.listen({ onUnhandledRequest: 'error' }); }); - it.each([AlertmanagerChoice.All, AlertmanagerChoice.Internal])( - 'Should not render when datasource is Grafana and Alertmanager choice is %s', - (choice) => { - const { container } = render( - - ); + afterAll(() => { + server.close(); + }); - expect(container).toBeEmptyDOMElement(); - } - ); + beforeEach(() => { + server.resetHandlers(); + }); + + it('Should not render when the datasource is not Grafana', () => { + mockAlertmanagerChoiceResponse(server, { + alertmanagersChoice: AlertmanagerChoice.External, + numExternalAlertmanagers: 0, + }); + + const { container } = renderWithStore( + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('Should render warning when the datasource is Grafana and using external AM', async () => { + mockAlertmanagerChoiceResponse(server, { + alertmanagersChoice: AlertmanagerChoice.External, + numExternalAlertmanagers: 1, + }); + + renderWithStore(); + + expect(await screen.findByText('Grafana alerts are not delivered to Grafana Alertmanager')).toBeVisible(); + }); + + it('Should render warning when the datasource is Grafana and using All AM', async () => { + mockAlertmanagerChoiceResponse(server, { + alertmanagersChoice: AlertmanagerChoice.All, + numExternalAlertmanagers: 1, + }); + + renderWithStore(); + + expect(await screen.findByText('You have additional Alertmanagers to configure')).toBeVisible(); + }); + + it('Should render no warning when choice is Internal', async () => { + mockAlertmanagerChoiceResponse(server, { + alertmanagersChoice: AlertmanagerChoice.Internal, + numExternalAlertmanagers: 1, + }); + + const { container } = renderWithStore( + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('Should render no warning when choice is All but no active AM instances', async () => { + mockAlertmanagerChoiceResponse(server, { + alertmanagersChoice: AlertmanagerChoice.All, + numExternalAlertmanagers: 0, + }); + + const { container } = renderWithStore( + + ); + + expect(container).toBeEmptyDOMElement(); + }); }); + +function renderWithStore(element: JSX.Element) { + const store = configureStore(); + + return render({element}); +} diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx index 89209f7204f..7d1306ccfad 100644 --- a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx @@ -5,37 +5,58 @@ import { GrafanaTheme2 } from '@grafana/data/src'; import { Alert, useStyles2 } from '@grafana/ui/src'; import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types'; +import { alertmanagerApi } from '../api/alertmanagerApi'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; interface GrafanaAlertmanagerDeliveryWarningProps { - alertmanagerChoice?: AlertmanagerChoice; currentAlertmanager: string; } -export function GrafanaAlertmanagerDeliveryWarning({ - alertmanagerChoice, - currentAlertmanager, -}: GrafanaAlertmanagerDeliveryWarningProps) { +export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: GrafanaAlertmanagerDeliveryWarningProps) { const styles = useStyles2(getStyles); - if (currentAlertmanager !== GRAFANA_RULES_SOURCE_NAME) { + const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi; + const { currentData: amChoiceStatus } = useGetAlertmanagerChoiceStatusQuery(); + + const viewingInternalAM = currentAlertmanager === GRAFANA_RULES_SOURCE_NAME; + + const interactsWithExternalAMs = + amChoiceStatus?.alertmanagersChoice && + [AlertmanagerChoice.External, AlertmanagerChoice.All].includes(amChoiceStatus?.alertmanagersChoice); + + if (!interactsWithExternalAMs || !viewingInternalAM) { return null; } - if (alertmanagerChoice !== AlertmanagerChoice.External) { - return null; + const hasActiveExternalAMs = amChoiceStatus.numExternalAlertmanagers > 0; + + if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.External) { + return ( + + Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration + will not affect delivery of your alerts. +
+ To change your Alertmanager setup, go to the Alerting Admin page. If you do not have access, contact your + Administrator. +
+
+ ); } - return ( - - Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration - will not affect delivery of your alerts! -
- You can change the configuration on the Alerting Admin page. If you do not have access, contact your - Administrator -
-
- ); + if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.All && hasActiveExternalAMs) { + return ( + + Ensure you make configuration changes in the correct Alertmanagers; both internal and external. Changing one + will not affect the others. +
+ To change your Alertmanager setup, go to the Alerting Admin page. If you do not have access, contact your + Administrator. +
+
+ ); + } + + return null; } const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx index 6bdc835d8ea..0870aece9ff 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx @@ -1,16 +1,22 @@ import { render } from '@testing-library/react'; +import { setupServer } from 'msw/node'; import React from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { byRole } from 'testing-library-selector'; +import { setBackendSrv } from '@grafana/runtime'; +import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; +import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; import { CombinedRule } from 'app/types/unified-alerting'; +import { AlertmanagersChoiceResponse } from '../../api/alertmanagerApi'; import { useIsRuleEditable } from '../../hooks/useIsRuleEditable'; import { getCloudRule, getGrafanaRule } from '../../mocks'; +import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi'; import { RuleDetails } from './RuleDetails'; @@ -30,10 +36,27 @@ const ui = { jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true); -beforeEach(() => { +const server = setupServer(); + +const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = { + alertmanagersChoice: AlertmanagerChoice.Internal, + numExternalAlertmanagers: 0, +}; + +beforeAll(() => { + setBackendSrv(backendSrv); + server.listen({ onUnhandledRequest: 'error' }); jest.clearAllMocks(); }); +afterAll(() => { + server.close(); +}); + +beforeEach(() => { + server.resetHandlers(); +}); + describe('RuleDetails RBAC', () => { describe('Grafana rules action buttons in details', () => { const grafanaRule = getGrafanaRule({ name: 'Grafana' }); @@ -63,6 +86,7 @@ describe('RuleDetails RBAC', () => { it('Should not render Silence button for users wihout the instance create permission', () => { // Arrange jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); // Act renderRuleDetails(grafanaRule); @@ -71,7 +95,9 @@ describe('RuleDetails RBAC', () => { expect(ui.actionButtons.silence.query()).not.toBeInTheDocument(); }); - it('Should render Silence button for users with the instance create permissions', () => { + it('Should render Silence button for users with the instance create permissions', async () => { + mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); + // Arrange jest .spyOn(contextSrv, 'hasPermission') @@ -81,9 +107,10 @@ describe('RuleDetails RBAC', () => { renderRuleDetails(grafanaRule); // Assert - expect(ui.actionButtons.silence.query()).toBeInTheDocument(); + expect(await ui.actionButtons.silence.find()).toBeInTheDocument(); }); }); + describe('Cloud rules action buttons', () => { const cloudRule = getCloudRule({ name: 'Cloud' }); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 9913bbbbbba..973ef437b6c 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -7,9 +7,11 @@ import { config } from '@grafana/runtime'; import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/services/context_srv'; +import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction, useDispatch } from 'app/types'; import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; +import { alertmanagerApi } from '../../api/alertmanagerApi'; import { useIsRuleEditable } from '../../hooks/useIsRuleEditable'; import { useStateHistoryModal } from '../../hooks/useStateHistoryModal'; import { deleteRuleAction } from '../../state/actions'; @@ -75,6 +77,7 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource, isViewM const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule); + const canSilence = useCanSilence(rule); const returnTo = location.pathname + location.search; // explore does not support grafana rule queries atm @@ -144,7 +147,7 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource, isViewM } } - if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) { + if (canSilence && alertmanagerSourceName) { buttons.push( = ({ rule, rulesSource, isViewM return null; }; +/** + * We don't want to show the silence button if either + * 1. the user has no permissions to create silences + * 2. the admin has configured to only send instances to external AMs + */ +function useCanSilence(rule: CombinedRule) { + const isGrafanaManagedRule = isGrafanaRulerRule(rule.rulerRule); + + const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi; + const { currentData: amConfigStatus, isLoading } = useGetAlertmanagerChoiceStatusQuery(undefined, { + skip: !isGrafanaManagedRule, + }); + + if (!isGrafanaManagedRule || isLoading) { + return false; + } + + const hasPermissions = contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor); + + const interactsOnlyWithExternalAMs = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External; + const interactsWithAll = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.All; + + return hasPermissions && (!interactsOnlyWithExternalAMs || interactsWithAll); +} + export const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` padding: ${theme.spacing(2)} 0; diff --git a/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx b/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx index be00d696cc0..3042d066ec5 100644 --- a/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx +++ b/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx @@ -64,12 +64,12 @@ export const MatchedSilencedRules = () => { {matchers.every((matcher) => !matcher.value && !matcher.name) ? ( Add a valid matcher to see affected alerts ) : ( - <> - - {matchedAlertRules.length > 5 && ( -
and {matchedAlertRules.length - 5} more
- )} - + )} @@ -92,7 +92,7 @@ function useColumns(): MatchedRulesTableColumnProps[] { renderCell: function renderName({ data: { matchedInstance } }) { return ; }, - size: '250px', + size: 'auto', }, { id: 'created', @@ -106,7 +106,7 @@ function useColumns(): MatchedRulesTableColumnProps[] { ); }, - size: '400px', + size: '180px', }, ]; } diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 3549e1c7a0b..ab35f02b0af 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -149,6 +149,25 @@ export const mockPromAlertingRule = (partial: Partial = {}): Alert }; }; +export const mockGrafanaRulerRule = (partial: Partial = {}): RulerGrafanaRuleDTO => { + return { + for: '', + annotations: {}, + labels: {}, + grafana_alert: { + ...partial, + uid: '', + title: 'my rule', + namespace_uid: '', + namespace_id: 0, + condition: '', + no_data_state: GrafanaAlertStateDecision.NoData, + exec_err_state: GrafanaAlertStateDecision.Error, + data: [], + }, + }; +}; + export const mockPromRecordingRule = (partial: Partial = {}): RecordingRule => { return { type: PromRuleType.Recording, @@ -523,6 +542,7 @@ export function getGrafanaRule(override?: Partial) { name: 'Grafana', rulesSource: 'grafana', }, + rulerRule: mockGrafanaRulerRule(), ...override, }); }