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()),
NumExternalAlertmanagers: len(externalAlertManagers),
}
return response.JSON(http.StatusOK, resp)
}

View File

@ -93,4 +93,5 @@ type GettableAlertmanagers struct {
// swagger:model
type AlertingStatus struct {
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', () => {
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', () => {
const { container } = render(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager="custom-alertmanager"
alertmanagerChoice={AlertmanagerChoice.External}
/>
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', () => {
const { container } = render(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={GRAFANA_RULES_SOURCE_NAME}
alertmanagerChoice={AlertmanagerChoice.External}
/>
);
expect(container).toHaveTextContent('Grafana alerts are not delivered to Grafana Alertmanager');
});
it('Should render warning when the datasource is Grafana and using external AM', async () => {
mockAlertmanagerChoiceResponse(server, {
alertmanagersChoice: AlertmanagerChoice.External,
numExternalAlertmanagers: 1,
});
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}
/>
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!
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
To change your Alertmanager setup, go to 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,
});
}