Alerting: Fix permissions for timeintervals UI and improve display of contact points in notification policies (#96129)

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Tom Ratcliffe 2024-11-14 09:55:15 +00:00 committed by GitHub
parent 9233ad6462
commit 7a414a04a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 172 additions and 166 deletions

View File

@ -452,6 +452,8 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
if hasAccess(ac.EvalAny(
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsWrite),
)) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Notification policies", SubTitle: "Determine how alerts are routed to contact points", Id: "am-routes", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
}

View File

@ -4,6 +4,10 @@ import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/type
import { AccessControlAction } from 'app/types';
import { PERMISSIONS_CONTACT_POINTS } from './unified/components/contact-points/permissions';
import {
PERMISSIONS_TIME_INTERVALS_MODIFY,
PERMISSIONS_TIME_INTERVALS_READ,
} from './unified/components/mute-timings/permissions';
import { PERMISSIONS_TEMPLATES } from './unified/components/templates/permissions';
import { evaluateAccess } from './unified/utils/access-control';
@ -33,6 +37,8 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
roles: evaluateAccess([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
...PERMISSIONS_TIME_INTERVALS_READ,
...PERMISSIONS_TIME_INTERVALS_MODIFY,
]),
component: importAlertingComponent(
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/NotificationPolicies')
@ -43,6 +49,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
roles: evaluateAccess([
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalWrite,
...PERMISSIONS_TIME_INTERVALS_MODIFY,
]),
component: importAlertingComponent(
() =>
@ -56,6 +63,8 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
roles: evaluateAccess([
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalWrite,
...PERMISSIONS_TIME_INTERVALS_READ,
...PERMISSIONS_TIME_INTERVALS_MODIFY,
]),
component: importAlertingComponent(
() =>

View File

@ -242,54 +242,51 @@ const AmRoutes = () => {
</TabsBar>
<TabContent className={styles.tabContent}>
{isFetching && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{haveError && (
<Alert severity="error" title="Error loading Alertmanager config">
{resultError.message || 'Unknown error.'}
</Alert>
)}
{haveData && (
{policyTreeTabActive && (
<>
{policyTreeTabActive && (
<>
<Stack direction="column" gap={1}>
{rootRoute && (
<NotificationPoliciesFilter
onChangeMatchers={setLabelMatchersFilter}
onChangeReceiver={setContactPointFilter}
matchingCount={routesMatchingFilters.matchedRoutesWithPath.size}
/>
)}
{rootRoute && (
<Policy
receivers={receivers}
routeTree={rootRoute}
currentRoute={rootRoute}
alertGroups={alertGroups ?? []}
contactPointsState={contactPointsState.receivers}
readOnly={!hasConfigurationAPI}
provisioned={isProvisioned}
alertManagerSourceName={selectedAlertmanager}
onAddPolicy={openAddModal}
onEditPolicy={openEditModal}
onDeletePolicy={openDeleteModal}
onShowAlertInstances={showAlertGroupsModal}
routesMatchingFilters={routesMatchingFilters}
matchingInstancesPreview={{ groupsMap: routeAlertGroupsMap, enabled: !instancesPreviewError }}
isAutoGenerated={false}
/>
)}
</Stack>
{addModal}
{editModal}
{deleteModal}
{alertInstancesModal}
</>
{haveError && (
<Alert severity="error" title="Error loading Alertmanager config">
{resultError.message || 'Unknown error.'}
</Alert>
)}
{muteTimingsTabActive && (
<MuteTimingsTable alertManagerSourceName={selectedAlertmanager} hideActions={!hasConfigurationAPI} />
{haveData && (
<Stack direction="column" gap={1}>
{rootRoute && (
<NotificationPoliciesFilter
onChangeMatchers={setLabelMatchersFilter}
onChangeReceiver={setContactPointFilter}
matchingCount={routesMatchingFilters.matchedRoutesWithPath.size}
/>
)}
{rootRoute && (
<Policy
receivers={receivers}
currentRoute={rootRoute}
contactPointsState={contactPointsState.receivers}
readOnly={!hasConfigurationAPI}
provisioned={isProvisioned}
alertManagerSourceName={selectedAlertmanager}
onAddPolicy={openAddModal}
onEditPolicy={openEditModal}
onDeletePolicy={openDeleteModal}
onShowAlertInstances={showAlertGroupsModal}
routesMatchingFilters={routesMatchingFilters}
matchingInstancesPreview={{ groupsMap: routeAlertGroupsMap, enabled: !instancesPreviewError }}
isAutoGenerated={false}
isDefaultPolicy
/>
)}
</Stack>
)}
{addModal}
{editModal}
{deleteModal}
{alertInstancesModal}
</>
)}
{muteTimingsTabActive && (
<MuteTimingsTable alertManagerSourceName={selectedAlertmanager} hideActions={!hasConfigurationAPI} />
)}
</TabContent>
</>
);

View File

@ -3,9 +3,9 @@ import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Stack, TextLink } from '@grafana/ui';
import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
import { createContactPointLink } from '../../utils/misc';
import { createContactPointSearchLink } from '../../utils/misc';
import { AlertLabels } from '../AlertLabels';
import { CollapseToggle } from '../CollapseToggle';
import { MetaText } from '../MetaText';
@ -44,7 +44,7 @@ export const AlertGroup = ({ alertManagerSourceName, group }: Props) => {
<MetaText icon="at">
Delivered to{' '}
<TextLink
href={createContactPointLink(contactPoint, alertManagerSourceName)}
href={createContactPointSearchLink(contactPoint, alertManagerSourceName)}
variant="bodySmall"
color="primary"
inline={false}
@ -77,7 +77,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'space-between',
padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`,
padding: theme.spacing(1),
backgroundColor: theme.colors.background.secondary,
width: '100%',
}),
@ -86,14 +86,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
flexDirection: 'row',
alignItems: 'center',
}),
summary: css({}),
[AlertState.Active]: css({
color: theme.colors.error.main,
}),
[AlertState.Suppressed]: css({
color: theme.colors.primary.main,
}),
[AlertState.Unprocessed]: css({
color: theme.colors.secondary.main,
}),
});

View File

@ -47,13 +47,7 @@ export const ContactPoint = ({ contactPoint }: ContactPointProps) => {
})
}
/>
{receivers.length === 0 && (
<div className={styles.noIntegrationsContainer}>
<MetaText color="warning" icon="exclamation-circle">
<Trans i18nKey="alerting.contact-points.no-integrations">No integrations configured</Trans>
</MetaText>
</div>
)}
{showFullMetadata ? (
<div>
{receivers.map((receiver, index) => {
@ -176,6 +170,11 @@ export const ContactPointReceiverSummary = ({ receivers, limit }: ContactPointRe
return (
<Stack direction="column" gap={0}>
<Stack direction="row" alignItems="center" gap={1}>
{integrationsShown.length === 0 && (
<MetaText color="warning" icon="exclamation-triangle">
<Trans i18nKey="alerting.contact-points.no-integrations">No integrations configured</Trans>
</MetaText>
)}
{integrationsShown.map(([type, receivers], index) => {
const iconName = INTEGRATION_ICONS[type];
const receiverName = receiverTypeNames[type] ?? upperFirst(type);
@ -198,7 +197,7 @@ export const ContactPointReceiverSummary = ({ receivers, limit }: ContactPointRe
{iconName && <Icon name={iconName} />}
<span>
{receiverName}
{receivers.length > 1 && receivers.length}
{receivers.length > 1 && ` (${receivers.length})`}
</span>
</Stack>
{!isLastItem && '⋅'}

View File

@ -243,6 +243,11 @@ const ContactPointsList = ({ contactPoints, search, pageSize = DEFAULT_PAGE_SIZE
const searchResults = useContactPointsSearch(contactPoints, search);
const { page, pageItems, numberOfPages, onPageChange } = usePagination(searchResults, 1, pageSize);
if (pageItems.length === 0) {
const emptyMessage = t('alerting.contact-points.no-contact-points-found', 'No contact points found');
return <EmptyState variant="not-found" message={emptyMessage} />;
}
return (
<>
{pageItems.map((contactPoint, index) => {

View File

@ -5,7 +5,7 @@ import { AccessControlAction } from 'app/types';
*
* Any permission in this list will be checked for client side access to view Contact Points functionality.
*/
const PERMISSIONS_CONTACT_POINTS_READ = [AccessControlAction.AlertingReceiversRead];
export const PERMISSIONS_CONTACT_POINTS_READ = [AccessControlAction.AlertingReceiversRead];
/**
* List of granular permissions that allow modifying contact points

View File

@ -129,7 +129,7 @@ export function enhanceContactPointsWithMetadata({
const receivers = extractReceivers(contactPoint);
const statusForReceiver = status.find((status) => status.name === contactPoint.name);
const id = 'id' in contactPoint && contactPoint.id ? contactPoint.id : contactPoint.name;
const id = getContactPointIdentifier(contactPoint);
return {
...contactPoint,
@ -157,6 +157,10 @@ export function enhanceContactPointsWithMetadata({
return enhanced.sort((a, b) => a.name.localeCompare(b.name));
}
function getContactPointIdentifier(contactPoint: Receiver): string {
return 'id' in contactPoint && contactPoint.id ? contactPoint.id : contactPoint.name;
}
export function isAutoGeneratedPolicy(route: Route) {
const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;
if (!simplifiedRoutingToggleEnabled) {

View File

@ -0,0 +1,11 @@
import { AccessControlAction } from 'app/types';
/**
* List of granular permissions that allow viewing contact points
*/
export const PERMISSIONS_TIME_INTERVALS_READ = [AccessControlAction.AlertingTimeIntervalsRead];
/**
* List of granular permissions that allow modifying time intervals
*/
export const PERMISSIONS_TIME_INTERVALS_MODIFY = [AccessControlAction.AlertingTimeIntervalsWrite];

View File

@ -15,7 +15,7 @@ import {
import { ReceiversState } from 'app/types/alerting';
import { useAlertmanagerAbilities } from '../../hooks/useAbilities';
import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../../mocks';
import { mockReceiversState } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
@ -57,7 +57,8 @@ describe('Policy', () => {
renderPolicy(
<Policy
routeTree={routeTree}
receivers={[{ name: 'grafana-default-email' }]}
isDefaultPolicy
currentRoute={routeTree}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
onEditPolicy={onEditPolicy}
@ -165,7 +166,7 @@ describe('Policy', () => {
renderPolicy(
<Policy
routeTree={routeTree}
isDefaultPolicy
currentRoute={routeTree}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
onEditPolicy={onEditPolicy}
@ -203,7 +204,7 @@ describe('Policy', () => {
renderPolicy(
<Policy
routeTree={routeTree}
isDefaultPolicy
currentRoute={routeTree}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
onEditPolicy={onEditPolicy}
@ -240,7 +241,7 @@ describe('Policy', () => {
renderPolicy(
<Policy
routeTree={routeTree}
isDefaultPolicy
currentRoute={routeTree}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
onEditPolicy={onEditPolicy}
@ -263,7 +264,6 @@ describe('Policy', () => {
renderPolicy(
<Policy
readOnly
routeTree={routeTree}
currentRoute={routeTree}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
onEditPolicy={noop}
@ -282,22 +282,10 @@ describe('Policy', () => {
routes: [{ id: '1', object_matchers: [['foo', eq, 'bar']] }],
};
const matchingGroups: AlertmanagerGroup[] = [
mockAlertGroup({
labels: {},
alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } }), mockAlertmanagerAlert({ labels: { foo: 'bar' } })],
}),
mockAlertGroup({
labels: {},
alerts: [mockAlertmanagerAlert({ labels: { bar: 'baz' } })],
}),
];
renderPolicy(
<Policy
readOnly
alertGroups={matchingGroups}
routeTree={routeTree}
isDefaultPolicy
currentRoute={routeTree}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
onEditPolicy={noop}
@ -325,7 +313,7 @@ describe('Policy', () => {
renderPolicy(
<Policy
readOnly
routeTree={routeTree}
isDefaultPolicy
currentRoute={routeTree}
contactPointsState={receiversState}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}

View File

@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import { defaults, groupBy, isArray, sumBy, uniqueId, upperFirst } from 'lodash';
import { defaults, isArray, sumBy, uniqueId } from 'lodash';
import pluralize from 'pluralize';
import { FC, Fragment, ReactNode, useState } from 'react';
import * as React from 'react';
import { FC, Fragment, ReactNode, useState } from 'react';
import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
@ -21,10 +21,11 @@ import {
getTagColorsFromName,
useStyles2,
} from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { Trans, t } from 'app/core/internationalization';
import ConditionalWrap from 'app/features/alerting/unified/components/ConditionalWrap';
import MoreButton from 'app/features/alerting/unified/components/MoreButton';
import { PrimaryText } from 'app/features/alerting/unified/components/common/TextVariants';
import { ContactPointReceiverSummary } from 'app/features/alerting/unified/components/contact-points/ContactPoint';
import {
AlertmanagerGroup,
MatcherOperator,
@ -36,10 +37,9 @@ import { ReceiversState } from 'app/types';
import { RoutesMatchingFilters } from '../../NotificationPolicies';
import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { INTEGRATION_ICONS } from '../../types/contact-points';
import { getAmMatcherFormatter } from '../../utils/alertmanager';
import { MatcherFormatter, normalizeMatchers } from '../../utils/matchers';
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
import { createContactPointLink, createContactPointSearchLink, createMuteTimingLink } from '../../utils/misc';
import { InheritableProperties, getInheritedProperties } from '../../utils/notification-policies';
import { InsertPosition } from '../../utils/routeTree';
import { Authorize } from '../Authorize';
@ -55,7 +55,6 @@ import { TIMING_OPTIONS_DEFAULTS, TimingOptions } from './timingOptions';
interface PolicyComponentProps {
receivers?: Receiver[];
alertGroups?: AlertmanagerGroup[];
contactPointsState?: ReceiversState;
readOnly?: boolean;
provisioned?: boolean;
@ -67,7 +66,6 @@ interface PolicyComponentProps {
enabled: boolean;
};
routeTree: RouteWithID;
currentRoute: RouteWithID;
alertManagerSourceName: string;
onEditPolicy: (route: RouteWithID, isDefault?: boolean, isAutogenerated?: boolean) => void;
@ -79,6 +77,7 @@ interface PolicyComponentProps {
formatter?: MatcherFormatter
) => void;
isAutoGenerated?: boolean;
isDefaultPolicy?: boolean;
}
const Policy = (props: PolicyComponentProps) => {
@ -87,10 +86,8 @@ const Policy = (props: PolicyComponentProps) => {
contactPointsState,
readOnly = false,
provisioned = false,
alertGroups = [],
alertManagerSourceName,
currentRoute,
routeTree,
inheritedProperties,
routesMatchingFilters = {
filtersApplied: false,
@ -102,12 +99,11 @@ const Policy = (props: PolicyComponentProps) => {
onDeletePolicy,
onShowAlertInstances,
isAutoGenerated = false,
isDefaultPolicy = false,
} = props;
const styles = useStyles2(getStyles);
const isDefaultPolicy = currentRoute === routeTree;
const contactPoint = currentRoute.receiver;
const continueMatching = currentRoute.continue ?? false;
@ -353,7 +349,6 @@ const Policy = (props: PolicyComponentProps) => {
return (
<Policy
key={child.id}
routeTree={routeTree}
currentRoute={child}
receivers={receivers}
contactPointsState={contactPointsState}
@ -364,7 +359,6 @@ const Policy = (props: PolicyComponentProps) => {
onDeletePolicy={onDeletePolicy}
onShowAlertInstances={onShowAlertInstances}
alertManagerSourceName={alertManagerSourceName}
alertGroups={alertGroups}
routesMatchingFilters={routesMatchingFilters}
matchingInstancesPreview={matchingInstancesPreview}
isAutoGenerated={isThisChildAutoGenerated}
@ -844,7 +838,6 @@ interface ContactPointDetailsProps {
receivers: Receiver[];
}
// @TODO make this work for cloud AMs too
const ContactPointsHoverDetails: FC<ContactPointDetailsProps> = ({
alertManagerSourceName,
contactPoint,
@ -852,67 +845,40 @@ const ContactPointsHoverDetails: FC<ContactPointDetailsProps> = ({
}) => {
const details = receivers.find((receiver) => receiver.name === contactPoint);
if (!details) {
// If we can't find details, then it's possible (likely) that the user doesn't have access to this
// contact point, so we don't try and link to it
return (
<TextLink
href={createContactPointLink(contactPoint, alertManagerSourceName)}
color="primary"
variant="bodySmall"
inline={false}
>
<Text color="secondary" variant="bodySmall">
{contactPoint}
</TextLink>
</Text>
);
}
const integrations = details.grafana_managed_receiver_configs;
if (!integrations) {
return (
<TextLink
href={createContactPointLink(contactPoint, alertManagerSourceName)}
color="primary"
variant="bodySmall"
inline={false}
>
{contactPoint}
</TextLink>
);
}
const groupedIntegrations = groupBy(details.grafana_managed_receiver_configs, (config) => config.type);
const contactPointLink =
'id' in details && details.id
? createContactPointLink(details.id, alertManagerSourceName)
: createContactPointSearchLink(details.name, alertManagerSourceName);
return (
<PopupCard
disabled={!integrations}
arrow
placement="top"
header={
<MetaText icon="at">
<div>
<Trans i18nKey="alerting.contact-point">Contact Point</Trans>
</div>
<Text color="primary">{contactPoint}</Text>
</MetaText>
}
key={uniqueId()}
content={
<Stack direction="row" gap={0.5}>
{/* use "label" to indicate how many of that type we have in the contact point */}
{Object.entries(groupedIntegrations).map(([type, integrations]) => (
<Label
key={uniqueId()}
label={integrations.length > 1 ? integrations.length : undefined}
icon={INTEGRATION_ICONS[type]}
value={upperFirst(type)}
/>
))}
</Stack>
<Text variant="bodySmall" color="secondary">
<ContactPointReceiverSummary receivers={details.grafana_managed_receiver_configs || []} limit={3} />
</Text>
}
>
<TextLink
href={createContactPointLink(contactPoint, alertManagerSourceName)}
color="primary"
variant="bodySmall"
inline={false}
>
<TextLink href={contactPointLink} color="primary" variant="bodySmall" inline={false}>
{contactPoint}
</TextLink>
</PopupCard>

View File

@ -9,7 +9,7 @@ import { AlertmanagerAction } from '../../../hooks/useAbilities';
import { AlertmanagerProvider } from '../../../state/AlertmanagerContext';
import { getAmMatcherFormatter } from '../../../utils/alertmanager';
import { MatcherFormatter } from '../../../utils/matchers';
import { makeAMLink } from '../../../utils/misc';
import { createContactPointSearchLink } from '../../../utils/misc';
import { Authorize } from '../../Authorize';
import { Matchers } from '../../notification-policies/Matchers';
@ -98,10 +98,7 @@ export function NotificationRouteDetailsModal({
<Authorize actions={[AlertmanagerAction.UpdateContactPoint]}>
<Stack gap={1} direction="row" alignItems="center">
<a
href={makeAMLink(
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
alertManagerSourceName
)}
href={createContactPointSearchLink(receiver.name, alertManagerSourceName)}
className={styles.link}
target="_blank"
rel="noreferrer"

View File

@ -1,6 +1,11 @@
import { useMemo } from 'react';
import { contextSrv as ctx } from 'app/core/services/context_srv';
import { PERMISSIONS_CONTACT_POINTS_READ } from 'app/features/alerting/unified/components/contact-points/permissions';
import {
PERMISSIONS_TIME_INTERVALS_MODIFY,
PERMISSIONS_TIME_INTERVALS_READ,
} from 'app/features/alerting/unified/components/mute-timings/permissions';
import { useFolder } from 'app/features/alerting/unified/hooks/useFolder';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
@ -229,22 +234,22 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
hasConfigurationAPI,
notificationsPermissions.create,
// TODO: Move this into the permissions config and generalise that code to allow for an array of permissions
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversCreate : null
...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversCreate] : [])
),
[AlertmanagerAction.ViewContactPoint]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversRead : null
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_CONTACT_POINTS_READ : [])
),
[AlertmanagerAction.UpdateContactPoint]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : [])
),
[AlertmanagerAction.DeleteContactPoint]: toAbility(
hasConfigurationAPI,
notificationsPermissions.delete,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : [])
),
// At the time of writing, only Grafana flavored alertmanager supports exporting,
// and if a user can view the contact point, then they can also export it
@ -254,17 +259,17 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
[AlertmanagerAction.CreateNotificationTemplate]: toAbility(
hasConfigurationAPI,
notificationsPermissions.create,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : [])
),
[AlertmanagerAction.ViewNotificationTemplate]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesRead : null
...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesRead] : [])
),
[AlertmanagerAction.UpdateNotificationTemplate]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : [])
),
[AlertmanagerAction.DeleteNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
// -- notification policies --
@ -287,11 +292,27 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
[AlertmanagerAction.ViewSilence]: toAbility(AlwaysSupported, instancePermissions.read),
[AlertmanagerAction.UpdateSilence]: toAbility(AlwaysSupported, instancePermissions.update),
[AlertmanagerAction.PreviewSilencedInstances]: toAbility(AlwaysSupported, instancePermissions.read),
// -- mute timtings --
[AlertmanagerAction.CreateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
[AlertmanagerAction.ViewMuteTiming]: toAbility(AlwaysSupported, notificationsPermissions.read),
[AlertmanagerAction.UpdateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
[AlertmanagerAction.DeleteMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
// -- mute timings --
[AlertmanagerAction.CreateMuteTiming]: toAbility(
hasConfigurationAPI,
notificationsPermissions.create,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
),
[AlertmanagerAction.ViewMuteTiming]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_READ : [])
),
[AlertmanagerAction.UpdateMuteTiming]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
),
[AlertmanagerAction.DeleteMuteTiming]: toAbility(
hasConfigurationAPI,
notificationsPermissions.delete,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
),
[AlertmanagerAction.ExportMuteTimings]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read),
};
@ -355,7 +376,8 @@ function useCanSilence(rule: CombinedRule): [boolean, boolean] {
}
// just a convenient function
const toAbility = (supported: boolean, ...actions: Array<AccessControlAction | null>): Ability => [
supported,
actions.some((action) => action && ctx.hasPermission(action)),
];
const toAbility = (
supported: boolean,
/** If user has any of these permissions, then they are allowed to perform the action */
...actions: AccessControlAction[]
): Ability => [supported, actions.some((action) => action && ctx.hasPermission(action))];

View File

@ -15,7 +15,7 @@ import { ProvisioningBadge } from '../../components/Provisioning';
import { PluginOriginBadge } from '../../plugins/PluginOriginBadge';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { labelsSize } from '../../utils/labels';
import { createContactPointLink } from '../../utils/misc';
import { createContactPointSearchLink } from '../../utils/misc';
import { RulePluginOrigin } from '../../utils/rules';
import { ListItem } from './ListItem';
@ -106,7 +106,7 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => {
<MetaText icon="at">
<Trans i18nKey="alerting.contact-points.delivered-to">Delivered to</Trans>{' '}
<TextLink
href={createContactPointLink(contactPoint, GRAFANA_RULES_SOURCE_NAME)}
href={createContactPointSearchLink(contactPoint, GRAFANA_RULES_SOURCE_NAME)}
variant="bodySmall"
color="primary"
inline={false}

View File

@ -48,6 +48,18 @@ export function createContactPointLink(contactPoint: string, alertManagerSourceN
});
}
/**
* @deprecated Avoid using this - we should instead endeavour to use IDs to link directly to contact points.
* This isn't always possible, so only use this if we only have access to a contact point's name
*/
export function createContactPointSearchLink(contactPoint: string, alertManagerSourceName = ''): string {
return createRelativeUrl(`/alerting/notifications`, {
search: contactPoint,
tab: 'contact_points',
alertmanager: alertManagerSourceName,
});
}
export function createMuteTimingLink(muteTimingName: string, alertManagerSourceName = ''): string {
return createRelativeUrl('/alerting/routes/mute-timing/edit', {
muteName: muteTimingName,

View File

@ -134,6 +134,10 @@ export enum AccessControlAction {
AlertingReceiversWrite = 'alert.notifications.receivers:write',
AlertingReceiversRead = 'alert.notifications.receivers:read',
// Alerting time intervals actions
AlertingTimeIntervalsRead = 'alert.notifications.time-intervals:read',
AlertingTimeIntervalsWrite = 'alert.notifications.time-intervals:write',
// Alerting templates actions
AlertingTemplatesRead = 'alert.notifications.templates:read',
AlertingTemplatesWrite = 'alert.notifications.templates:write',

View File

@ -137,7 +137,6 @@
"export-all": "Export all",
"view": "View"
},
"contact-point": "Contact Point",
"contact-points": {
"create": "Create contact point",
"custom-template-value": "Custom template value",
@ -155,6 +154,7 @@
},
"last-delivery-attempt": "Last delivery attempt",
"last-delivery-failed": "Last delivery attempt failed",
"no-contact-points-found": "No contact points found",
"no-delivery-attempts": "No delivery attempts",
"no-integrations": "No integrations configured",
"only-firing": "Delivering <1>only firing</1> notifications",

View File

@ -137,7 +137,6 @@
"export-all": "Ēχpőřŧ äľľ",
"view": "Vįęŵ"
},
"contact-point": "Cőʼnŧäčŧ Pőįʼnŧ",
"contact-points": {
"create": "Cřęäŧę čőʼnŧäčŧ pőįʼnŧ",
"custom-template-value": "Cūşŧőm ŧęmpľäŧę väľūę",
@ -155,6 +154,7 @@
},
"last-delivery-attempt": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ",
"last-delivery-failed": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ ƒäįľęđ",
"no-contact-points-found": "Ńő čőʼnŧäčŧ pőįʼnŧş ƒőūʼnđ",
"no-delivery-attempts": "Ńő đęľįvęřy äŧŧęmpŧş",
"no-integrations": "Ńő įʼnŧęģřäŧįőʼnş čőʼnƒįģūřęđ",
"only-firing": "Đęľįvęřįʼnģ <1>őʼnľy ƒįřįʼnģ</1> ʼnőŧįƒįčäŧįőʼnş",