diff --git a/.betterer.results b/.betterer.results index 82af97cb0ac..95b94a48007 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1704,6 +1704,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "public/app/features/alerting/unified/components/Authorize.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] + ], "public/app/features/alerting/unified/components/Expression.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] @@ -1825,9 +1829,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/app/features/alerting/unified/utils/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/alerting/unified/utils/misc.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/public/app/features/alerting/unified/AlertGroups.test.tsx b/public/app/features/alerting/unified/AlertGroups.test.tsx index fd0dd82b153..9144f619db4 100644 --- a/public/app/features/alerting/unified/AlertGroups.test.tsx +++ b/public/app/features/alerting/unified/AlertGroups.test.tsx @@ -5,21 +5,21 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { byRole, byTestId, byText } from 'testing-library-selector'; import { setDataSourceSrv } from '@grafana/runtime'; +import { AccessControlAction } from 'app/types'; import AlertGroups from './AlertGroups'; import { fetchAlertGroups } from './api/alertmanager'; -import { mockAlertGroup, mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv } from './mocks'; +import { + grantUserPermissions, + mockAlertGroup, + mockAlertmanagerAlert, + mockDataSource, + MockDataSourceSrv, +} from './mocks'; +import { AlertmanagerProvider } from './state/AlertmanagerContext'; import { DataSourceType } from './utils/datasource'; jest.mock('./api/alertmanager'); - -jest.mock('app/core/services/context_srv', () => ({ - contextSrv: { - isEditor: true, - hasAccess: () => true, - hasPermission: () => true, - }, -})); const mocks = { api: { fetchAlertGroups: jest.mocked(fetchAlertGroups), @@ -29,7 +29,9 @@ const mocks = { const renderAmNotifications = () => { return render( - + + + ); }; @@ -57,6 +59,13 @@ const ui = { describe('AlertGroups', () => { beforeAll(() => { + grantUserPermissions([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstanceCreate, + AccessControlAction.AlertingInstancesExternalRead, + AccessControlAction.AlertingRuleRead, + ]); + mocks.api.fetchAlertGroups.mockImplementation(() => { return Promise.resolve([ mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }), diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx index 24b624f8c67..861c5658d9e 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.tsx @@ -31,7 +31,6 @@ import { updateAlertManagerConfigAction } from './state/actions'; import { FormAmRoute } from './types/amroutes'; import { useRouteGroupsMatcher } from './useRouteGroupsMatcher'; import { addUniqueIdentifierToRoute } from './utils/amroutes'; -import { isVanillaPrometheusAlertManagerDataSource } from './utils/datasource'; import { normalizeMatchers } from './utils/matchers'; import { computeInheritedTree } from './utils/notification-policies'; import { initialAsyncRequestState } from './utils/redux'; @@ -57,7 +56,7 @@ const AmRoutes = () => { const [labelMatchersFilter, setLabelMatchersFilter] = useState([]); const { getRouteGroupsMap } = useRouteGroupsMatcher(); - const { selectedAlertmanager } = useAlertmanager(); + const { selectedAlertmanager, hasConfigurationAPI } = useAlertmanager(); const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? ''); @@ -186,10 +185,6 @@ const AmRoutes = () => { return null; } - const vanillaPrometheusAlertManager = isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager); - const readOnlyPolicies = vanillaPrometheusAlertManager; - const readOnlyMuteTimings = vanillaPrometheusAlertManager; - const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0; const haveData = result && !resultError && !resultLoading; const isFetching = !result && resultLoading; @@ -246,7 +241,7 @@ const AmRoutes = () => { currentRoute={rootRoute} alertGroups={alertGroups ?? []} contactPointsState={contactPointsState.receivers} - readOnly={readOnlyPolicies} + readOnly={!hasConfigurationAPI} provisioned={isProvisioned} alertManagerSourceName={selectedAlertmanager} onAddPolicy={openAddModal} @@ -265,7 +260,7 @@ const AmRoutes = () => { )} {muteTimingsTabActive && ( - + )} )} diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 804a89759d1..f371c453c89 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -7,7 +7,6 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { DataSourceSrv, locationService, logInfo, setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; -import { contextSrv } from 'app/core/services/context_srv'; import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; import * as actions from 'app/features/alerting/unified/state/actions'; import { AccessControlAction } from 'app/types'; @@ -19,7 +18,6 @@ import { discoverFeatures } from './api/buildInfo'; import { fetchRules } from './api/prometheus'; import { deleteNamespace, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from './api/ruler'; import { - disableRBAC, enableRBAC, grantUserPermissions, mockDataSource, @@ -137,7 +135,12 @@ beforeAll(() => { describe('RuleList', () => { beforeEach(() => { - contextSrv.isEditor = true; + grantUserPermissions([ + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleUpdate, + AccessControlAction.AlertingRuleExternalRead, + AccessControlAction.AlertingRuleExternalWrite, + ]); mocks.rulesInSameGroupHaveInvalidForMock.mockReturnValue([]); }); @@ -147,7 +150,6 @@ describe('RuleList', () => { }); it('load & show rule groups from multiple cloud data sources', async () => { - disableRBAC(); mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources)); setDataSourceSrv(new MockDataSourceSrv(dataSources)); diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index d1e56f45d65..e478cb8baea 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -14,7 +14,7 @@ import { SilenceState } from '../../../plugins/datasource/alertmanager/types'; import Silences from './Silences'; import { createOrUpdateSilence, fetchAlerts, fetchSilences } from './api/alertmanager'; -import { mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks'; +import { grantUserPermissions, mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks'; import { parseMatchers } from './utils/alertmanager'; import { DataSourceType } from './utils/datasource'; @@ -98,19 +98,13 @@ const resetMocks = () => { mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence()); - mocks.contextSrv.evaluatePermission.mockImplementation(() => []); - mocks.contextSrv.hasPermission.mockImplementation((action) => { - const permissions = [ - AccessControlAction.AlertingInstanceRead, - AccessControlAction.AlertingInstanceCreate, - AccessControlAction.AlertingInstanceUpdate, - AccessControlAction.AlertingInstancesExternalRead, - AccessControlAction.AlertingInstancesExternalWrite, - ]; - return permissions.includes(action as AccessControlAction); - }); - - mocks.contextSrv.hasAccess.mockImplementation(() => true); + grantUserPermissions([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstanceCreate, + AccessControlAction.AlertingInstanceUpdate, + AccessControlAction.AlertingInstancesExternalRead, + AccessControlAction.AlertingInstancesExternalWrite, + ]); }; const setUserLogged = (isLogged: boolean) => { @@ -207,10 +201,7 @@ describe('Silences', () => { }); it('hides actions for creating a silence for users without access', async () => { - mocks.contextSrv.hasAccess.mockImplementation((action) => { - const permissions = [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead]; - return permissions.includes(action as AccessControlAction); - }); + grantUserPermissions([AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead]); renderSilences(); await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); diff --git a/public/app/features/alerting/unified/components/Authorize.tsx b/public/app/features/alerting/unified/components/Authorize.tsx index ee78e85db5f..1dead1a4e49 100644 --- a/public/app/features/alerting/unified/components/Authorize.tsx +++ b/public/app/features/alerting/unified/components/Authorize.tsx @@ -1,17 +1,73 @@ -import React from 'react'; +import { chain, filter } from 'lodash'; +import React, { PropsWithChildren } from 'react'; -import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { + Abilities, + Action, + AlertmanagerAction, + AlertSourceAction, + useAlertSourceAbilities, + useAllAlertmanagerAbilities, +} from '../hooks/useAbilities'; -type Props = { - actions: AccessControlAction[]; - fallback?: boolean; +interface AuthorizeProps extends PropsWithChildren { + actions: AlertmanagerAction[] | AlertSourceAction[]; +} + +export const Authorize = ({ actions, children }: AuthorizeProps) => { + const alertmanagerActions = filter(actions, isAlertmanagerAction) as AlertmanagerAction[]; + const alertSourceActions = filter(actions, isAlertSourceAction) as AlertSourceAction[]; + + if (alertmanagerActions.length) { + return {children}; + } + + if (alertSourceActions.length) { + return {children}; + } + + return null; }; -export const Authorize = ({ actions, children, fallback = true }: React.PropsWithChildren) => { - if (actions.some((action) => contextSrv.hasAccess(action, fallback))) { +interface ActionsProps extends PropsWithChildren { + actions: T[]; +} + +const AuthorizeAlertmanager = ({ actions, children }: ActionsProps) => { + const alertmanagerAbilties = useAllAlertmanagerAbilities(); + const allowed = actionsAllowed(alertmanagerAbilties, actions); + + if (allowed) { return <>{children}; } else { return null; } }; + +const AuthorizeAlertsource = ({ actions, children }: ActionsProps) => { + const alertSourceAbilities = useAlertSourceAbilities(); + const allowed = actionsAllowed(alertSourceAbilities, actions); + + if (allowed) { + return <>{children}; + } else { + return null; + } +}; + +// check if some action is allowed from the abilities +function actionsAllowed(abilities: Abilities, actions: T[]) { + return chain(abilities) + .pick(actions) + .values() + .value() + .some(([_supported, allowed]) => allowed === true); +} + +function isAlertmanagerAction(action: AlertmanagerAction) { + return Object.values(AlertmanagerAction).includes(action); +} + +function isAlertSourceAction(action: AlertSourceAction) { + return Object.values(AlertSourceAction).includes(action); +} diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx index ecef884ae84..22b8f9192e9 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx @@ -7,7 +7,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; -import { getInstancesPermissions } from '../../utils/access-control'; +import { AlertmanagerAction } from '../../hooks/useAbilities'; import { isGrafanaRulesSource } from '../../utils/datasource'; import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc'; import { AnnotationDetailsField } from '../AnnotationDetailsField'; @@ -20,7 +20,6 @@ interface AmNotificationsAlertDetailsProps { export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsAlertDetailsProps) => { const styles = useStyles2(getStyles); - const instancePermissions = getInstancesPermissions(alertManagerSourceName); // For Grafana Managed alerts the Generator URL redirects to the alert rule edit page, so update permission is required // For external alert manager the Generator URL redirects to an external service which we don't control @@ -32,8 +31,8 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA return ( <>
- - {alert.status.state === AlertState.Suppressed && ( + {alert.status.state === AlertState.Suppressed && ( + Manage silences - )} - {alert.status.state === AlertState.Active && ( + + )} + {alert.status.state === AlertState.Active && ( + Silence - )} - + + )} {isSeeSourceButtonEnabled && alert.generatorURL && ( See source diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx index 288f5ad92ac..c16a2be72d6 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx @@ -27,6 +27,7 @@ import * as receiversApi from '../../api/receiversApi'; import * as grafanaApp from '../../components/receivers/grafanaAppReceivers/grafanaApp'; import { mockApi, setupMswServer } from '../../mockApi'; import { + grantUserPermissions, mockDataSource, MockDataSourceSrv, onCallPluginMetaMock, @@ -86,14 +87,11 @@ const dataSources = { }; const renderReceivers = (alertManagerSourceName?: string) => { - locationService.push( - '/alerting/notifications' + - (alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '') - ); + locationService.push('/alerting/notifications'); return render( - + @@ -167,26 +165,15 @@ describe('Receivers', () => { mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false }); mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); setDataSourceSrv(new MockDataSourceSrv(dataSources)); - mocks.contextSrv.isEditor = true; + store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); - mocks.contextSrv.evaluatePermission.mockImplementation(() => []); - mocks.contextSrv.hasPermission.mockImplementation((action) => { - const permissions = [ - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalRead, - AccessControlAction.AlertingNotificationsExternalWrite, - ]; - return permissions.includes(action as AccessControlAction); - }); - - // respond with "true" when asked if we are an administrator - mocks.contextSrv.hasRole.mockImplementation((role: string) => { - return role === 'Admin'; - }); - - mocks.contextSrv.hasAccess.mockImplementation(() => true); + grantUserPermissions([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalRead, + AccessControlAction.AlertingNotificationsExternalWrite, + ]); }); it('Template and receiver tables are rendered, alertmanager can be selected, no notification errors', async () => { @@ -340,11 +327,10 @@ describe('Receivers', () => { mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); - mocks.contextSrv.hasPermission.mockImplementation((action) => - [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead].some( - (a) => a === action - ) - ); + grantUserPermissions([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsExternalRead, + ]); mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); renderReceivers(); await ui.receiversTable.find(); @@ -523,7 +509,6 @@ describe('Receivers', () => { expect(templatesTable).toBeInTheDocument(); expect(receiversTable).toBeInTheDocument(); - expect(ui.newContactPointButton.get()).toBeInTheDocument(); }); describe('Contact points health', () => { diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx index 8bd5358f6a4..8c8017e0a6e 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx @@ -4,14 +4,13 @@ import React, { useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { IconButton, LinkButton, Link, useStyles2, ConfirmModal } from '@grafana/ui'; -import { contextSrv } from 'app/core/services/context_srv'; import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch } from 'app/types/store'; import { Authorize } from '../../components/Authorize'; +import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig'; import { deleteMuteTimingAction } from '../../state/actions'; -import { getNotificationsPermissions } from '../../utils/access-control'; import { makeAMLink } from '../../utils/misc'; import { DynamicTable, DynamicTableItemProps, DynamicTableColumnProps } from '../DynamicTable'; import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA'; @@ -29,7 +28,6 @@ interface Props { export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hideActions }: Props) => { const styles = useStyles2(getStyles); const dispatch = useDispatch(); - const permissions = getNotificationsPermissions(alertManagerSourceName); const { currentData } = useAlertmanagerConfig(alertManagerSourceName, { refetchOnFocus: true, @@ -57,6 +55,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide }, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]); const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName); + const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateMuteTiming); return (
@@ -67,7 +66,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide {!hideActions && items.length > 0 && ( - + ) : ( @@ -111,11 +110,11 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide }; function useColumns(alertManagerSourceName: string, hideActions = false, setMuteTimingName: (name: string) => void) { - const permissions = getNotificationsPermissions(alertManagerSourceName); - - const userHasEditPermissions = contextSrv.hasPermission(permissions.update); - const userHasDeletePermissions = contextSrv.hasPermission(permissions.delete); - const showActions = !hideActions && (userHasEditPermissions || userHasDeletePermissions); + const [[_editSupported, allowedToEdit], [_deleteSupported, allowedToDelete]] = useAlertmanagerAbilities([ + AlertmanagerAction.UpdateMuteTiming, + AlertmanagerAction.DeleteMuteTiming, + ]); + const showActions = !hideActions && (allowedToEdit || allowedToDelete); return useMemo((): Array> => { const columns: Array> = [ @@ -159,7 +158,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute } return (
- + - + ({ diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx index 7d6d7979e7b..e6174e28c6e 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx @@ -15,6 +15,7 @@ import { import { ReceiversState } from 'app/types/alerting'; import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../../mocks'; +import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { Policy } from './Policy'; @@ -221,7 +222,11 @@ describe('Policy', () => { }); const renderPolicy = (element: JSX.Element) => - render({element}); + render( + + {element} + + ); const eq = MatcherOperator.equal; diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 50f1054e173..f20be14714d 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -7,18 +7,17 @@ import { Link } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { Badge, Button, Dropdown, getTagColorsFromName, Icon, Menu, Tooltip, useStyles2, Text } from '@grafana/ui'; -import { contextSrv } from 'app/core/core'; import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; import { RouteWithID, Receiver, ObjectMatcher, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types'; import { ReceiversState } from 'app/types'; +import { AlertmanagerAction, useAlertmanagerAbilities } from '../../hooks/useAbilities'; import { INTEGRATION_ICONS } from '../../types/contact-points'; -import { getNotificationsPermissions } from '../../utils/access-control'; -import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { normalizeMatchers } from '../../utils/matchers'; import { createContactPointLink, createMuteTimingLink } from '../../utils/misc'; import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies'; import { createUrl } from '../../utils/url'; +import { Authorize } from '../Authorize'; import { HoverCard } from '../HoverCard'; import { Label } from '../Label'; import { MetaText } from '../MetaText'; @@ -70,12 +69,15 @@ const Policy: FC = ({ const styles = useStyles2(getStyles); const isDefaultPolicy = currentRoute === routeTree; - const permissions = getNotificationsPermissions(alertManagerSourceName); - const canEditRoutes = contextSrv.hasPermission(permissions.update); - const canDeleteRoutes = contextSrv.hasPermission(permissions.delete); - const canReadProvisioning = - contextSrv.hasPermission(permissions.provisioning.read) || - contextSrv.hasPermission(permissions.provisioning.readSecrets); + const [ + [updatePoliciesSupported, updatePoliciesAllowed], + [deletePolicySupported, deletePolicyAllowed], + [exportPoliciesSupported, exportPoliciesAllowed], + ] = useAlertmanagerAbilities([ + AlertmanagerAction.UpdateNotificationPolicyTree, + AlertmanagerAction.DeleteNotificationPolicy, + AlertmanagerAction.ExportNotificationPolicies, + ]); const contactPoint = currentRoute.receiver; const continueMatching = currentRoute.continue ?? false; @@ -116,9 +118,6 @@ const Policy: FC = ({ const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0; const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0; - const isEditable = canEditRoutes; - const isDeletable = canDeleteRoutes && !isDefaultPolicy; - const matchingAlertGroups = matchingInstancesPreview?.groupsMap?.get(currentRoute.id); // sum all alert instances for all groups we're handling @@ -126,8 +125,59 @@ const Policy: FC = ({ ? sumBy(matchingAlertGroups, (group) => group.alerts.length) : undefined; - const isGrafanaAM = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME; - const showExport = isGrafanaAM && isDefaultPolicy && canReadProvisioning; + const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy; + const showEditAction = updatePoliciesSupported && updatePoliciesAllowed; + const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy; + + // build the menu actions for our policy + const dropdownMenuActions: JSX.Element[] = []; + + if (showEditAction) { + dropdownMenuActions.push( + + + onEditPolicy(currentRoute, isDefaultPolicy)} + /> + + + ); + } + + if (showExportAction) { + dropdownMenuActions.push( + + ); + } + + if (showDeleteAction) { + dropdownMenuActions.push( + + + + onDeletePolicy(currentRoute)} + /> + + + ); + } // TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated return ( @@ -155,9 +205,9 @@ const Policy: FC = ({ {/* TODO maybe we should move errors to the gutter instead? */} {errors.length > 0 && } {provisioned && } - {readOnly && !showExport ? null : ( + {!readOnly && ( - {!readOnly && ( + - )} + - - {!readOnly && ( - - onEditPolicy(currentRoute, isDefaultPolicy)} - /> - - )} - {showExport && ( - - )} - {!readOnly && isDeletable && ( - <> - - - onDeletePolicy(currentRoute)} - /> - - - )} - - } - > -