({
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,
});
}