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 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{ resp := apimodels.AlertingStatus{
AlertmanagersChoice: apimodels.AlertmanagersChoice(sendsAlertsTo.String()), AlertmanagersChoice: apimodels.AlertmanagersChoice(sendsAlertsTo.String()),
NumExternalAlertmanagers: len(externalAlertManagers),
} }
return response.JSON(http.StatusOK, resp) return response.JSON(http.StatusOK, resp)
} }

View File

@ -93,4 +93,5 @@ type GettableAlertmanagers struct {
// swagger:model // swagger:model
type AlertingStatus struct { 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'; import { initialAsyncRequestState } from './utils/redux';
const AlertGroups = () => { const AlertGroups = () => {
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi; const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi;
const alertManagers = useAlertManagersByPermission('instance'); const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers); const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
@ -34,7 +34,7 @@ const AlertGroups = () => {
const { groupBy = [] } = getFiltersFromUrlParams(queryParams); const { groupBy = [] } = getFiltersFromUrlParams(queryParams);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery(); const { currentData: amConfigStatus } = useGetAlertmanagerChoiceStatusQuery();
const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups); const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups);
const { const {
@ -47,7 +47,8 @@ const AlertGroups = () => {
const filteredAlertGroups = useFilteredAmGroups(groupedAlerts); const filteredAlertGroups = useFilteredAmGroups(groupedAlerts);
const grafanaAmDeliveryDisabled = const grafanaAmDeliveryDisabled =
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && alertmanagerChoice === AlertmanagerChoice.External; alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External;
useEffect(() => { useEffect(() => {
function fetchNotifications() { function fetchNotifications() {

View File

@ -7,7 +7,6 @@ import { useDispatch } from 'app/types';
import { useCleanup } from '../../../core/hooks/useCleanup'; import { useCleanup } from '../../../core/hooks/useCleanup';
import { alertmanagerApi } from './api/alertmanagerApi';
import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
@ -29,12 +28,11 @@ import { initialAsyncRequestState } from './utils/redux';
const AmRoutes = () => { const AmRoutes = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [isRootRouteEditMode, setIsRootRouteEditMode] = useState(false); const [isRootRouteEditMode, setIsRootRouteEditMode] = useState(false);
const alertManagers = useAlertManagersByPermission('notification'); const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery();
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
@ -130,10 +128,7 @@ const AmRoutes = () => {
{resultError.message || 'Unknown error.'} {resultError.message || 'Unknown error.'}
</Alert> </Alert>
)} )}
<GrafanaAlertmanagerDeliveryWarning <GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
currentAlertmanager={alertManagerSourceName}
alertmanagerChoice={alertmanagerChoice}
/>
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.RootNotificationPolicy} />} {isProvisioned && <ProvisioningAlert resource={ProvisionedResource.RootNotificationPolicy} />}
{resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />} {resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{result && !resultLoading && !resultError && ( {result && !resultLoading && !resultError && (

View File

@ -24,6 +24,7 @@ import 'whatwg-fetch';
import Receivers from './Receivers'; import Receivers from './Receivers';
import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from './api/alertmanager'; import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from './api/alertmanager';
import { AlertmanagersChoiceResponse } from './api/alertmanagerApi';
import { discoverAlertmanagerFeatures } from './api/buildInfo'; import { discoverAlertmanagerFeatures } from './api/buildInfo';
import { fetchNotifiers } from './api/grafana'; import { fetchNotifiers } from './api/grafana';
import * as receiversApi from './api/receiversApi'; import * as receiversApi from './api/receiversApi';
@ -63,6 +64,11 @@ const mocks = {
contextSrv: jest.mocked(contextSrv), contextSrv: jest.mocked(contextSrv),
}; };
const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = {
alertmanagersChoice: AlertmanagerChoice.Internal,
numExternalAlertmanagers: 0,
};
const renderReceivers = (alertManagerSourceName?: string) => { const renderReceivers = (alertManagerSourceName?: string) => {
const store = configureStore(); const store = configureStore();
@ -185,7 +191,7 @@ describe('Receivers', () => {
}); });
it('Template and receiver tables are rendered, alertmanager can be selected, no notification errors', async () => { 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) => mocks.api.fetchConfig.mockImplementation((name) =>
Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig) Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig)
); );
@ -230,7 +236,7 @@ describe('Receivers', () => {
}); });
it('Grafana receiver can be tested', async () => { it('Grafana receiver can be tested', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
@ -288,7 +294,7 @@ describe('Receivers', () => {
}); });
it('Grafana receiver can be created', async () => { it('Grafana receiver can be created', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue(); mocks.api.updateConfig.mockResolvedValue();
@ -352,7 +358,7 @@ describe('Receivers', () => {
}); });
it('Hides create contact point button for users without permission', () => { 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.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue(); mocks.api.updateConfig.mockResolvedValue();
@ -368,7 +374,7 @@ describe('Receivers', () => {
}); });
it('Cloud alertmanager receiver can be edited', async () => { it('Cloud alertmanager receiver can be edited', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig); mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue(); mocks.api.updateConfig.mockResolvedValue();
@ -464,7 +470,7 @@ describe('Receivers', () => {
}); });
it('Prometheus Alertmanager receiver cannot be edited', async () => { it('Prometheus Alertmanager receiver cannot be edited', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All }); mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchStatus.mockResolvedValue({ mocks.api.fetchStatus.mockResolvedValue({
...someCloudAlertManagerStatus, ...someCloudAlertManagerStatus,
@ -503,7 +509,7 @@ describe('Receivers', () => {
}); });
it('Loads config from status endpoint if there is no user config', async () => { 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 // loading an empty config with make it fetch config from status endpoint
mocks.api.fetchConfig.mockResolvedValue({ mocks.api.fetchConfig.mockResolvedValue({
template_files: {}, 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 () => { 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.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true });
mocks.api.fetchConfig.mockRejectedValue({ message: 'alertmanager storage object not found' }); mocks.api.fetchConfig.mockRejectedValue({ message: 'alertmanager storage object not found' });
@ -542,7 +548,7 @@ describe('Receivers', () => {
describe('Contact points state', () => { describe('Contact points state', () => {
it('Should render error notifications when there are some points state ', async () => { 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.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue(); mocks.api.updateConfig.mockResolvedValue();
@ -614,7 +620,7 @@ describe('Receivers', () => {
expect(byText('OK').getAll(criticalDetailTable)).toHaveLength(2); 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 () => { 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.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue(); 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 () => { 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.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue(); mocks.api.updateConfig.mockResolvedValue();

View File

@ -10,7 +10,6 @@ import { useDispatch } from 'app/types';
import { ContactPointsState } from '../../../types'; import { ContactPointsState } from '../../../types';
import { alertmanagerApi } from './api/alertmanagerApi';
import { useGetContactPointsState } from './api/receiversApi'; import { useGetContactPointsState } from './api/receiversApi';
import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertingPageWrapper } from './components/AlertingPageWrapper';
@ -55,8 +54,6 @@ function NotificationError({ errorCount }: NotificationErrorProps) {
type PageType = 'receivers' | 'templates' | 'global-config'; type PageType = 'receivers' | 'templates' | 'global-config';
const Receivers = () => { const Receivers = () => {
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi;
const alertManagers = useAlertManagersByPermission('notification'); const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -97,8 +94,6 @@ const Receivers = () => {
const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? ''); const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? '');
const integrationsErrorCount = contactPointsState?.errorCount ?? 0; const integrationsErrorCount = contactPointsState?.errorCount ?? 0;
const { data: alertmanagerChoice } = useGetAlertmanagerChoiceQuery();
const disableAmSelect = !isRoot; const disableAmSelect = !isRoot;
let pageNav = getPageNavigationModel(type, id); let pageNav = getPageNavigationModel(type, id);
@ -131,10 +126,7 @@ const Receivers = () => {
{error.message || 'Unknown error.'} {error.message || 'Unknown error.'}
</Alert> </Alert>
)} )}
<GrafanaAlertmanagerDeliveryWarning <GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
alertmanagerChoice={alertmanagerChoice}
currentAlertmanager={alertManagerSourceName}
/>
{loading && !config && <LoadingPlaceholder text="loading configuration..." />} {loading && !config && <LoadingPlaceholder text="loading configuration..." />}
{config && !error && ( {config && !error && (
<Switch> <Switch>

View File

@ -5,7 +5,6 @@ import { Alert, withErrorBoundary } from '@grafana/ui';
import { Silence } from 'app/plugins/datasource/alertmanager/types'; import { Silence } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types'; import { useDispatch } from 'app/types';
import { alertmanagerApi } from './api/alertmanagerApi';
import { featureDiscoveryApi } from './api/featureDiscoveryApi'; import { featureDiscoveryApi } from './api/featureDiscoveryApi';
import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertingPageWrapper } from './components/AlertingPageWrapper';
@ -26,7 +25,6 @@ const Silences = () => {
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi;
const silences = useUnifiedAlertingSelector((state) => state.silences); const silences = useUnifiedAlertingSelector((state) => state.silences);
const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts); const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts);
const alertsRequest = alertManagerSourceName const alertsRequest = alertManagerSourceName
@ -42,8 +40,6 @@ const Silences = () => {
{ skip: !alertManagerSourceName } { skip: !alertManagerSourceName }
); );
const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery();
useEffect(() => { useEffect(() => {
function fetchAll() { function fetchAll() {
if (alertManagerSourceName) { if (alertManagerSourceName) {
@ -84,10 +80,7 @@ const Silences = () => {
onChange={setAlertManagerSourceName} onChange={setAlertManagerSourceName}
dataSources={alertManagers} dataSources={alertManagers}
/> />
<GrafanaAlertmanagerDeliveryWarning <GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
currentAlertmanager={alertManagerSourceName}
alertmanagerChoice={alertmanagerChoice}
/>
{mimirLazyInitError && ( {mimirLazyInitError && (
<Alert title="The selected Alertmanager has no configuration" severity="warning"> <Alert title="The selected Alertmanager has no configuration" severity="warning">

View File

@ -9,14 +9,14 @@ import { alertingApi } from './alertingApi';
export interface AlertmanagersChoiceResponse { export interface AlertmanagersChoiceResponse {
alertmanagersChoice: AlertmanagerChoice; alertmanagersChoice: AlertmanagerChoice;
numExternalAlertmanagers: number;
} }
export const alertmanagerApi = alertingApi.injectEndpoints({ export const alertmanagerApi = alertingApi.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
getAlertmanagerChoice: build.query<AlertmanagerChoice, void>({ getAlertmanagerChoiceStatus: build.query<AlertmanagersChoiceResponse, void>({
query: () => ({ url: '/api/v1/ngalert' }), query: () => ({ url: '/api/v1/ngalert' }),
providesTags: ['AlertmanagerChoice'], providesTags: ['AlertmanagerChoice'],
transformResponse: (response: AlertmanagersChoiceResponse) => response.alertmanagersChoice,
}), }),
getExternalAlertmanagerConfig: build.query<ExternalAlertmanagerConfig, void>({ 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 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 { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
import { mockAlertmanagerChoiceResponse } from '../mocks/alertmanagerApi';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { GrafanaAlertmanagerDeliveryWarning } from './GrafanaAlertmanagerDeliveryWarning'; import { GrafanaAlertmanagerDeliveryWarning } from './GrafanaAlertmanagerDeliveryWarning';
describe('GrafanaAlertmanagerDeliveryWarning', () => { describe('GrafanaAlertmanagerDeliveryWarning', () => {
describe('When AlertmanagerChoice set to External', () => { const server = setupServer();
beforeAll(() => {
setBackendSrv(backendSrv);
server.listen({ onUnhandledRequest: 'error' });
});
afterAll(() => {
server.close();
});
beforeEach(() => {
server.resetHandlers();
});
it('Should not render when the datasource is not Grafana', () => { it('Should not render when the datasource is not Grafana', () => {
const { container } = render( mockAlertmanagerChoiceResponse(server, {
<GrafanaAlertmanagerDeliveryWarning alertmanagersChoice: AlertmanagerChoice.External,
currentAlertmanager="custom-alertmanager" numExternalAlertmanagers: 0,
alertmanagerChoice={AlertmanagerChoice.External} });
/>
const { container } = renderWithStore(
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager="custom-alertmanager" />
); );
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
it('Should render warning when the datasource is Grafana', () => { it('Should render warning when the datasource is Grafana and using external AM', async () => {
const { container } = render( mockAlertmanagerChoiceResponse(server, {
<GrafanaAlertmanagerDeliveryWarning alertmanagersChoice: AlertmanagerChoice.External,
currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} numExternalAlertmanagers: 1,
alertmanagerChoice={AlertmanagerChoice.External}
/>
);
expect(container).toHaveTextContent('Grafana alerts are not delivered to Grafana Alertmanager');
});
}); });
it.each([AlertmanagerChoice.All, AlertmanagerChoice.Internal])( renderWithStore(<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
'Should not render when datasource is Grafana and Alertmanager choice is %s',
(choice) => { expect(await screen.findByText('Grafana alerts are not delivered to Grafana Alertmanager')).toBeVisible();
const { container } = render( });
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} it('Should render warning when the datasource is Grafana and using All AM', async () => {
alertmanagerChoice={choice} 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(); 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,39 +5,60 @@ import { GrafanaTheme2 } from '@grafana/data/src';
import { Alert, useStyles2 } from '@grafana/ui/src'; import { Alert, useStyles2 } from '@grafana/ui/src';
import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types'; import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
interface GrafanaAlertmanagerDeliveryWarningProps { interface GrafanaAlertmanagerDeliveryWarningProps {
alertmanagerChoice?: AlertmanagerChoice;
currentAlertmanager: string; currentAlertmanager: string;
} }
export function GrafanaAlertmanagerDeliveryWarning({ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: GrafanaAlertmanagerDeliveryWarningProps) {
alertmanagerChoice,
currentAlertmanager,
}: GrafanaAlertmanagerDeliveryWarningProps) {
const styles = useStyles2(getStyles); 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; return null;
} }
if (alertmanagerChoice !== AlertmanagerChoice.External) { const hasActiveExternalAMs = amChoiceStatus.numExternalAlertmanagers > 0;
return null;
}
if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.External) {
return ( return (
<Alert title="Grafana alerts are not delivered to Grafana Alertmanager"> <Alert title="Grafana alerts are not delivered to Grafana Alertmanager">
Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration
will not affect delivery of your alerts! will not affect delivery of your alerts.
<div className={styles.adminHint}> <div className={styles.adminHint}>
You can change the configuration on the Alerting Admin page. If you do not have access, contact your To change your Alertmanager setup, go to the Alerting Admin page. If you do not have access, contact your
Administrator Administrator.
</div> </div>
</Alert> </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) => ({ const getStyles = (theme: GrafanaTheme2) => ({
adminHint: css` adminHint: css`
font-size: ${theme.typography.bodySmall.fontSize}; font-size: ${theme.typography.bodySmall.fontSize};

View File

@ -1,4 +1,5 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
@ -7,12 +8,15 @@ import { byRole } from 'testing-library-selector';
import { setBackendSrv } from '@grafana/runtime'; import { setBackendSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore'; import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { AlertmanagersChoiceResponse } from '../../api/alertmanagerApi';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable'; import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { getCloudRule, getGrafanaRule } from '../../mocks'; import { getCloudRule, getGrafanaRule } from '../../mocks';
import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi';
import { RuleDetails } from './RuleDetails'; import { RuleDetails } from './RuleDetails';
@ -32,11 +36,27 @@ const ui = {
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true); jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
const server = setupServer();
const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = {
alertmanagersChoice: AlertmanagerChoice.Internal,
numExternalAlertmanagers: 0,
};
beforeAll(() => { beforeAll(() => {
setBackendSrv(backendSrv); setBackendSrv(backendSrv);
server.listen({ onUnhandledRequest: 'error' });
jest.clearAllMocks(); jest.clearAllMocks();
}); });
afterAll(() => {
server.close();
});
beforeEach(() => {
server.resetHandlers();
});
describe('RuleDetails RBAC', () => { describe('RuleDetails RBAC', () => {
describe('Grafana rules action buttons in details', () => { describe('Grafana rules action buttons in details', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' }); 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 () => { it('Should not render Silence button for users wihout the instance create permission', async () => {
// Arrange // Arrange
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
// Act // Act
renderRuleDetails(grafanaRule); renderRuleDetails(grafanaRule);
@ -78,6 +99,8 @@ describe('RuleDetails RBAC', () => {
}); });
it('Should render Silence button for users with the instance create permissions', async () => { it('Should render Silence button for users with the instance create permissions', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
// Arrange // Arrange
jest jest
.spyOn(contextSrv, 'hasPermission') .spyOn(contextSrv, 'hasPermission')
@ -91,6 +114,7 @@ describe('RuleDetails RBAC', () => {
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' })); await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
}); });
}); });
describe('Cloud rules action buttons', () => { describe('Cloud rules action buttons', () => {
const cloudRule = getCloudRule({ name: 'Cloud' }); 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 { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction, useDispatch } from 'app/types'; import { AccessControlAction, useDispatch } from 'app/types';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable'; import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal'; import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
import { deleteRuleAction } from '../../state/actions'; import { deleteRuleAction } from '../../state/actions';
@ -84,6 +86,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
const rulesPermissions = getRulesPermissions(rulesSourceName); const rulesPermissions = getRulesPermissions(rulesSourceName);
const hasCreateRulePermission = contextSrv.hasPermission(rulesPermissions.create); const hasCreateRulePermission = contextSrv.hasPermission(rulesPermissions.create);
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule); const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
const canSilence = useCanSilence(rule);
const returnTo = location.pathname + location.search; const returnTo = location.pathname + location.search;
// explore does not support grafana rule queries atm // 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( buttons.push(
<LinkButton <LinkButton
size="sm" size="sm"
@ -263,6 +266,31 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
return null; 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) => ({ export const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css` wrapper: css`
padding: ${theme.spacing(2)} 0; padding: ${theme.spacing(2)} 0;

View File

@ -64,12 +64,12 @@ export const MatchedSilencedRules = () => {
{matchers.every((matcher) => !matcher.value && !matcher.name) ? ( {matchers.every((matcher) => !matcher.value && !matcher.name) ? (
<span>Add a valid matcher to see affected alerts</span> <span>Add a valid matcher to see affected alerts</span>
) : ( ) : (
<> <DynamicTable
<DynamicTable items={matchedAlertRules.slice(0, 5) ?? []} isExpandable={false} cols={columns} /> items={matchedAlertRules}
{matchedAlertRules.length > 5 && ( isExpandable={false}
<div className={styles.moreMatches}>and {matchedAlertRules.length - 5} more</div> cols={columns}
)} pagination={{ itemsPerPage: 5 }}
</> />
)} )}
</div> </div>
</div> </div>
@ -92,7 +92,7 @@ function useColumns(): MatchedRulesTableColumnProps[] {
renderCell: function renderName({ data: { matchedInstance } }) { renderCell: function renderName({ data: { matchedInstance } }) {
return <AlertLabels labels={matchedInstance.labels} />; return <AlertLabels labels={matchedInstance.labels} />;
}, },
size: '250px', size: 'auto',
}, },
{ {
id: 'created', 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 => { export const mockPromRecordingRule = (partial: Partial<RecordingRule> = {}): RecordingRule => {
return { return {
type: PromRuleType.Recording, type: PromRuleType.Recording,
@ -570,6 +589,7 @@ export function getGrafanaRule(override?: Partial<CombinedRule>) {
name: 'Grafana', name: 'Grafana',
rulesSource: 'grafana', rulesSource: 'grafana',
}, },
rulerRule: mockGrafanaRulerRule(),
...override, ...override,
}); });
} }