[v11.0.x] Alerting: Allow deleting contact points referenced only by auto-generated policies (#87115)
Alerting: Allow deleting contact points referenced only by auto-generated policies (#86800)
(cherry picked from commit b679a32fad)
This commit is contained in:
+34
-6
@@ -20,6 +20,7 @@ import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimi
|
|||||||
import setupVanillaAlertmanagerFlavoredServer, {
|
import setupVanillaAlertmanagerFlavoredServer, {
|
||||||
VANILLA_ALERTMANAGER_DATASOURCE_UID,
|
VANILLA_ALERTMANAGER_DATASOURCE_UID,
|
||||||
} from './__mocks__/vanillaAlertmanagerServer';
|
} from './__mocks__/vanillaAlertmanagerServer';
|
||||||
|
import { RouteReference } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them.
|
* There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them.
|
||||||
@@ -185,13 +186,19 @@ describe('contact points', () => {
|
|||||||
expect(deleteButton).toBeDisabled();
|
expect(deleteButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable delete when contact point is linked to at least one notification policy', async () => {
|
it('should disable delete when contact point is linked to at least one normal notification policy', async () => {
|
||||||
render(
|
const policies: RouteReference[] = [
|
||||||
<ContactPoint name={'my-contact-point'} provisioned={true} receivers={[]} policies={1} onDelete={noop} />,
|
|
||||||
{
|
{
|
||||||
wrapper,
|
receiver: 'my-contact-point',
|
||||||
}
|
route: {
|
||||||
);
|
type: 'normal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<ContactPoint name={'my-contact-point'} receivers={[]} policies={policies} onDelete={noop} />, {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -202,6 +209,27 @@ describe('contact points', () => {
|
|||||||
expect(deleteButton).toBeDisabled();
|
expect(deleteButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not disable delete when contact point is linked only to auto-generated notification policy', async () => {
|
||||||
|
const policies: RouteReference[] = [
|
||||||
|
{
|
||||||
|
receiver: 'my-contact-point',
|
||||||
|
route: {
|
||||||
|
type: 'auto-generated',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<ContactPoint name={'my-contact-point'} receivers={[]} policies={policies} onDelete={noop} />, {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||||
|
await userEvent.click(moreActions);
|
||||||
|
|
||||||
|
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||||
|
expect(deleteButton).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to search', async () => {
|
it('should be able to search', async () => {
|
||||||
render(
|
render(
|
||||||
<AlertmanagerProvider accessType={'notification'}>
|
<AlertmanagerProvider accessType={'notification'}>
|
||||||
|
|||||||
@@ -60,7 +60,13 @@ import {
|
|||||||
useContactPointsWithStatus,
|
useContactPointsWithStatus,
|
||||||
useDeleteContactPoint,
|
useDeleteContactPoint,
|
||||||
} from './useContactPoints';
|
} from './useContactPoints';
|
||||||
import { ContactPointWithMetadata, getReceiverDescription, isProvisioned, ReceiverConfigWithMetadata } from './utils';
|
import {
|
||||||
|
ContactPointWithMetadata,
|
||||||
|
getReceiverDescription,
|
||||||
|
isProvisioned,
|
||||||
|
ReceiverConfigWithMetadata,
|
||||||
|
RouteReference,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
enum ActiveTab {
|
enum ActiveTab {
|
||||||
ContactPoints,
|
ContactPoints,
|
||||||
@@ -226,7 +232,7 @@ const ContactPointsList = ({
|
|||||||
<>
|
<>
|
||||||
{pageItems.map((contactPoint, index) => {
|
{pageItems.map((contactPoint, index) => {
|
||||||
const provisioned = isProvisioned(contactPoint);
|
const provisioned = isProvisioned(contactPoint);
|
||||||
const policies = contactPoint.numberOfPolicies;
|
const policies = contactPoint.policies ?? [];
|
||||||
const key = `${contactPoint.name}-${index}`;
|
const key = `${contactPoint.name}-${index}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -287,7 +293,7 @@ interface ContactPointProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
provisioned?: boolean;
|
provisioned?: boolean;
|
||||||
receivers: ReceiverConfigWithMetadata[];
|
receivers: ReceiverConfigWithMetadata[];
|
||||||
policies?: number;
|
policies?: RouteReference[];
|
||||||
onDelete: (name: string) => void;
|
onDelete: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +302,7 @@ export const ContactPoint = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
provisioned = false,
|
provisioned = false,
|
||||||
receivers,
|
receivers,
|
||||||
policies = 0,
|
policies = [],
|
||||||
onDelete,
|
onDelete,
|
||||||
}: ContactPointProps) => {
|
}: ContactPointProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@@ -350,12 +356,12 @@ interface ContactPointHeaderProps {
|
|||||||
name: string;
|
name: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
provisioned?: boolean;
|
provisioned?: boolean;
|
||||||
policies?: number;
|
policies?: RouteReference[];
|
||||||
onDelete: (name: string) => void;
|
onDelete: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
||||||
const { name, disabled = false, provisioned = false, policies = 0, onDelete } = props;
|
const { name, disabled = false, provisioned = false, policies = [], onDelete } = props;
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
|
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
|
||||||
@@ -364,9 +370,12 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
|||||||
|
|
||||||
const [ExportDrawer, openExportDrawer] = useExportContactPoint();
|
const [ExportDrawer, openExportDrawer] = useExportContactPoint();
|
||||||
|
|
||||||
const isReferencedByPolicies = policies > 0;
|
const numberOfPolicies = policies.length;
|
||||||
|
const isReferencedByAnyPolicy = numberOfPolicies > 0;
|
||||||
|
const isReferencedByRegularPolicies = policies.some((ref) => ref.route.type !== 'auto-generated');
|
||||||
|
|
||||||
const canEdit = editSupported && editAllowed && !provisioned;
|
const canEdit = editSupported && editAllowed && !provisioned;
|
||||||
const canDelete = deleteSupported && deleteAllowed && !provisioned && policies === 0;
|
const canDelete = deleteSupported && deleteAllowed && !provisioned && !isReferencedByRegularPolicies;
|
||||||
|
|
||||||
const menuActions: JSX.Element[] = [];
|
const menuActions: JSX.Element[] = [];
|
||||||
|
|
||||||
@@ -390,7 +399,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
|||||||
menuActions.push(
|
menuActions.push(
|
||||||
<ConditionalWrap
|
<ConditionalWrap
|
||||||
key="delete-contact-point"
|
key="delete-contact-point"
|
||||||
shouldWrap={isReferencedByPolicies}
|
shouldWrap={!canDelete}
|
||||||
wrap={(children) => (
|
wrap={(children) => (
|
||||||
<Tooltip content="Contact point is currently in use by one or more notification policies" placement="top">
|
<Tooltip content="Contact point is currently in use by one or more notification policies" placement="top">
|
||||||
<span>{children}</span>
|
<span>{children}</span>
|
||||||
@@ -417,15 +426,15 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
|||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
{isReferencedByPolicies && (
|
{isReferencedByAnyPolicy && (
|
||||||
<MetaText>
|
<MetaText>
|
||||||
<Link to={createUrl('/alerting/routes', { contactPoint: name })}>
|
<Link to={createUrl('/alerting/routes', { contactPoint: name })}>
|
||||||
is used by <Strong>{policies}</Strong> {pluralize('notification policy', policies)}
|
is used by <Strong>{numberOfPolicies}</Strong> {pluralize('notification policy', numberOfPolicies)}
|
||||||
</Link>
|
</Link>
|
||||||
</MetaText>
|
</MetaText>
|
||||||
)}
|
)}
|
||||||
{provisioned && <ProvisioningBadge />}
|
{provisioned && <ProvisioningBadge />}
|
||||||
{!isReferencedByPolicies && <UnusedContactPointBadge />}
|
{!isReferencedByAnyPolicy && <UnusedContactPointBadge />}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<LinkButton
|
<LinkButton
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
|
|||||||
+18
-4
@@ -30,7 +30,14 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "grafana-default-email",
|
"name": "grafana-default-email",
|
||||||
"numberOfPolicies": 1,
|
"policies": [
|
||||||
|
{
|
||||||
|
"receiver": "grafana-default-email",
|
||||||
|
"route": {
|
||||||
|
"type": "normal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"grafana_managed_receiver_configs": [
|
"grafana_managed_receiver_configs": [
|
||||||
@@ -58,7 +65,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "lotsa-emails",
|
"name": "lotsa-emails",
|
||||||
"numberOfPolicies": 0,
|
"policies": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"grafana_managed_receiver_configs": [
|
"grafana_managed_receiver_configs": [
|
||||||
@@ -87,7 +94,14 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "provisioned-contact-point",
|
"name": "provisioned-contact-point",
|
||||||
"numberOfPolicies": 1,
|
"policies": [
|
||||||
|
{
|
||||||
|
"receiver": "provisioned-contact-point",
|
||||||
|
"route": {
|
||||||
|
"type": "normal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"grafana_managed_receiver_configs": [
|
"grafana_managed_receiver_configs": [
|
||||||
@@ -139,7 +153,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"name": "Slack with multiple channels",
|
"name": "Slack with multiple channels",
|
||||||
"numberOfPolicies": 0,
|
"policies": [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"error": undefined,
|
"error": undefined,
|
||||||
|
|||||||
+5
-1
@@ -2,6 +2,8 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { Button, Modal, ModalProps } from '@grafana/ui';
|
import { Button, Modal, ModalProps } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { stringifyErrorLike } from '../../../utils/misc';
|
||||||
|
|
||||||
type ModalHook<T = undefined> = [JSX.Element, (item: T) => void, () => void];
|
type ModalHook<T = undefined> = [JSX.Element, (item: T) => void, () => void];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +85,9 @@ const ErrorModal = ({ isOpen, onDismiss, error }: ErrorModalProps) => (
|
|||||||
>
|
>
|
||||||
<p>Failed to update your configuration:</p>
|
<p>Failed to update your configuration:</p>
|
||||||
<p>
|
<p>
|
||||||
<code>{String(error)}</code>
|
<pre>
|
||||||
|
<code>{stringifyErrorLike(error)}</code>
|
||||||
|
</pre>
|
||||||
</p>
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { countBy, difference, take, trim, upperFirst } from 'lodash';
|
import { difference, groupBy, take, trim, upperFirst } from 'lodash';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
@@ -99,7 +99,7 @@ export interface ReceiverConfigWithMetadata extends GrafanaManagedReceiverConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
|
export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
|
||||||
numberOfPolicies?: number; // now is optional as we don't have the data from the read-only endpoint
|
policies?: RouteReference[]; // now is optional as we don't have the data from the read-only endpoint
|
||||||
grafana_managed_receiver_configs: ReceiverConfigWithMetadata[];
|
grafana_managed_receiver_configs: ReceiverConfigWithMetadata[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ export function enhanceContactPointsWithMetadata(
|
|||||||
// compute the entire inherited tree before finding what notification policies are using a particular contact point
|
// compute the entire inherited tree before finding what notification policies are using a particular contact point
|
||||||
const fullyInheritedTree = computeInheritedTree(alertmanagerConfiguration?.alertmanager_config?.route ?? {});
|
const fullyInheritedTree = computeInheritedTree(alertmanagerConfiguration?.alertmanager_config?.route ?? {});
|
||||||
const usedContactPoints = getUsedContactPoints(fullyInheritedTree);
|
const usedContactPoints = getUsedContactPoints(fullyInheritedTree);
|
||||||
const usedContactPointsByName = countBy(usedContactPoints);
|
const usedContactPointsByName = groupBy(usedContactPoints, 'receiver');
|
||||||
|
|
||||||
const contactPointsList = alertmanagerConfiguration
|
const contactPointsList = alertmanagerConfiguration
|
||||||
? alertmanagerConfiguration?.alertmanager_config.receivers ?? []
|
? alertmanagerConfiguration?.alertmanager_config.receivers ?? []
|
||||||
@@ -133,8 +133,8 @@ export function enhanceContactPointsWithMetadata(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...contactPoint,
|
...contactPoint,
|
||||||
numberOfPolicies:
|
policies:
|
||||||
alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? 0),
|
alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? []),
|
||||||
grafana_managed_receiver_configs: receivers.map((receiver, index) => {
|
grafana_managed_receiver_configs: receivers.map((receiver, index) => {
|
||||||
const isOnCallReceiver = receiver.type === ReceiverTypes.OnCall;
|
const isOnCallReceiver = receiver.type === ReceiverTypes.OnCall;
|
||||||
return {
|
return {
|
||||||
@@ -166,10 +166,26 @@ export function isAutoGeneratedPolicy(route: Route) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUsedContactPoints(route: Route): string[] {
|
export interface RouteReference {
|
||||||
|
receiver: string;
|
||||||
|
route: {
|
||||||
|
type: 'auto-generated' | 'normal';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsedContactPoints(route: Route): RouteReference[] {
|
||||||
const childrenContactPoints = route.routes?.flatMap((route) => getUsedContactPoints(route)) ?? [];
|
const childrenContactPoints = route.routes?.flatMap((route) => getUsedContactPoints(route)) ?? [];
|
||||||
|
|
||||||
if (route.receiver) {
|
if (route.receiver) {
|
||||||
return [route.receiver, ...childrenContactPoints];
|
return [
|
||||||
|
{
|
||||||
|
receiver: route.receiver,
|
||||||
|
route: {
|
||||||
|
type: isAutoGeneratedPolicy(route) ? 'auto-generated' : 'normal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...childrenContactPoints,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return childrenContactPoints;
|
return childrenContactPoints;
|
||||||
|
|||||||
+2
-1
@@ -259,6 +259,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
expect(mocks.api.setRulerRuleGroup).not.toHaveBeenCalled();
|
expect(mocks.api.setRulerRuleGroup).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can create new grafana managed alert when using simplified routing and selecting a contact point', async () => {
|
it('can create new grafana managed alert when using simplified routing and selecting a contact point', async () => {
|
||||||
const contactPointsAvailable: ContactPointWithMetadata[] = [
|
const contactPointsAvailable: ContactPointWithMetadata[] = [
|
||||||
{
|
{
|
||||||
@@ -275,7 +276,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
settings: {},
|
settings: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
numberOfPolicies: 0,
|
policies: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
mocks.useContactPointsWithStatus.mockReturnValue({
|
mocks.useContactPointsWithStatus.mockReturnValue({
|
||||||
|
|||||||
Reference in New Issue
Block a user