mirror of
https://github.com/grafana/grafana.git
synced 2025-01-11 08:32:10 -06:00
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:
parent
9233ad6462
commit
7a414a04a0
@ -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"})
|
||||
}
|
||||
|
@ -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(
|
||||
() =>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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,
|
||||
}),
|
||||
});
|
||||
|
@ -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 && '⋅'}
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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];
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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))];
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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ş",
|
||||
|
Loading…
Reference in New Issue
Block a user