For each external Alertmanager you can define global settings, like server addresses, usernames and
diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx
index f8b7ed01eb5..b15eb6ec741 100644
--- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx
+++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx
@@ -14,6 +14,7 @@ import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } fr
import { backendSrv } from '../../../../../core/services/backend_srv';
import * as receiversApi from '../../api/receiversApi';
import { enableRBAC, grantUserPermissions } from '../../mocks';
+import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { fetchGrafanaNotifiersAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { createUrl } from '../../utils/url';
@@ -39,7 +40,9 @@ const renderReceieversTable = async (
return render(
-
+
+
+
);
};
diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx
index a283792577c..247d9a90b1b 100644
--- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx
+++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx
@@ -4,14 +4,16 @@ import React, { useMemo, useState } from 'react';
import { dateTime, dateTimeFormat } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Badge, Button, ConfirmModal, Icon, Modal, useStyles2 } from '@grafana/ui';
-import { contextSrv } from 'app/core/services/context_srv';
+import { contextSrv } from 'app/core/core';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
-import { AccessControlAction, ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
+import { ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
import { isOrgAdmin } from '../../../../plugins/admin/permissions';
import { useGetContactPointsState } from '../../api/receiversApi';
import { Authorize } from '../../components/Authorize';
+import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
+import { useAlertmanager } from '../../state/AlertmanagerContext';
import { deleteReceiverAction } from '../../state/actions';
import { getAlertTableStyles } from '../../styles/table';
import { SupportedPlugin } from '../../types/pluginBridges';
@@ -34,10 +36,10 @@ interface UpdateActionProps extends ActionProps {
onClickDeleteReceiver: (receiverName: string) => void;
}
-function UpdateActions({ permissions, alertManagerName, receiverName, onClickDeleteReceiver }: UpdateActionProps) {
+function UpdateActions({ alertManagerName, receiverName, onClickDeleteReceiver }: UpdateActionProps) {
return (
<>
-
+
-
+
onClickDeleteReceiver(receiverName)}
tooltip="Delete contact point"
@@ -61,23 +63,13 @@ function UpdateActions({ permissions, alertManagerName, receiverName, onClickDel
}
interface ActionProps {
- permissions: {
- read: AccessControlAction;
- create: AccessControlAction;
- update: AccessControlAction;
- delete: AccessControlAction;
- provisioning: {
- read: AccessControlAction;
- readSecrets: AccessControlAction;
- };
- };
alertManagerName: string;
receiverName: string;
}
-function ViewAction({ permissions, alertManagerName, receiverName }: ActionProps) {
+function ViewAction({ alertManagerName, receiverName }: ActionProps) {
return (
-
+
+
{
const dispatch = useDispatch();
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
- const permissions = getNotificationsPermissions(alertManagerName);
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
const configHealth = useAlertmanagerConfigHealth(config.alertmanager_config);
@@ -305,11 +300,8 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
const [receiverToDelete, setReceiverToDelete] = useState();
const [showCannotDeleteReceiverModal, setShowCannotDeleteReceiverModal] = useState(false);
- const isGrafanaAM = alertManagerName === GRAFANA_RULES_SOURCE_NAME;
- const showExport =
- isGrafanaAM &&
- (contextSrv.hasPermission(permissions.provisioning.read) ||
- contextSrv.hasPermission(permissions.provisioning.readSecrets));
+ const [supportsExport, allowedToExport] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
+ const showExport = supportsExport && allowedToExport;
const onClickDeleteReceiver = (receiverName: string): void => {
if (isReceiverUsed(receiverName, config)) {
@@ -355,15 +347,16 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
contactPointsState,
configHealth,
onClickDeleteReceiver,
- permissions,
isVanillaAM
);
+ const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
+
return (
void,
- permissions: {
- read: AccessControlAction;
- create: AccessControlAction;
- update: AccessControlAction;
- delete: AccessControlAction;
- provisioning: {
- read: AccessControlAction;
- readSecrets: AccessControlAction;
- };
- },
isVanillaAM: boolean
): RowTableColumnProps[] {
const tableStyles = useStyles2(getAlertTableStyles);
@@ -510,28 +493,21 @@ function useGetColumns(
renderCell: ({ data: { provisioned, name } }) => (
{!isVanillaAM && !provisioned && (
)}
- {(isVanillaAM || provisioned) && (
-
- )}
- {isGrafanaAlertManager && (
-
- )}
+ {(isVanillaAM || provisioned) && }
+ {isGrafanaAlertManager && }
),
diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx
index 77e0aa0efbd..b173d5387c6 100644
--- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx
+++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx
@@ -4,11 +4,13 @@ import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { locationService } from '@grafana/runtime';
-import { contextSrv } from 'app/core/services/context_srv';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
+import { grantUserPermissions } from '../../mocks';
+import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
+
import { TemplatesTable } from './TemplatesTable';
const defaultConfig: AlertManagerCortexConfig = {
@@ -25,7 +27,6 @@ jest.mock('app/types', () => ({
}));
jest.mock('app/core/services/context_srv');
-const contextSrvMock = jest.mocked(contextSrv);
const renderWithProvider = () => {
const store = configureStore();
@@ -33,7 +34,9 @@ const renderWithProvider = () => {
render(
-
+
+
+
);
@@ -42,16 +45,12 @@ const renderWithProvider = () => {
describe('TemplatesTable', () => {
beforeEach(() => {
jest.resetAllMocks();
- contextSrvMock.hasAccess.mockImplementation(() => true);
- contextSrvMock.hasPermission.mockImplementation((action) => {
- const permissions = [
- AccessControlAction.AlertingNotificationsRead,
- AccessControlAction.AlertingNotificationsWrite,
- AccessControlAction.AlertingNotificationsExternalRead,
- AccessControlAction.AlertingNotificationsExternalWrite,
- ];
- return permissions.includes(action as AccessControlAction);
- });
+ grantUserPermissions([
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsWrite,
+ AccessControlAction.AlertingNotificationsExternalRead,
+ AccessControlAction.AlertingNotificationsExternalWrite,
+ ]);
});
it('Should render templates table with the correct rows', () => {
renderWithProvider();
@@ -64,13 +63,11 @@ describe('TemplatesTable', () => {
expect(within(rows[0]).getByRole('cell', { name: /Copy/i })).toBeInTheDocument();
});
it('Should not render duplicate template button when not having write permissions', () => {
- contextSrvMock.hasPermission.mockImplementation((action) => {
- const permissions = [
- AccessControlAction.AlertingNotificationsRead,
- AccessControlAction.AlertingNotificationsExternalRead,
- ];
- return permissions.includes(action as AccessControlAction);
- });
+ grantUserPermissions([
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsExternalRead,
+ ]);
+
renderWithProvider();
const rows = screen.getAllByRole('row', { name: /template1/i });
expect(within(rows[0]).queryByRole('cell', { name: /Copy/i })).not.toBeInTheDocument();
diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx
index 33d56c2360e..ddfa6e3a34f 100644
--- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx
+++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx
@@ -1,14 +1,13 @@
import React, { Fragment, useMemo, useState } from 'react';
import { ConfirmModal, useStyles2 } from '@grafana/ui';
-import { contextSrv } from 'app/core/services/context_srv';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { Authorize } from '../../components/Authorize';
+import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { deleteTemplateAction } from '../../state/actions';
import { getAlertTableStyles } from '../../styles/table';
-import { getNotificationsPermissions } from '../../utils/access-control';
import { makeAMLink } from '../../utils/misc';
import { CollapseToggle } from '../CollapseToggle';
import { DetailsField } from '../DetailsField';
@@ -27,7 +26,9 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
const dispatch = useDispatch();
const [expandedTemplates, setExpandedTemplates] = useState>({});
const tableStyles = useStyles2(getAlertTableStyles);
- const permissions = getNotificationsPermissions(alertManagerName);
+ const [createNotificationTemplateSupported, createNotificationTemplateAllowed] = useAlertmanagerAbility(
+ AlertmanagerAction.CreateNotificationTemplate
+ );
const templateRows = useMemo(() => {
const templates = Object.entries(config.template_files);
@@ -53,7 +54,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
description="Create notification templates to customize your notifications."
addButtonLabel="Add template"
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
- showButton={contextSrv.hasPermission(permissions.create)}
+ showButton={createNotificationTemplateSupported && createNotificationTemplateAllowed}
>
@@ -65,7 +66,13 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
|
Template |
-
+
Actions |
@@ -102,7 +109,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
/>
)}
{!provenance && (
-
+
{
/>
)}
- {contextSrv.hasPermission(permissions.create) && (
+
{
tooltip="Copy template"
icon="copy"
/>
- )}
-
+
{!provenance && (
-
+
setTemplateToDelete(name)}
tooltip="delete template"
diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx
index a6997717a7e..5a2ef4156cd 100644
--- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx
@@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import { byRole, byTestId, byText } from 'testing-library-selector';
-import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction } from 'app/types/accessControl';
import 'core-js/stable/structured-clone';
@@ -11,7 +10,7 @@ import { TestProvider } from '../../../../../../../test/helpers/TestProvider';
import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types';
import { Labels } from '../../../../../../types/unified-alerting-dto';
import { mockApi, setupMswServer } from '../../../mockApi';
-import { mockAlertQuery } from '../../../mocks';
+import { grantUserPermissions, mockAlertQuery } from '../../../mocks';
import { mockPreviewApiResponse } from '../../../mocks/alertRuleApi';
import * as dataSource from '../../../utils/datasource';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
@@ -34,8 +33,6 @@ jest.spyOn(notificationPreview, 'useGetAlertManagersSourceNamesAndImage').mockRe
]);
jest.spyOn(dataSource, 'getDatasourceAPIUid').mockImplementation((ds: string) => ds);
-jest.mock('app/core/services/context_srv');
-const contextSrvMock = jest.mocked(contextSrv);
const useGetAlertManagersSourceNamesAndImageMock = useGetAlertManagersSourceNamesAndImage as jest.MockedFunction<
typeof useGetAlertManagersSourceNamesAndImage
@@ -113,20 +110,19 @@ function mockTwoAlertManagers() {
}
function mockHasEditPermission(enabled: boolean) {
- contextSrvMock.accessControlEnabled.mockReturnValue(true);
- contextSrvMock.hasAccess.mockImplementation((action) => {
- const onlyReadPermissions: string[] = [
- AccessControlAction.AlertingNotificationsRead,
- AccessControlAction.AlertingNotificationsExternalRead,
- ];
- const readAndWritePermissions: string[] = [
- AccessControlAction.AlertingNotificationsRead,
- AccessControlAction.AlertingNotificationsWrite,
- AccessControlAction.AlertingNotificationsExternalRead,
- AccessControlAction.AlertingNotificationsExternalWrite,
- ];
- return enabled ? readAndWritePermissions.includes(action) : onlyReadPermissions.includes(action);
- });
+ const onlyReadPermissions = [
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsExternalRead,
+ ];
+
+ const readAndWritePermissions = [
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsWrite,
+ AccessControlAction.AlertingNotificationsExternalRead,
+ AccessControlAction.AlertingNotificationsExternalWrite,
+ ];
+
+ return enabled ? grantUserPermissions(readAndWritePermissions) : grantUserPermissions(onlyReadPermissions);
}
const folder: Folder = {
diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx
index 57a246b23eb..f66e525f8a1 100644
--- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx
@@ -7,7 +7,9 @@ import { Button, Icon, Modal, useStyles2 } from '@grafana/ui';
import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types';
import { Stack } from '../../../../../../plugins/datasource/parca/QueryEditor/Stack';
-import { getNotificationsPermissions } from '../../../utils/access-control';
+import { AlertmanagerAction } from '../../../hooks/useAbilities';
+import { AlertmanagerProvider } from '../../../state/AlertmanagerContext';
+import { GRAFANA_DATASOURCE_NAME } from '../../../utils/datasource';
import { makeAMLink } from '../../../utils/misc';
import { Authorize } from '../../Authorize';
import { Matchers } from '../../notification-policies/Matchers';
@@ -57,54 +59,57 @@ export function NotificationRouteDetailsModal({
const styles = useStyles2(getStyles);
const isDefault = isDefaultPolicy(route);
- const permissions = getNotificationsPermissions(alertManagerSourceName);
return (
-
-
- Your alert instances are routed as follows.
- Notification policy path
- {isDefault && Default policy
}
-
- {!isDefault && (
- <>
-
- >
- )}
-
-
-
- Contact point:
- {receiver.name}
-
-
+
+
+
+
+ Your alert instances are routed as follows.
+
+ Notification policy path
+ {isDefault && Default policy
}
+
+ {!isDefault && (
+ <>
+
+ >
+ )}
+
+
-
-
-
-
-
+
+
+
+ See details
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx
index 68486537dcb..ead6e0cf27f 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx
@@ -14,11 +14,10 @@ import {
import { config } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema';
import { DateTimePicker, LinkButton, useStyles2 } from '@grafana/ui';
+import { contextSrv } from 'app/core/core';
import { isExpressionQuery } from 'app/features/expressions/guards';
-import { AccessControlAction } from 'app/types';
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
-import { Authorize } from '../Authorize';
import { VizWrapper } from '../rule-editor/VizWrapper';
import { ThresholdDefinition } from '../rule-editor/util';
@@ -64,6 +63,8 @@ export function RuleViewerVisualization({
return null;
}
+ const allowedToExploreDataSources = contextSrv.hasAccessToExplore();
+
return (
@@ -71,19 +72,18 @@ export function RuleViewerVisualization({
{!isExpression && relativeTimeRange ? (
) : null}
-
- {!isExpression && (
-
- View in Explore
-
- )}
-
+
+ {allowedToExploreDataSources && !isExpression && (
+
+ View in Explore
+
+ )}
diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx
index 8a124e121e2..80edba11416 100644
--- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx
@@ -1,10 +1,10 @@
import React, { useEffect, useMemo } from 'react';
import { logInfo } from '@grafana/runtime';
-import { AccessControlAction } from 'app/types';
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
import { LogMessages } from '../../Analytics';
+import { AlertSourceAction } from '../../hooks/useAbilities';
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
import { Authorize } from '../Authorize';
@@ -36,10 +36,10 @@ export const RuleListGroupView = ({ namespaces, expandAll }: Props) => {
return (
<>
-
+
-
+
>
diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
index 3e38dedc3a1..402df1507c3 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
@@ -5,12 +5,11 @@ import { dateMath, GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { CollapsableSection, Icon, Link, LinkButton, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
-import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
+import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { expireSilenceAction } from '../../state/actions';
-import { getInstancesPermissions } from '../../utils/access-control';
import { parseMatchers } from '../../utils/alertmanager';
import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc';
import { Authorize } from '../Authorize';
@@ -41,7 +40,6 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }:
const [queryParams] = useQueryParams();
const filteredSilencesNotExpired = useFilteredSilences(silences, false);
const filteredSilencesExpired = useFilteredSilences(silences, true);
- const permissions = getInstancesPermissions(alertManagerSourceName);
const { silenceState: silenceStateInParams } = getSilenceFiltersFromUrlParams(queryParams);
const showExpiredFromUrl = silenceStateInParams === SilenceState.Expired;
@@ -77,7 +75,7 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }:
{!!silences.length && (
-
+
Add Silence
@@ -205,12 +203,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
function useColumns(alertManagerSourceName: string) {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
- const permissions = getInstancesPermissions(alertManagerSourceName);
+ const [updateSupported, updateAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateSilence);
+
return useMemo((): SilenceTableColumnProps[] => {
const handleExpireSilenceClick = (id: string) => {
dispatch(expireSilenceAction(alertManagerSourceName, id));
};
- const showActions = contextSrv.hasAccess(permissions.update, contextSrv.isEditor);
const columns: SilenceTableColumnProps[] = [
{
id: 'state',
@@ -254,7 +252,7 @@ function useColumns(alertManagerSourceName: string) {
size: 7,
},
];
- if (showActions) {
+ if (updateSupported && updateAllowed) {
columns.push({
id: 'actions',
label: 'Actions',
@@ -285,6 +283,6 @@ function useColumns(alertManagerSourceName: string) {
});
}
return columns;
- }, [alertManagerSourceName, dispatch, styles, permissions]);
+ }, [alertManagerSourceName, dispatch, styles.editButton, updateAllowed, updateSupported]);
}
export default SilencesTable;
diff --git a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap
new file mode 100644
index 00000000000..b763b6479a0
--- /dev/null
+++ b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap
@@ -0,0 +1,292 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`alertmanager abilities should report Create / Update / Delete actions aren't supported for external vanilla alertmanager 1`] = `
+{
+ "create-contact-point": [
+ false,
+ false,
+ ],
+ "create-mute-timing": [
+ false,
+ false,
+ ],
+ "create-notification-policy": [
+ false,
+ false,
+ ],
+ "create-notification-template": [
+ false,
+ false,
+ ],
+ "create-silence": [
+ false,
+ false,
+ ],
+ "delete-contact-point": [
+ false,
+ false,
+ ],
+ "delete-mute-timing": [
+ false,
+ false,
+ ],
+ "delete-notification-policy": [
+ false,
+ false,
+ ],
+ "delete-notification-template": [
+ false,
+ false,
+ ],
+ "edit-contact-points": [
+ false,
+ false,
+ ],
+ "edit-notification-template": [
+ false,
+ false,
+ ],
+ "export-contact-point": [
+ false,
+ false,
+ ],
+ "export-notification-policies": [
+ false,
+ false,
+ ],
+ "update-external-configuration": [
+ false,
+ false,
+ ],
+ "update-mute-timing": [
+ false,
+ false,
+ ],
+ "update-notification-policy-tree": [
+ false,
+ false,
+ ],
+ "update-silence": [
+ false,
+ false,
+ ],
+ "view-contact-point": [
+ true,
+ false,
+ ],
+ "view-external-configuration": [
+ true,
+ false,
+ ],
+ "view-mute-timing": [
+ true,
+ false,
+ ],
+ "view-notification-policy-tree": [
+ true,
+ false,
+ ],
+ "view-notification-template": [
+ true,
+ false,
+ ],
+ "view-silence": [
+ true,
+ false,
+ ],
+}
+`;
+
+exports[`alertmanager abilities should report everything except exporting for Mimir alertmanager 1`] = `
+{
+ "create-contact-point": [
+ true,
+ true,
+ ],
+ "create-mute-timing": [
+ true,
+ true,
+ ],
+ "create-notification-policy": [
+ true,
+ true,
+ ],
+ "create-notification-template": [
+ true,
+ true,
+ ],
+ "create-silence": [
+ true,
+ true,
+ ],
+ "delete-contact-point": [
+ true,
+ true,
+ ],
+ "delete-mute-timing": [
+ true,
+ true,
+ ],
+ "delete-notification-policy": [
+ true,
+ true,
+ ],
+ "delete-notification-template": [
+ true,
+ true,
+ ],
+ "edit-contact-points": [
+ true,
+ true,
+ ],
+ "edit-notification-template": [
+ true,
+ true,
+ ],
+ "export-contact-point": [
+ false,
+ false,
+ ],
+ "export-notification-policies": [
+ false,
+ false,
+ ],
+ "update-external-configuration": [
+ true,
+ true,
+ ],
+ "update-mute-timing": [
+ true,
+ true,
+ ],
+ "update-notification-policy-tree": [
+ true,
+ true,
+ ],
+ "update-silence": [
+ true,
+ true,
+ ],
+ "view-contact-point": [
+ true,
+ true,
+ ],
+ "view-external-configuration": [
+ true,
+ true,
+ ],
+ "view-mute-timing": [
+ true,
+ true,
+ ],
+ "view-notification-policy-tree": [
+ true,
+ true,
+ ],
+ "view-notification-template": [
+ true,
+ true,
+ ],
+ "view-silence": [
+ true,
+ true,
+ ],
+}
+`;
+
+exports[`alertmanager abilities should report everything is supported for builtin alertmanager 1`] = `
+{
+ "create-contact-point": [
+ true,
+ false,
+ ],
+ "create-mute-timing": [
+ true,
+ false,
+ ],
+ "create-notification-policy": [
+ true,
+ false,
+ ],
+ "create-notification-template": [
+ true,
+ false,
+ ],
+ "create-silence": [
+ true,
+ false,
+ ],
+ "delete-contact-point": [
+ true,
+ false,
+ ],
+ "delete-mute-timing": [
+ true,
+ false,
+ ],
+ "delete-notification-policy": [
+ true,
+ false,
+ ],
+ "delete-notification-template": [
+ true,
+ false,
+ ],
+ "edit-contact-points": [
+ true,
+ false,
+ ],
+ "edit-notification-template": [
+ true,
+ false,
+ ],
+ "export-contact-point": [
+ true,
+ false,
+ ],
+ "export-notification-policies": [
+ true,
+ false,
+ ],
+ "update-external-configuration": [
+ true,
+ false,
+ ],
+ "update-mute-timing": [
+ true,
+ false,
+ ],
+ "update-notification-policy-tree": [
+ true,
+ false,
+ ],
+ "update-silence": [
+ true,
+ false,
+ ],
+ "view-contact-point": [
+ true,
+ true,
+ ],
+ "view-external-configuration": [
+ true,
+ false,
+ ],
+ "view-mute-timing": [
+ true,
+ true,
+ ],
+ "view-notification-policy-tree": [
+ true,
+ true,
+ ],
+ "view-notification-template": [
+ true,
+ true,
+ ],
+ "view-silence": [
+ true,
+ true,
+ ],
+}
+`;
diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx
new file mode 100644
index 00000000000..f434d260f25
--- /dev/null
+++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx
@@ -0,0 +1,145 @@
+import { renderHook } from '@testing-library/react';
+import { createBrowserHistory } from 'history';
+import React, { PropsWithChildren } from 'react';
+import { Router } from 'react-router-dom';
+
+import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
+import { AccessControlAction } from 'app/types';
+
+import { grantUserPermissions, mockDataSource } from '../mocks';
+import { AlertmanagerProvider } from '../state/AlertmanagerContext';
+import { setupDataSources } from '../testSetup/datasources';
+import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
+
+import {
+ AlertmanagerAction,
+ useAlertmanagerAbilities,
+ useAlertmanagerAbility,
+ useAllAlertmanagerAbilities,
+} from './useAbilities';
+
+/**
+ * This test will write snapshots with a record of the current permissions assigned to actions.
+ * We encourage that every change to the snapshot is inspected _very_ thoroughly!
+ */
+describe('alertmanager abilities', () => {
+ it("should report Create / Update / Delete actions aren't supported for external vanilla alertmanager", () => {
+ setupDataSources(
+ mockDataSource({
+ name: GRAFANA_RULES_SOURCE_NAME,
+ type: DataSourceType.Alertmanager,
+ jsonData: { implementation: AlertManagerImplementation.prometheus },
+ })
+ );
+
+ const abilities = renderHook(() => useAllAlertmanagerAbilities(), { wrapper: createWrapper('does-not-exist') });
+ expect(abilities.result.current).toMatchSnapshot();
+ });
+
+ it('should report everything is supported for builtin alertmanager', () => {
+ setupDataSources(
+ mockDataSource({
+ name: GRAFANA_RULES_SOURCE_NAME,
+ type: DataSourceType.Alertmanager,
+ })
+ );
+
+ grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingInstanceRead]);
+
+ const abilities = renderHook(() => useAllAlertmanagerAbilities(), {
+ wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
+ });
+
+ Object.values(abilities.result.current).forEach(([supported]) => {
+ expect(supported).toBe(true);
+ });
+
+ // since we only granted "read" permissions, only those should be allowed
+ const viewAbility = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
+ wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
+ });
+
+ const [viewSupported, viewAllowed] = viewAbility.result.current;
+
+ expect(viewSupported).toBe(true);
+ expect(viewAllowed).toBe(true);
+
+ // editing should not be allowed, but supported
+ const editAbility = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
+ wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
+ });
+
+ const [editSupported, editAllowed] = editAbility.result.current;
+
+ expect(editSupported).toBe(true);
+ expect(editAllowed).toBe(true);
+
+ // record the snapshot to prevent future regressions
+ expect(abilities.result.current).toMatchSnapshot();
+ });
+
+ it('should report everything except exporting for Mimir alertmanager', () => {
+ setupDataSources(
+ mockDataSource({
+ name: 'mimir',
+ type: DataSourceType.Alertmanager,
+ jsonData: {
+ implementation: AlertManagerImplementation.mimir,
+ },
+ })
+ );
+
+ grantUserPermissions([
+ AccessControlAction.AlertingNotificationsExternalRead,
+ AccessControlAction.AlertingNotificationsExternalWrite,
+ AccessControlAction.AlertingInstancesExternalRead,
+ AccessControlAction.AlertingInstancesExternalWrite,
+ ]);
+
+ const abilities = renderHook(() => useAllAlertmanagerAbilities(), {
+ wrapper: createWrapper('mimir'),
+ });
+
+ expect(abilities.result.current).toMatchSnapshot();
+ });
+
+ it('should be able to return multiple abilities', () => {
+ setupDataSources(
+ mockDataSource({
+ name: GRAFANA_RULES_SOURCE_NAME,
+ type: DataSourceType.Alertmanager,
+ })
+ );
+
+ grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
+
+ const abilities = renderHook(
+ () =>
+ useAlertmanagerAbilities([
+ AlertmanagerAction.ViewContactPoint,
+ AlertmanagerAction.CreateContactPoint,
+ AlertmanagerAction.ExportContactPoint,
+ ]),
+ {
+ wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
+ }
+ );
+
+ expect(abilities.result.current).toHaveLength(3);
+ expect(abilities.result.current[0]).toStrictEqual([true, true]);
+ expect(abilities.result.current[1]).toStrictEqual([true, false]);
+ expect(abilities.result.current[2]).toStrictEqual([true, false]);
+ });
+});
+
+function createWrapper(alertmanagerSourceName: string) {
+ const wrapper = (props: PropsWithChildren) => (
+
+
+ {props.children}
+
+
+ );
+
+ return wrapper;
+}
diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts
new file mode 100644
index 00000000000..f9aae764b8f
--- /dev/null
+++ b/public/app/features/alerting/unified/hooks/useAbilities.ts
@@ -0,0 +1,205 @@
+import { useMemo } from 'react';
+
+import { contextSrv as ctx } from 'app/core/services/context_srv';
+import { AccessControlAction } from 'app/types';
+
+import { useAlertmanager } from '../state/AlertmanagerContext';
+import { getInstancesPermissions, getNotificationsPermissions } from '../utils/access-control';
+
+/**
+ * These hooks will determine if
+ * 1. the action is supported in the current alertmanager or data source context
+ * 2. user is allowed to perform actions based on their set of permissions / assigned role
+ */
+export enum AlertmanagerAction {
+ // configuration
+ ViewExternalConfiguration = 'view-external-configuration',
+ UpdateExternalConfiguration = 'update-external-configuration',
+
+ // contact points
+ CreateContactPoint = 'create-contact-point',
+ ViewContactPoint = 'view-contact-point',
+ UpdateContactPoint = 'edit-contact-points',
+ DeleteContactPoint = 'delete-contact-point',
+ ExportContactPoint = 'export-contact-point',
+
+ // notification templates
+ CreateNotificationTemplate = 'create-notification-template',
+ ViewNotificationTemplate = 'view-notification-template',
+ UpdateNotificationTemplate = 'edit-notification-template',
+ DeleteNotificationTemplate = 'delete-notification-template',
+
+ // notification policies
+ CreateNotificationPolicy = 'create-notification-policy',
+ ViewNotificationPolicyTree = 'view-notification-policy-tree',
+ UpdateNotificationPolicyTree = 'update-notification-policy-tree',
+ DeleteNotificationPolicy = 'delete-notification-policy',
+ ExportNotificationPolicies = 'export-notification-policies',
+
+ // silences – these cannot be deleted only "expired" (updated)
+ CreateSilence = 'create-silence',
+ ViewSilence = 'view-silence',
+ UpdateSilence = 'update-silence',
+
+ // mute timings
+ ViewMuteTiming = 'view-mute-timing',
+ CreateMuteTiming = 'create-mute-timing',
+ UpdateMuteTiming = 'update-mute-timing',
+ DeleteMuteTiming = 'delete-mute-timing',
+}
+
+export enum AlertSourceAction {
+ // internal (Grafana managed)
+ CreateAlertRule = 'create-alert-rule',
+ ViewAlertRule = 'view-alert-rule',
+ UpdateAlertRule = 'update-alert-rule',
+ DeleteAlertRule = 'delete-alert-rule',
+ // external (any compatible alerting data source)
+ CreateExternalAlertRule = 'create-external-alert-rule',
+ ViewExternalAlertRule = 'view-external-alert-rule',
+ UpdateExternalAlertRule = 'update-external-alert-rule',
+ DeleteExternalAlertRule = 'delete-external-alert-rule',
+}
+
+const AlwaysSupported = true; // this just makes it easier to understand the code
+export type Action = AlertmanagerAction | AlertSourceAction;
+
+export type Ability = [actionSupported: boolean, actionAllowed: boolean];
+export type Abilities = Record;
+
+export function useAlertSourceAbilities(): Abilities {
+ // TODO add the "supported" booleans here, we currently only do authorization
+
+ const abilities: Abilities = {
+ // -- Grafana managed alert rules --
+ [AlertSourceAction.CreateAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleCreate)],
+ [AlertSourceAction.ViewAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleRead)],
+ [AlertSourceAction.UpdateAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleUpdate)],
+ [AlertSourceAction.DeleteAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleDelete)],
+ // -- External alert rules (Mimir / Loki / etc) --
+ // for these we only have "read" and "write" permissions
+ [AlertSourceAction.CreateExternalAlertRule]: [
+ AlwaysSupported,
+ ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
+ ],
+ [AlertSourceAction.ViewExternalAlertRule]: [
+ AlwaysSupported,
+ ctx.hasPermission(AccessControlAction.AlertingRuleExternalRead),
+ ],
+ [AlertSourceAction.UpdateExternalAlertRule]: [
+ AlwaysSupported,
+ ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
+ ],
+ [AlertSourceAction.DeleteExternalAlertRule]: [
+ AlwaysSupported,
+ ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
+ ],
+ };
+
+ return abilities;
+}
+
+export function useAllAlertmanagerAbilities(): Abilities {
+ const {
+ selectedAlertmanager,
+ hasConfigurationAPI,
+ isGrafanaAlertmanager: isGrafanaFlavoredAlertmanager,
+ } = useAlertmanager();
+
+ // These are used for interacting with Alertmanager resources where we apply alert.notifications: permissions.
+ // There are different permissions based on wether the built-in alertmanager is selected (grafana) or an external one.
+ const notificationsPermissions = getNotificationsPermissions(selectedAlertmanager!);
+ const instancePermissions = getInstancesPermissions(selectedAlertmanager!);
+
+ // list out all of the abilities, and if the user has permissions to perform them
+ const abilities: Abilities = {
+ // -- configuration --
+ [AlertmanagerAction.ViewExternalConfiguration]: [
+ AlwaysSupported,
+ ctx.hasPermission(AccessControlAction.AlertingNotificationsExternalRead),
+ ],
+ [AlertmanagerAction.UpdateExternalConfiguration]: [
+ hasConfigurationAPI,
+ ctx.hasPermission(AccessControlAction.AlertingNotificationsExternalWrite),
+ ],
+ // -- contact points --
+ [AlertmanagerAction.CreateContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.create)],
+ [AlertmanagerAction.ViewContactPoint]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
+ [AlertmanagerAction.UpdateContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.update)],
+ [AlertmanagerAction.DeleteContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.delete)],
+ // only Grafana flavored alertmanager supports exporting
+ [AlertmanagerAction.ExportContactPoint]: [
+ isGrafanaFlavoredAlertmanager,
+ ctx.hasPermission(notificationsPermissions.provisioning.read) ||
+ ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
+ ],
+ // -- notification templates --
+ [AlertmanagerAction.CreateNotificationTemplate]: [
+ hasConfigurationAPI,
+ ctx.hasPermission(notificationsPermissions.create),
+ ],
+ [AlertmanagerAction.ViewNotificationTemplate]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
+ [AlertmanagerAction.UpdateNotificationTemplate]: [
+ hasConfigurationAPI,
+ ctx.hasPermission(notificationsPermissions.update),
+ ],
+ [AlertmanagerAction.DeleteNotificationTemplate]: [
+ hasConfigurationAPI,
+ ctx.hasPermission(notificationsPermissions.delete),
+ ],
+ // -- notification policies --
+ [AlertmanagerAction.CreateNotificationPolicy]: [
+ hasConfigurationAPI,
+ ctx.hasPermission(notificationsPermissions.create),
+ ],
+ [AlertmanagerAction.ViewNotificationPolicyTree]: [
+ AlwaysSupported,
+ ctx.hasPermission(notificationsPermissions.read),
+ ],
+ [AlertmanagerAction.UpdateNotificationPolicyTree]: [
+ hasConfigurationAPI,
+ ctx.hasPermission(notificationsPermissions.update),
+ ],
+ [AlertmanagerAction.DeleteNotificationPolicy]: [
+ hasConfigurationAPI,
+ ctx.hasPermission(notificationsPermissions.delete),
+ ],
+ [AlertmanagerAction.ExportNotificationPolicies]: [
+ isGrafanaFlavoredAlertmanager,
+ ctx.hasPermission(notificationsPermissions.provisioning.read) ||
+ ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
+ ],
+ // -- silences --
+ [AlertmanagerAction.CreateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.create)],
+ [AlertmanagerAction.ViewSilence]: [AlwaysSupported, ctx.hasPermission(instancePermissions.read)],
+ [AlertmanagerAction.UpdateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.update)],
+ // -- mute timtings --
+ [AlertmanagerAction.CreateMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.create)],
+ [AlertmanagerAction.ViewMuteTiming]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
+ [AlertmanagerAction.UpdateMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.update)],
+ [AlertmanagerAction.DeleteMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.delete)],
+ };
+
+ return abilities;
+}
+
+export function useAlertmanagerAbility(action: AlertmanagerAction): Ability {
+ const abilities = useAllAlertmanagerAbilities();
+
+ return useMemo(() => {
+ return abilities[action];
+ }, [abilities, action]);
+}
+
+export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability[] {
+ const abilities = useAllAlertmanagerAbilities();
+
+ return useMemo(() => {
+ return actions.map((action) => abilities[action]);
+ }, [abilities, actions]);
+}
+
+export function useAlertSourceAbility(action: AlertSourceAction): Ability {
+ const abilities = useAlertSourceAbilities();
+ return useMemo(() => abilities[action], [abilities, action]);
+}
diff --git a/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx b/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx
index 363ca104aa7..b1a00a5386e 100644
--- a/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx
+++ b/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx
@@ -4,12 +4,13 @@ import React from 'react';
import { MemoryRouter, Router } from 'react-router-dom';
import store from 'app/core/store';
+import { AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import * as useAlertManagerSources from '../hooks/useAlertManagerSources';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
-import { AlertmanagerProvider, useAlertmanager } from './AlertmanagerContext';
+import { AlertmanagerProvider, isAlertManagerWithConfigAPI, useAlertmanager } from './AlertmanagerContext';
const grafanaAm: AlertManagerDataSource = {
name: GRAFANA_RULES_SOURCE_NAME,
@@ -118,3 +119,23 @@ describe('useAlertmanager', () => {
expect(result.current.selectedAlertmanager).toBe(externalAmProm.name);
});
});
+
+test('isAlertManagerWithConfigAPI', () => {
+ expect(
+ isAlertManagerWithConfigAPI({
+ implementation: undefined,
+ })
+ ).toBe(true);
+
+ expect(
+ isAlertManagerWithConfigAPI({
+ implementation: AlertManagerImplementation.mimir,
+ })
+ ).toBe(true);
+
+ expect(
+ isAlertManagerWithConfigAPI({
+ implementation: AlertManagerImplementation.prometheus,
+ })
+ ).toBe(false);
+});
diff --git a/public/app/features/alerting/unified/state/AlertmanagerContext.tsx b/public/app/features/alerting/unified/state/AlertmanagerContext.tsx
index 8ae6653118f..57c68ccfe62 100644
--- a/public/app/features/alerting/unified/state/AlertmanagerContext.tsx
+++ b/public/app/features/alerting/unified/state/AlertmanagerContext.tsx
@@ -2,13 +2,21 @@ import * as React from 'react';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import store from 'app/core/store';
+import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
-import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
+import {
+ AlertManagerDataSource,
+ getAlertmanagerDataSourceByName,
+ GRAFANA_RULES_SOURCE_NAME,
+} from '../utils/datasource';
interface Context {
selectedAlertmanager: string | undefined;
+ hasConfigurationAPI: boolean; // returns true when a configuration API is available
+ isGrafanaAlertmanager: boolean; // returns true if we are dealing with the built-in Alertmanager
+ selectedAlertmanagerConfig: AlertManagerDataSourceJsonData | undefined;
availableAlertManagers: AlertManagerDataSource[];
setSelectedAlertmanager: (name: string) => void;
}
@@ -17,9 +25,11 @@ const AlertmanagerContext = React.createContext(undefined);
interface Props extends React.PropsWithChildren {
accessType: 'instance' | 'notification';
+ // manually setting the alertmanagersource name will override all of the other sources
+ alertmanagerSourceName?: string;
}
-const AlertmanagerProvider = ({ children, accessType }: Props) => {
+const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }: Props) => {
const [queryParams, updateQueryParams] = useQueryParams();
const availableAlertManagers = useAlertManagersByPermission(accessType);
@@ -45,13 +55,26 @@ const AlertmanagerProvider = ({ children, accessType }: Props) => {
const defaultSource = GRAFANA_RULES_SOURCE_NAME;
// queryParam > localStorage > default
- const desiredAlertmanager = sourceFromQuery ?? sourceFromStore ?? defaultSource;
+ const desiredAlertmanager = alertmanagerSourceName ?? sourceFromQuery ?? sourceFromStore ?? defaultSource;
const selectedAlertmanager = isAlertManagerAvailable(availableAlertManagers, desiredAlertmanager)
? desiredAlertmanager
: undefined;
+ const selectedAlertmanagerConfig = getAlertmanagerDataSourceByName(selectedAlertmanager)?.jsonData;
+
+ // determine if we're dealing with an Alertmanager data source that supports the ruler API
+ const isGrafanaAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
+ const isAlertmanagerWithConfigAPI = selectedAlertmanagerConfig
+ ? isAlertManagerWithConfigAPI(selectedAlertmanagerConfig)
+ : false;
+
+ const hasConfigurationAPI = isGrafanaAlertmanager || isAlertmanagerWithConfigAPI;
+
const value: Context = {
selectedAlertmanager,
+ hasConfigurationAPI,
+ isGrafanaAlertmanager,
+ selectedAlertmanagerConfig,
availableAlertManagers,
setSelectedAlertmanager: updateSelectedAlertmanager,
};
@@ -75,3 +98,15 @@ function isAlertManagerAvailable(availableAlertManagers: AlertManagerDataSource[
const availableAlertManagersNames = availableAlertManagers.map((am) => am.name);
return availableAlertManagersNames.includes(alertManagerName);
}
+
+// when the `implementation` is set to `undefined` we assume we're dealing with an AlertManager with config API. The reason for this is because
+// our Hosted Grafana stacks provision Alertmanager data sources without `jsonData: { implementation: "mimir" }`.
+const CONFIG_API_ENABLED_ALERTMANAGER_FLAVORS = [
+ AlertManagerImplementation.mimir,
+ AlertManagerImplementation.cortex,
+ undefined,
+];
+
+export function isAlertManagerWithConfigAPI(dataSourceConfig: AlertManagerDataSourceJsonData): boolean {
+ return CONFIG_API_ENABLED_ALERTMANAGER_FLAVORS.includes(dataSourceConfig.implementation);
+}
diff --git a/public/app/features/alerting/unified/utils/config.ts b/public/app/features/alerting/unified/utils/config.ts
index d5f7817561d..9472a8b543e 100644
--- a/public/app/features/alerting/unified/utils/config.ts
+++ b/public/app/features/alerting/unified/utils/config.ts
@@ -1,9 +1,12 @@
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { config } from '@grafana/runtime';
+import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
import { isValidPrometheusDuration, parsePrometheusDuration } from './time';
-export function getAllDataSources(): Array> {
+export function getAllDataSources(): Array<
+ DataSourceInstanceSettings
+> {
return Object.values(config.datasources);
}
diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts
index 23b234dbecc..d57ee10db1d 100644
--- a/public/app/features/alerting/unified/utils/datasource.ts
+++ b/public/app/features/alerting/unified/utils/datasource.ts
@@ -137,8 +137,7 @@ export function isCloudRulesSource(rulesSource: RulesSource | string): rulesSour
export function isVanillaPrometheusAlertManagerDataSource(name: string): boolean {
return (
name !== GRAFANA_RULES_SOURCE_NAME &&
- (getDataSourceByName(name)?.jsonData as AlertManagerDataSourceJsonData)?.implementation ===
- AlertManagerImplementation.prometheus
+ getAlertmanagerDataSourceByName(name)?.jsonData?.implementation === AlertManagerImplementation.prometheus
);
}
@@ -152,6 +151,13 @@ export function getDataSourceByName(name: string): DataSourceInstanceSettings source.name === name);
}
+export function getAlertmanagerDataSourceByName(name: string) {
+ return getAllDataSources().find(
+ (source): source is DataSourceInstanceSettings =>
+ source.name === name && source.type === 'alertmanager'
+ );
+}
+
export function getRulesSourceByName(name: string): RulesSource | undefined {
if (name === GRAFANA_RULES_SOURCE_NAME) {
return GRAFANA_RULES_SOURCE_NAME;