diff --git a/.betterer.results b/.betterer.results index 0ac51d62848..16780915d45 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1659,15 +1659,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/features/alerting/unified/components/notification-policies/Policy.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/alerting/unified/components/notification-policies/PromDurationDocs.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx index 8c66e0222d2..be19ca4ed14 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useAsyncFn } from 'react-use'; import { GrafanaTheme2, UrlQueryMap } from '@grafana/data'; -import { Alert, LoadingPlaceholder, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary, Stack } from '@grafana/ui'; +import { Alert, LoadingPlaceholder, Stack, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch } from 'app/types'; @@ -16,12 +16,12 @@ import { useGetContactPointsState } from './api/receiversApi'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable'; -import { findRoutesMatchingPredicate, NotificationPoliciesFilter } from './components/notification-policies/Filters'; +import { NotificationPoliciesFilter, findRoutesMatchingPredicate } from './components/notification-policies/Filters'; import { useAddPolicyModal, - useEditPolicyModal, - useDeletePolicyModal, useAlertGroupsModal, + useDeletePolicyModal, + useEditPolicyModal, } from './components/notification-policies/Modals'; import { Policy } from './components/notification-policies/Policy'; import { useAlertmanagerConfig } from './hooks/useAlertmanagerConfig'; @@ -249,6 +249,7 @@ const AmRoutes = () => { onShowAlertInstances={showAlertGroupsModal} routesMatchingFilters={routesMatchingFilters} matchingInstancesPreview={{ groupsMap: routeAlertGroupsMap, enabled: !instancesPreviewError }} + isAutoGenerated={false} /> )} diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx index 842c90c52db..f7eabb4896d 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx @@ -322,6 +322,7 @@ const getStyles = (theme: GrafanaTheme2) => { `, noMatchersWarning: css` padding: ${theme.spacing(1)} ${theme.spacing(2)}; + margin-bottom: ${theme.spacing(1)}; `, }; }; 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 88fece21e9f..88e43ab5a71 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 @@ -1,10 +1,10 @@ -import { render, screen, within } from '@testing-library/react'; +import { render, renderHook, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { noop } from 'lodash'; import React from 'react'; import { Router } from 'react-router-dom'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { AlertmanagerGroup, @@ -19,7 +19,12 @@ import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../.. import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; -import { Policy } from './Policy'; +import { + AUTOGENERATED_ROOT_LABEL_NAME, + Policy, + isAutoGeneratedRootAndSimplifiedEnabled, + useCreateDropdownMenuActions, +} from './Policy'; jest.mock('../../hooks/useAbilities', () => ({ ...jest.requireActual('../../hooks/useAbilities'), @@ -163,6 +168,7 @@ describe('Policy', () => { onAddPolicy={onAddPolicy} onDeletePolicy={onDeletePolicy} onShowAlertInstances={onShowAlertInstances} + isAutoGenerated={false} /> ); // should have default policy @@ -379,3 +385,104 @@ const mockRoutes: RouteWithID = { group_interval: undefined, repeat_interval: undefined, }; + +describe('isAutoGeneratedRootAndSimplifiedEnabled', () => { + it('returns false when simplified routing is not enabled', () => { + const route: RouteWithID = { + id: '1', + object_matchers: [['label', MatcherOperator.equal, 'true']], + }; + config.featureToggles.alertingSimplifiedRouting = false; + expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false); + }); + + it('returns false when object_matchers is not defined', () => { + const route: RouteWithID = { + id: '1', + }; + config.featureToggles.alertingSimplifiedRouting = true; + expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false); + }); + + it('returns true when object_matchers contains AUTOGENERATED_ROOT_LABEL_NAME, and simplified routing is enabled', () => { + const route: RouteWithID = { + id: '1', + object_matchers: [[AUTOGENERATED_ROOT_LABEL_NAME, MatcherOperator.equal, 'true']], + }; + config.featureToggles.alertingSimplifiedRouting = true; + expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(true); + }); + + it('returns false when object_matchers does not contain AUTOGENERATED_ROOT_LABEL_NAME, and simplified routing is enabled', () => { + const route: RouteWithID = { + id: '1', + object_matchers: [['label', MatcherOperator.equal, 'true']], + }; + config.featureToggles.alertingSimplifiedRouting = true; + expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false); + }); +}); + +describe('useCreateDropdownMenuActions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const openDetailModal = jest.fn(); + const currentRoute: RouteWithID = { id: '0', routes: [{ id: '1' }] }; + const toggleShowExportDrawer = jest.fn(); + const onDeletePolicy = jest.fn(); + const testCases = [ + { + isAutoGenerated: false, + isDefaultPolicy: true, + provisioned: false, + expectedMenu: ['edit-policy', 'export-policy'], + }, + { + isAutoGenerated: false, + isDefaultPolicy: true, + provisioned: true, + expectedMenu: ['edit-policy', 'export-policy'], + }, + { + isAutoGenerated: false, + isDefaultPolicy: false, + provisioned: false, + expectedMenu: ['edit-policy', 'delete-policy'], + }, + { + isAutoGenerated: false, + isDefaultPolicy: false, + provisioned: true, + expectedMenu: ['edit-policy', 'delete-policy'], + }, + { isAutoGenerated: true, isDefaultPolicy: true, provisioned: true, expectedMenu: ['edit-policy'] }, + { isAutoGenerated: true, isDefaultPolicy: false, provisioned: false, expectedMenu: ['edit-policy'] }, + { isAutoGenerated: true, isDefaultPolicy: true, provisioned: false, expectedMenu: ['edit-policy'] }, + { isAutoGenerated: true, isDefaultPolicy: false, provisioned: true, expectedMenu: ['edit-policy'] }, + ]; + + testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provisioned, expectedMenu }) => { + it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, provisioned=${provisioned}`, () => { + useAlertmanagerAbilitiesMock.mockReturnValue([ + [true, true], + [true, true], + [true, true], + ]); + const { result } = renderHook(() => + useCreateDropdownMenuActions( + isAutoGenerated, + isDefaultPolicy, + provisioned, + openDetailModal, + currentRoute, + toggleShowExportDrawer, + onDeletePolicy + ) + ); + + const menuItemsKeys = result.current.map((item) => item.key ?? ''); + expect(menuItemsKeys).toEqual(expectedMenu); + }); + }); +}); 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 d5e766bc342..d4cb411e1bc 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -6,11 +6,13 @@ import { Link } from 'react-router-dom'; import { useToggle } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Badge, Button, Dropdown, Icon, + IconButton, Menu, Stack, Text, @@ -19,14 +21,20 @@ import { useStyles2, } from '@grafana/ui'; import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; -import { AlertmanagerGroup, ObjectMatcher, Receiver, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; +import { + AlertmanagerGroup, + MatcherOperator, + ObjectMatcher, + Receiver, + RouteWithID, +} from 'app/plugins/datasource/alertmanager/types'; import { ReceiversState } from 'app/types'; -import { AlertmanagerAction, useAlertmanagerAbilities } from '../../hooks/useAbilities'; +import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { INTEGRATION_ICONS } from '../../types/contact-points'; import { normalizeMatchers } from '../../utils/matchers'; import { createContactPointLink, createMuteTimingLink } from '../../utils/misc'; -import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies'; +import { InhertitableProperties, getInheritedProperties } from '../../utils/notification-policies'; import { Authorize } from '../Authorize'; import { HoverCard } from '../HoverCard'; import { Label } from '../Label'; @@ -37,7 +45,7 @@ import { Strong } from '../Strong'; import { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter'; import { Matchers } from './Matchers'; -import { TimingOptions, TIMING_OPTIONS_DEFAULTS } from './timingOptions'; +import { TIMING_OPTIONS_DEFAULTS, TimingOptions } from './timingOptions'; interface PolicyComponentProps { receivers?: Receiver[]; @@ -54,55 +62,42 @@ interface PolicyComponentProps { routeTree: RouteWithID; currentRoute: RouteWithID; alertManagerSourceName: string; - onEditPolicy: (route: RouteWithID, isDefault?: boolean) => void; + onEditPolicy: (route: RouteWithID, isDefault?: boolean, isAutogenerated?: boolean) => void; onAddPolicy: (route: RouteWithID) => void; onDeletePolicy: (route: RouteWithID) => void; onShowAlertInstances: (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => void; + isAutoGenerated?: boolean; } -const Policy: FC = ({ - receivers = [], - contactPointsState, - readOnly = false, - provisioned = false, - alertGroups = [], - alertManagerSourceName, - currentRoute, - routeTree, - inheritedProperties, - routesMatchingFilters = [], - matchingInstancesPreview = { enabled: false }, - onEditPolicy, - onAddPolicy, - onDeletePolicy, - onShowAlertInstances, -}) => { - const styles = useStyles2(getStyles); - const isDefaultPolicy = currentRoute === routeTree; +const Policy = (props: PolicyComponentProps) => { + const { + receivers = [], + contactPointsState, + readOnly = false, + provisioned = false, + alertGroups = [], + alertManagerSourceName, + currentRoute, + routeTree, + inheritedProperties, + routesMatchingFilters = [], + matchingInstancesPreview = { enabled: false }, + onEditPolicy, + onAddPolicy, + onDeletePolicy, + onShowAlertInstances, + isAutoGenerated = false, + } = props; - const [ - [updatePoliciesSupported, updatePoliciesAllowed], - [deletePolicySupported, deletePolicyAllowed], - [exportPoliciesSupported, exportPoliciesAllowed], - ] = useAlertmanagerAbilities([ - AlertmanagerAction.UpdateNotificationPolicyTree, - AlertmanagerAction.DeleteNotificationPolicy, - AlertmanagerAction.ExportNotificationPolicies, - ]); + const styles = useStyles2(getStyles); + + const isDefaultPolicy = currentRoute === routeTree; const contactPoint = currentRoute.receiver; const continueMatching = currentRoute.continue ?? false; - const groupBy = currentRoute.group_by; - const muteTimings = currentRoute.mute_time_intervals ?? []; - const timingOptions: TimingOptions = { - group_wait: currentRoute.group_wait, - group_interval: currentRoute.group_interval, - repeat_interval: currentRoute.repeat_interval, - }; const matchers = normalizeMatchers(currentRoute); const hasMatchers = Boolean(matchers && matchers.length); - const hasMuteTimings = Boolean(muteTimings.length); const hasFocus = routesMatchingFilters.some((route) => route.id === currentRoute.id); // gather errors here @@ -116,33 +111,340 @@ const Policy: FC = ({ const actualContactPoint = contactPoint ?? inheritedProperties?.receiver ?? ''; const contactPointErrors = contactPointsState ? getContactPointErrors(actualContactPoint, contactPointsState) : []; - contactPointErrors.forEach((error) => { - errors.push(error); - }); - - const hasInheritedProperties = inheritedProperties && Object.keys(inheritedProperties).length > 0; - const childPolicies = currentRoute.routes ?? []; - - const inheritedGrouping = hasInheritedProperties && inheritedProperties.group_by; - const noGrouping = isArray(groupBy) && groupBy[0] === '...'; - const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0; - const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0; - + const [showExportDrawer, toggleShowExportDrawer] = useToggle(false); const matchingAlertGroups = matchingInstancesPreview?.groupsMap?.get(currentRoute.id); - // sum all alert instances for all groups we're handling const numberOfAlertInstances = matchingAlertGroups ? sumBy(matchingAlertGroups, (group) => group.alerts.length) : undefined; - const [showExportDrawer, toggleShowExportDrawer] = useToggle(false); - const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy; - const showEditAction = updatePoliciesSupported && updatePoliciesAllowed; - const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy; + // simplified routing permissions + const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility( + AlertmanagerAction.ViewAutogeneratedPolicyTree + ); + // collapsible policies variables + const isThisPolicyCollapsible = useShouldPolicyBeCollapsible(currentRoute); + const [isBranchOpen, toggleBranchOpen] = useToggle(false); + const renderChildPolicies = (isThisPolicyCollapsible && isBranchOpen) || !isThisPolicyCollapsible; + + const groupBy = currentRoute.group_by; + const muteTimings = currentRoute.mute_time_intervals ?? []; + + const timingOptions: TimingOptions = { + group_wait: currentRoute.group_wait, + group_interval: currentRoute.group_interval, + repeat_interval: currentRoute.repeat_interval, + }; + + contactPointErrors.forEach((error) => { + errors.push(error); + }); + + const isAutogeneratedPolicyRoot = isAutoGeneratedRootAndSimplifiedEnabled(currentRoute); // build the menu actions for our policy - const dropdownMenuActions: JSX.Element[] = []; + const dropdownMenuActions: JSX.Element[] = useCreateDropdownMenuActions( + isAutoGenerated, + isDefaultPolicy, + provisioned, + onEditPolicy, + currentRoute, + toggleShowExportDrawer, + onDeletePolicy + ); + + // check if this policy should be visible. If it's autogenerated and the user is not allowed to see autogenerated + // policies then we should not show it. Same if the user is not supported to see autogenerated policies. + const hideCurrentPolicy = + isAutoGenerated && (!isAllowedToSeeAutogeneratedChunk || !isSupportedToSeeAutogeneratedChunk); + + if (hideCurrentPolicy) { + return null; + } + const isImmutablePolicy = isDefaultPolicy || isAutogeneratedPolicyRoot; + // TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated + return ( + <> + +
+ {/* continueMatching and showMatchesAllLabelsWarning are mutually exclusive so the icons can't overlap */} + {continueMatching && } + {showMatchesAllLabelsWarning && } + +
+ + {/* Matchers and actions */} +
+ + {isThisPolicyCollapsible && ( + + )} + {isImmutablePolicy ? ( + isAutogeneratedPolicyRoot ? ( + + ) : ( + + ) + ) : hasMatchers ? ( + + ) : ( + No matchers + )} + + {/* TODO maybe we should move errors to the gutter instead? */} + {errors.length > 0 && } + {provisioned && } + + {!isAutoGenerated && !readOnly && ( + + + + + + )} + {dropdownMenuActions.length > 0 && ( + {dropdownMenuActions}}> +
+ + {/* Metadata row */} + +
+
+
+
+ {renderChildPolicies && ( + <> + {childPolicies.map((child) => { + const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties); + // This child is autogenerated if it's the autogenerated root or if it's a child of an autogenerated policy. + const isThisChildAutoGenerated = isAutoGeneratedRootAndSimplifiedEnabled(child) || isAutoGenerated; + /* pass the "readOnly" prop from the parent, because for any child policy , if its parent it's not editable, + then the child policy should not be editable either */ + const isThisChildReadOnly = readOnly || provisioned || isAutoGenerated; + + return ( + + ); + })} + + )} +
+ {showExportDrawer && } +
+ + ); +}; +/** + * This function returns if the policy should be collapsible or not. + * Add here conditions for policies that should be collapsible. + */ +function useShouldPolicyBeCollapsible(route: RouteWithID): boolean { + const childrenCount = route.routes?.length ?? 0; + const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility( + AlertmanagerAction.ViewAutogeneratedPolicyTree + ); + return ( + childrenCount > 0 && + isSupportedToSeeAutogeneratedChunk && + isAllowedToSeeAutogeneratedChunk && + isAutoGeneratedRootAndSimplifiedEnabled(route) + ); +} + +interface MetadataRowProps { + matchingInstancesPreview: { groupsMap?: Map; enabled: boolean }; + numberOfAlertInstances?: number; + contactPoint?: string; + groupBy?: string[]; + muteTimings?: string[]; + timingOptions?: TimingOptions; + inheritedProperties?: Partial; + alertManagerSourceName: string; + receivers: Receiver[]; + matchingAlertGroups?: AlertmanagerGroup[]; + matchers?: ObjectMatcher[]; + isDefaultPolicy: boolean; + onShowAlertInstances: (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => void; +} + +function MetadataRow({ + numberOfAlertInstances, + isDefaultPolicy, + timingOptions, + groupBy, + muteTimings = [], + matchingInstancesPreview, + inheritedProperties, + matchingAlertGroups, + onShowAlertInstances, + matchers, + contactPoint, + alertManagerSourceName, + receivers, +}: MetadataRowProps) { + const styles = useStyles2(getStyles); + + const inheritedGrouping = inheritedProperties && inheritedProperties.group_by; + const hasInheritedProperties = inheritedProperties && Object.keys(inheritedProperties).length > 0; + + const noGrouping = isArray(groupBy) && groupBy[0] === '...'; + const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0; + const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0; + + const hasMuteTimings = Boolean(muteTimings.length); + + return ( +
+ + {matchingInstancesPreview.enabled && ( + { + matchingAlertGroups && onShowAlertInstances(matchingAlertGroups, matchers); + }} + data-testid="matching-instances" + > + {numberOfAlertInstances ?? '-'} + {pluralize('instance', numberOfAlertInstances)} + + )} + {contactPoint && ( + + Delivered to + + + )} + {!inheritedGrouping && ( + <> + {customGrouping && ( + + Grouped by + {groupBy.join(', ')} + + )} + {singleGroup && ( + + Single group + + )} + {noGrouping && ( + + Not grouping + + )} + + )} + {hasMuteTimings && ( + + Muted when + + + )} + {timingOptions && ( + // for the default policy we will also merge the default timings, that way a user can observe what the timing options would be + + )} + {hasInheritedProperties && ( + <> + + Inherited + + + + )} + +
+ ); +} + +export const useCreateDropdownMenuActions = ( + isAutoGenerated: boolean, + isDefaultPolicy: boolean, + provisioned: boolean, + onEditPolicy: (route: RouteWithID, isDefault?: boolean, readOnly?: boolean) => void, + currentRoute: RouteWithID, + toggleShowExportDrawer: (nextValue?: any) => void, + onDeletePolicy: (route: RouteWithID) => void +) => { + const [ + [updatePoliciesSupported, updatePoliciesAllowed], + [deletePolicySupported, deletePolicyAllowed], + [exportPoliciesSupported, exportPoliciesAllowed], + ] = useAlertmanagerAbilities([ + AlertmanagerAction.UpdateNotificationPolicyTree, + AlertmanagerAction.DeleteNotificationPolicy, + AlertmanagerAction.ExportNotificationPolicies, + ]); + const dropdownMenuActions = []; + const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy && !isAutoGenerated; + const showEditAction = updatePoliciesSupported && updatePoliciesAllowed; + const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy && !isAutoGenerated; if (showEditAction) { dropdownMenuActions.push( @@ -150,7 +452,7 @@ const Policy: FC = ({ onEditPolicy(currentRoute, isDefaultPolicy)} /> @@ -173,7 +475,7 @@ const Policy: FC = ({ onDeletePolicy(currentRoute)} /> @@ -181,167 +483,31 @@ const Policy: FC = ({ ); } - - // TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated - return ( - -
- {/* continueMatching and showMatchesAllLabelsWarning are mutually exclusive so the icons can't overlap */} - {continueMatching && } - {showMatchesAllLabelsWarning && } -
- - {/* Matchers and actions */} -
- - {isDefaultPolicy ? ( - - ) : hasMatchers ? ( - - ) : ( - No matchers - )} - - {/* TODO maybe we should move errors to the gutter instead? */} - {errors.length > 0 && } - {provisioned && } - {!readOnly && ( - - - - - - - {dropdownMenuActions.length > 0 && ( - {dropdownMenuActions}}> -
- - {/* Metadata row */} -
- - {matchingInstancesPreview.enabled && ( - { - matchingAlertGroups && onShowAlertInstances(matchingAlertGroups, matchers); - }} - data-testid="matching-instances" - > - {numberOfAlertInstances ?? '-'} - {pluralize('instance', numberOfAlertInstances)} - - )} - {contactPoint && ( - - Delivered to - - - )} - {!inheritedGrouping && ( - <> - {customGrouping && ( - - Grouped by - {groupBy.join(', ')} - - )} - {singleGroup && ( - - Single group - - )} - {noGrouping && ( - - Not grouping - - )} - - )} - {hasMuteTimings && ( - - Muted when - - - )} - {timingOptions && ( - // for the default policy we will also merge the default timings, that way a user can observe what the timing options would be - - )} - {hasInheritedProperties && ( - <> - - Inherited - - - - )} - -
-
-
-
-
- {/* pass the "readOnly" prop from the parent, because if you can't edit the parent you can't edit children */} - {childPolicies.map((child) => { - const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties); - - return ( - - ); - })} -
- {showExportDrawer && } -
- ); + return dropdownMenuActions; }; +export const AUTOGENERATED_ROOT_LABEL_NAME = '__grafana_autogenerated__'; + +export function isAutoGeneratedRootAndSimplifiedEnabled(route: RouteWithID) { + const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; + if (!simplifiedRoutingToggleEnabled) { + return false; + } + if (!route.object_matchers) { + return false; + } + return ( + route.object_matchers.some((objectMatcher) => { + return ( + objectMatcher[0] === AUTOGENERATED_ROOT_LABEL_NAME && + objectMatcher[1] === MatcherOperator.equal && + objectMatcher[2] === 'true' + ); + }) ?? false + ); + // return simplifiedRoutingToggleEnabled && route.receiver === 'contact_point_5'; +} + const ProvisionedTooltip = (children: ReactNode) => ( {children} @@ -388,7 +554,7 @@ const AllMatchesIndicator: FC = () => { ); }; -const DefaultPolicyIndicator: FC = () => { +function DefaultPolicyIndicator() { const styles = useStyles2(getStyles); return ( <> @@ -398,7 +564,11 @@ const DefaultPolicyIndicator: FC = () => { ); -}; +} + +function AutogeneratedRootIndicator() { + return Auto-generated policies; +} const InheritedProperties: FC<{ properties: InhertitableProperties }> = ({ properties }) => ( ({ const { color, borderColor } = getTagColorsFromName(label); return { - wrapper: css` - color: #fff; - background: ${color}; - padding: ${theme.spacing(0.33)} ${theme.spacing(0.66)}; - font-size: ${theme.typography.bodySmall.fontSize}; - - border: solid 1px ${borderColor}; - border-radius: ${theme.shape.radius.default}; - `, + wrapper: css({ + color: '#fff', + background: color, + padding: `${theme.spacing(0.33)} ${theme.spacing(0.66)}`, + fontSize: theme.typography.bodySmall.fontSize, + border: `solid 1px ${borderColor}`, + borderRadius: theme.shape.radius.default, + }), }; }, - childPolicies: css` - margin-left: ${theme.spacing(4)}; - position: relative; - - &:before { - content: ''; - position: absolute; - height: calc(100% - 10px); - - border-left: solid 1px ${theme.colors.border.weak}; - - margin-top: 0; - margin-left: -20px; - } - `, - policyItemWrapper: css` - padding: ${theme.spacing(1.5)}; - `, - metadataRow: css` - background: ${theme.colors.background.secondary}; - - border-bottom-left-radius: ${theme.shape.borderRadius(2)}; - border-bottom-right-radius: ${theme.shape.borderRadius(2)}; - `, - policyWrapper: (hasFocus = false) => css` - flex: 1; - position: relative; - background: ${theme.colors.background.secondary}; - - border-radius: ${theme.shape.radius.default}; - border: solid 1px ${theme.colors.border.weak}; - - ${hasFocus && - css` - border-color: ${theme.colors.primary.border}; - `} - `, - metadata: css` - color: ${theme.colors.text.secondary}; - - font-size: ${theme.typography.bodySmall.fontSize}; - font-weight: ${theme.typography.bodySmall.fontWeight}; - `, - break: css` - width: 100%; - height: 0; - margin-bottom: ${theme.spacing(2)}; - `, - gutterIcon: css` - position: absolute; - - top: 0; - transform: translateY(50%); - left: -${theme.spacing(4)}; - - color: ${theme.colors.text.secondary}; - background: ${theme.colors.background.primary}; - - width: 25px; - height: 25px; - text-align: center; - - border: solid 1px ${theme.colors.border.weak}; - border-radius: ${theme.shape.radius.default}; - - padding: 0; - `, + childPolicies: css({ + marginLeft: theme.spacing(4), + position: 'relative', + '&:before': { + content: '""', + position: 'absolute', + height: 'calc(100% - 10px)', + borderLeft: `solid 1px ${theme.colors.border.weak}`, + marginTop: 0, + marginLeft: '-20px', + }, + }), + policyItemWrapper: css({ + padding: theme.spacing(1.5), + }), + metadataRow: css({ + background: theme.colors.background.secondary, + borderBottomLeftRadius: theme.shape.borderRadius(2), + borderBottomRightRadius: theme.shape.borderRadius(2), + }), + policyWrapper: (hasFocus = false) => + css({ + flex: 1, + position: 'relative', + background: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + border: `solid 1px ${theme.colors.border.weak}`, + ...(hasFocus && { borderColor: theme.colors.primary.border }), + }), + metadata: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.bodySmall.fontWeight, + }), + break: css({ + width: '100%', + height: 0, + marginBottom: theme.spacing(2), + }), + gutterIcon: css({ + position: 'absolute', + top: 0, + transform: 'translateY(50%)', + left: `-${theme.spacing(4)}`, + color: theme.colors.text.secondary, + background: theme.colors.background.primary, + width: '25px', + height: '25px', + textAlign: 'center', + border: `solid 1px ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + padding: 0, + }), }); export { Policy }; diff --git a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap index 98f40f5cac5..52ae3a31f20 100644 --- a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap +++ b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap @@ -78,6 +78,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a true, false, ], + "view-autogenerated-policy-tree": [ + false, + false, + ], "view-contact-point": [ true, false, @@ -183,6 +187,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi true, true, ], + "view-autogenerated-policy-tree": [ + false, + false, + ], "view-contact-point": [ true, true, @@ -288,6 +296,10 @@ exports[`alertmanager abilities should report everything is supported for builti true, false, ], + "view-autogenerated-policy-tree": [ + true, + false, + ], "view-contact-point": [ true, true, diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 0638fc4b2bb..df4cf5e5eb7 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { contextSrv as ctx } from 'app/core/services/context_srv'; +import { contextSrv, contextSrv as ctx } from 'app/core/services/context_srv'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; @@ -45,6 +45,7 @@ export enum AlertmanagerAction { UpdateNotificationPolicyTree = 'update-notification-policy-tree', DeleteNotificationPolicy = 'delete-notification-policy', ExportNotificationPolicies = 'export-notification-policies', + ViewAutogeneratedPolicyTree = 'view-autogenerated-policy-tree', // silences – these cannot be deleted only "expired" (updated) CreateSilence = 'create-silence', @@ -190,6 +191,9 @@ export function useAllAlertmanagerAbilities(): Abilities { const notificationsPermissions = getNotificationsPermissions(selectedAlertmanager!); const instancePermissions = getInstancesPermissions(selectedAlertmanager!); + //we need to know user role to determine if they can view autogenerated policy tree + const isAdmin = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin; + // list out all of the abilities, and if the user has permissions to perform them const abilities: Abilities = { // -- configuration -- @@ -226,6 +230,7 @@ export function useAllAlertmanagerAbilities(): Abilities { isGrafanaFlavoredAlertmanager, notificationsPermissions.provisioning.readSecrets ), + [AlertmanagerAction.ViewAutogeneratedPolicyTree]: [isGrafanaFlavoredAlertmanager, isAdmin], // -- silences -- // for now, all supported Alertmanager flavors have API endpoints for managing silences [AlertmanagerAction.CreateSilence]: toAbility(AlwaysSupported, instancePermissions.create),