Alerting: hide "silence" button for external AM setups (#62133)

This commit is contained in:
Gilles De Mey 2023-02-01 15:51:05 +01:00 committed by GitHub
parent a190e03133
commit 26866953c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 244 additions and 103 deletions

View File

@ -148,8 +148,15 @@ func (srv ConfigSrv) RouteGetAlertingStatus(c *contextmodel.ReqContext) response
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)
}

View File

@ -92,5 +92,6 @@ type GettableAlertmanagers struct {
// swagger:model
type AlertingStatus struct {
AlertmanagersChoice AlertmanagersChoice `json:"alertmanagersChoice"`
AlertmanagersChoice AlertmanagersChoice `json:"alertmanagersChoice"`
NumExternalAlertmanagers int `json:"numExternalAlertmanagers"`
}

View File

@ -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() {

View File

@ -7,7 +7,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';
@ -29,12 +28,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);
@ -130,10 +128,7 @@ const AmRoutes = () => {
{resultError.message || 'Unknown error.'}
</Alert>
)}
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={alertManagerSourceName}
alertmanagerChoice={alertmanagerChoice}
/>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.RootNotificationPolicy} />}
{resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{result && !resultLoading && !resultError && (

View File

@ -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';
@ -63,6 +64,11 @@ const mocks = {
contextSrv: jest.mocked(contextSrv),
};
const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = {
alertmanagersChoice: AlertmanagerChoice.Internal,
numExternalAlertmanagers: 0,
};
const renderReceivers = (alertManagerSourceName?: string) => {
const store = configureStore();
@ -185,7 +191,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)
);
@ -230,7 +236,7 @@ describe('Receivers', () => {
});
it('Grafana receiver can be tested', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
@ -288,7 +294,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();
@ -352,7 +358,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();
@ -368,7 +374,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();
@ -464,7 +470,7 @@ describe('Receivers', () => {
});
it('Prometheus Alertmanager receiver cannot be edited', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchStatus.mockResolvedValue({
...someCloudAlertManagerStatus,
@ -503,7 +509,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: {},
@ -525,7 +531,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' });
@ -542,7 +548,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();
@ -614,7 +620,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();
@ -691,7 +697,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();

View File

@ -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';
@ -55,8 +54,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();
@ -97,8 +94,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);
@ -131,10 +126,7 @@ const Receivers = () => {
{error.message || 'Unknown error.'}
</Alert>
)}
<GrafanaAlertmanagerDeliveryWarning
alertmanagerChoice={alertmanagerChoice}
currentAlertmanager={alertManagerSourceName}
/>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
{loading && !config && <LoadingPlaceholder text="loading configuration..." />}
{config && !error && (
<Switch>

View File

@ -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}
/>
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={alertManagerSourceName}
alertmanagerChoice={alertmanagerChoice}
/>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
{mimirLazyInitError && (
<Alert title="The selected Alertmanager has no configuration" severity="warning">

View File

@ -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<AlertmanagerChoice, void>({
getAlertmanagerChoiceStatus: build.query<AlertmanagersChoiceResponse, void>({
query: () => ({ url: '/api/v1/ngalert' }),
providesTags: ['AlertmanagerChoice'],
transformResponse: (response: AlertmanagersChoiceResponse) => response.alertmanagersChoice,
}),
getExternalAlertmanagerConfig: build.query<ExternalAlertmanagerConfig, void>({

View File

@ -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(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager="custom-alertmanager"
alertmanagerChoice={AlertmanagerChoice.External}
/>
);
const server = setupServer();
expect(container).toBeEmptyDOMElement();
});
it('Should render warning when the datasource is Grafana', () => {
const { container } = render(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={GRAFANA_RULES_SOURCE_NAME}
alertmanagerChoice={AlertmanagerChoice.External}
/>
);
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(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={GRAFANA_RULES_SOURCE_NAME}
alertmanagerChoice={choice}
/>
);
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(
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager="custom-alertmanager" />
);
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(<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
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(<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
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(
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
);
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(
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
);
expect(container).toBeEmptyDOMElement();
});
});
function renderWithStore(element: JSX.Element) {
const store = configureStore();
return render(<Provider store={store}>{element}</Provider>);
}

View File

@ -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 (
<Alert title="Grafana alerts are not delivered to Grafana Alertmanager">
Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration
will not affect delivery of your alerts.
<div className={styles.adminHint}>
To change your Alertmanager setup, go to the Alerting Admin page. If you do not have access, contact your
Administrator.
</div>
</Alert>
);
}
return (
<Alert title="Grafana alerts are not delivered to Grafana Alertmanager">
Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration
will not affect delivery of your alerts!
<div className={styles.adminHint}>
You can change the configuration on the Alerting Admin page. If you do not have access, contact your
Administrator
</div>
</Alert>
);
if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.All && hasActiveExternalAMs) {
return (
<Alert title="You have additional Alertmanagers to configure" severity="warning">
Ensure you make configuration changes in the correct Alertmanagers; both internal and external. Changing one
will not affect the others.
<div className={styles.adminHint}>
To change your Alertmanager setup, go to the Alerting Admin page. If you do not have access, contact your
Administrator.
</div>
</Alert>
);
}
return null;
}
const getStyles = (theme: GrafanaTheme2) => ({

View File

@ -1,4 +1,5 @@
import { render, screen, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
@ -7,12 +8,15 @@ 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';
@ -32,11 +36,27 @@ const ui = {
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
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' });
@ -68,6 +88,7 @@ describe('RuleDetails RBAC', () => {
it('Should not render Silence button for users wihout the instance create permission', async () => {
// Arrange
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
// Act
renderRuleDetails(grafanaRule);
@ -78,6 +99,8 @@ describe('RuleDetails RBAC', () => {
});
it('Should render Silence button for users with the instance create permissions', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
// Arrange
jest
.spyOn(contextSrv, 'hasPermission')
@ -91,6 +114,7 @@ describe('RuleDetails RBAC', () => {
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
});
});
describe('Cloud rules action buttons', () => {
const cloudRule = getCloudRule({ name: 'Cloud' });

View File

@ -7,10 +7,12 @@ 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 { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
import { deleteRuleAction } from '../../state/actions';
@ -84,6 +86,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
const rulesPermissions = getRulesPermissions(rulesSourceName);
const hasCreateRulePermission = contextSrv.hasPermission(rulesPermissions.create);
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
const canSilence = useCanSilence(rule);
const returnTo = location.pathname + location.search;
// explore does not support grafana rule queries atm
@ -149,7 +152,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
}
}
if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) {
if (canSilence && alertmanagerSourceName) {
buttons.push(
<LinkButton
size="sm"
@ -263,6 +266,31 @@ export const RuleDetailsActionButtons: FC<Props> = ({ 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;

View File

@ -64,12 +64,12 @@ export const MatchedSilencedRules = () => {
{matchers.every((matcher) => !matcher.value && !matcher.name) ? (
<span>Add a valid matcher to see affected alerts</span>
) : (
<>
<DynamicTable items={matchedAlertRules.slice(0, 5) ?? []} isExpandable={false} cols={columns} />
{matchedAlertRules.length > 5 && (
<div className={styles.moreMatches}>and {matchedAlertRules.length - 5} more</div>
)}
</>
<DynamicTable
items={matchedAlertRules}
isExpandable={false}
cols={columns}
pagination={{ itemsPerPage: 5 }}
/>
)}
</div>
</div>
@ -92,7 +92,7 @@ function useColumns(): MatchedRulesTableColumnProps[] {
renderCell: function renderName({ data: { matchedInstance } }) {
return <AlertLabels labels={matchedInstance.labels} />;
},
size: '250px',
size: 'auto',
},
{
id: 'created',
@ -106,7 +106,7 @@ function useColumns(): MatchedRulesTableColumnProps[] {
</>
);
},
size: '400px',
size: '180px',
},
];
}

View File

@ -173,6 +173,25 @@ export const mockPromAlertingRule = (partial: Partial<AlertingRule> = {}): Alert
};
};
export const mockGrafanaRulerRule = (partial: Partial<RulerGrafanaRuleDTO> = {}): 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> = {}): RecordingRule => {
return {
type: PromRuleType.Recording,
@ -570,6 +589,7 @@ export function getGrafanaRule(override?: Partial<CombinedRule>) {
name: 'Grafana',
rulesSource: 'grafana',
},
rulerRule: mockGrafanaRulerRule(),
...override,
});
}