diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index 325867253b8..2b6ddc72922 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -20,6 +20,7 @@ import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimi import setupVanillaAlertmanagerFlavoredServer, { VANILLA_ALERTMANAGER_DATASOURCE_UID, } 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. @@ -185,13 +186,19 @@ describe('contact points', () => { expect(deleteButton).toBeDisabled(); }); - it('should disable delete when contact point is linked to at least one notification policy', async () => { - render( - , + it('should disable delete when contact point is linked to at least one normal notification policy', async () => { + const policies: RouteReference[] = [ { - wrapper, - } - ); + receiver: 'my-contact-point', + route: { + type: 'normal', + }, + }, + ]; + + render(, { + wrapper, + }); expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument(); @@ -202,6 +209,27 @@ describe('contact points', () => { 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(, { + 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 () => { render( diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index 646fa4e8ef2..41fc2e4c76c 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -60,7 +60,13 @@ import { useContactPointsWithStatus, useDeleteContactPoint, } from './useContactPoints'; -import { ContactPointWithMetadata, getReceiverDescription, isProvisioned, ReceiverConfigWithMetadata } from './utils'; +import { + ContactPointWithMetadata, + getReceiverDescription, + isProvisioned, + ReceiverConfigWithMetadata, + RouteReference, +} from './utils'; enum ActiveTab { ContactPoints, @@ -226,7 +232,7 @@ const ContactPointsList = ({ <> {pageItems.map((contactPoint, index) => { const provisioned = isProvisioned(contactPoint); - const policies = contactPoint.numberOfPolicies; + const policies = contactPoint.policies ?? []; const key = `${contactPoint.name}-${index}`; return ( @@ -287,7 +293,7 @@ interface ContactPointProps { disabled?: boolean; provisioned?: boolean; receivers: ReceiverConfigWithMetadata[]; - policies?: number; + policies?: RouteReference[]; onDelete: (name: string) => void; } @@ -296,7 +302,7 @@ export const ContactPoint = ({ disabled = false, provisioned = false, receivers, - policies = 0, + policies = [], onDelete, }: ContactPointProps) => { const styles = useStyles2(getStyles); @@ -350,12 +356,12 @@ interface ContactPointHeaderProps { name: string; disabled?: boolean; provisioned?: boolean; - policies?: number; + policies?: RouteReference[]; onDelete: (name: string) => void; } 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 [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint); @@ -364,9 +370,12 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { 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 canDelete = deleteSupported && deleteAllowed && !provisioned && policies === 0; + const canDelete = deleteSupported && deleteAllowed && !provisioned && !isReferencedByRegularPolicies; const menuActions: JSX.Element[] = []; @@ -390,7 +399,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { menuActions.push( ( {children} @@ -417,15 +426,15 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { {name} - {isReferencedByPolicies && ( + {isReferencedByAnyPolicy && ( - is used by {policies} {pluralize('notification policy', policies)} + is used by {numberOfPolicies} {pluralize('notification policy', numberOfPolicies)} )} {provisioned && } - {!isReferencedByPolicies && } + {!isReferencedByAnyPolicy && } = [JSX.Element, (item: T) => void, () => void]; /** @@ -83,7 +85,9 @@ const ErrorModal = ({ isOpen, onDismiss, error }: ErrorModalProps) => ( >

Failed to update your configuration:

- {String(error)} +

+        {stringifyErrorLike(error)}
+      

); diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index a07e3cc03dc..0895a2e5646 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -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 { config } from '@grafana/runtime'; @@ -99,7 +99,7 @@ export interface ReceiverConfigWithMetadata extends GrafanaManagedReceiverConfig } 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[]; } @@ -121,7 +121,7 @@ export function enhanceContactPointsWithMetadata( // compute the entire inherited tree before finding what notification policies are using a particular contact point const fullyInheritedTree = computeInheritedTree(alertmanagerConfiguration?.alertmanager_config?.route ?? {}); const usedContactPoints = getUsedContactPoints(fullyInheritedTree); - const usedContactPointsByName = countBy(usedContactPoints); + const usedContactPointsByName = groupBy(usedContactPoints, 'receiver'); const contactPointsList = alertmanagerConfiguration ? alertmanagerConfiguration?.alertmanager_config.receivers ?? [] @@ -133,8 +133,8 @@ export function enhanceContactPointsWithMetadata( return { ...contactPoint, - numberOfPolicies: - alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? 0), + policies: + alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? []), grafana_managed_receiver_configs: receivers.map((receiver, index) => { const isOnCallReceiver = receiver.type === ReceiverTypes.OnCall; 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)) ?? []; + if (route.receiver) { - return [route.receiver, ...childrenContactPoints]; + return [ + { + receiver: route.receiver, + route: { + type: isAutoGeneratedPolicy(route) ? 'auto-generated' : 'normal', + }, + }, + ...childrenContactPoints, + ]; } return childrenContactPoints; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index fc6db8b26bd..2472e9ae7be 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -259,6 +259,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', () expect(mocks.api.setRulerRuleGroup).not.toHaveBeenCalled(); }); }); + it('can create new grafana managed alert when using simplified routing and selecting a contact point', async () => { const contactPointsAvailable: ContactPointWithMetadata[] = [ { @@ -275,7 +276,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', () settings: {}, }, ], - numberOfPolicies: 0, + policies: [], }, ]; mocks.useContactPointsWithStatus.mockReturnValue({