View File Name : AlertAmRoutes.10a0d9a23a082caf12c8.js.map
\n }\n >\n
\n \n \n \n }\n invalid={inputInvalid}\n error={inputInvalid ? 'Query must use valid matcher syntax' : null}\n >\n
}\n onChange={(event) => {\n setSearchParams({ queryString: event.currentTarget.value });\n }}\n defaultValue={queryString}\n />\n \n
\n \n {hasFilters && (\n
\n \n \n {matchingCount === 0 && 'No policies matching filters.'}\n {matchingCount === 1 && `${matchingCount} policy matches the filters.`}\n {matchingCount > 1 && `${matchingCount} policies match the filters.`}\n \n \n )}\n \n );\n};\n\n/**\n * Find a list of route IDs that match given input filters\n */\ntype FilterPredicate = (route: RouteWithID) => boolean;\n\n/**\n * Find routes int the tree that match the given predicate function\n * @param routeTree the route tree to search\n * @param predicateFn the predicate function to match routes\n * @returns\n * - matches: list of routes that match the predicate\n * - matchingRouteIdsWithPath: map with routeids that are part of the path of a matching route\n * key is the route id, value is an array of route ids that are part of its path\n */\nexport function findRoutesMatchingPredicate(\n routeTree: RouteWithID,\n predicateFn: FilterPredicate\n): Map
{\n // map with routids that are part of the path of a matching route\n // key is the route id, value is an array of route ids that are part of the path\n const matchingRouteIdsWithPath = new Map();\n\n function findMatch(route: RouteWithID, path: RouteWithID[]) {\n const newPath = [...path, route];\n\n if (predicateFn(route)) {\n // if the route matches the predicate, we need to add the path to the map of matching routes\n const previousPath = matchingRouteIdsWithPath.get(route) ?? [];\n // add the current route id to the map with its path\n matchingRouteIdsWithPath.set(route, [...previousPath, ...newPath]);\n }\n\n // if the route has subroutes, call findMatch recursively\n route.routes?.forEach((route) => findMatch(route, newPath));\n }\n\n findMatch(routeTree, []);\n\n return matchingRouteIdsWithPath;\n}\n\nexport function findRoutesByMatchers(route: RouteWithID, labelMatchersFilter: ObjectMatcher[]): boolean {\n const routeMatchers = normalizeMatchers(route);\n\n return labelMatchersFilter.every((filter) => routeMatchers.some((matcher) => isEqual(filter, matcher)));\n}\n\nconst toOption = (receiver: Receiver) => ({\n label: receiver.name,\n value: receiver.name,\n});\n\nconst getNotificationPoliciesFilters = (searchParams: URLSearchParams) => ({\n queryString: searchParams.get('queryString') ?? undefined,\n contactPoint: searchParams.get('contactPoint') ?? undefined,\n});\n\nconst getStyles = () => ({\n noBottom: css({\n marginBottom: 0,\n }),\n});\n\nexport { NotificationPoliciesFilter };\n","import { Receiver } from 'app/plugins/datasource/alertmanager/types';\n\nimport { onCallApi } from '../../../api/onCallApi';\nimport { usePluginBridge } from '../../../hooks/usePluginBridge';\nimport { SupportedPlugin } from '../../../types/pluginBridges';\n\nimport { isOnCallReceiver } from './onCall/onCall';\nimport { AmRouteReceiver } from './types';\n\nexport const useGetGrafanaReceiverTypeChecker = () => {\n const { installed: isOnCallEnabled } = usePluginBridge(SupportedPlugin.OnCall);\n const { data } = onCallApi.useGrafanaOnCallIntegrationsQuery(undefined, {\n skip: !isOnCallEnabled,\n });\n const getGrafanaReceiverType = (receiver: Receiver): SupportedPlugin | undefined => {\n //CHECK FOR ONCALL PLUGIN\n const onCallIntegrations = data ?? [];\n if (isOnCallEnabled && isOnCallReceiver(receiver, onCallIntegrations)) {\n return SupportedPlugin.OnCall;\n }\n //WE WILL ADD IN HERE IF THERE ARE MORE TYPES TO CHECK\n return undefined;\n };\n\n return getGrafanaReceiverType;\n};\n\nexport const useGetAmRouteReceiverWithGrafanaAppTypes = (receivers: Receiver[]) => {\n const getGrafanaReceiverType = useGetGrafanaReceiverTypeChecker();\n const receiverToSelectableContactPointValue = (receiver: Receiver): AmRouteReceiver => {\n const amRouteReceiverValue: AmRouteReceiver = {\n label: receiver.name,\n value: receiver.name,\n grafanaAppReceiverType: getGrafanaReceiverType(receiver),\n };\n return amRouteReceiverValue;\n };\n\n return receivers.map(receiverToSelectableContactPointValue);\n};\n","import pluralize from 'pluralize';\nimport React, { Fragment } from 'react';\n\nimport { Badge, Stack } from '@grafana/ui';\n\ninterface Props {\n active?: number;\n suppressed?: number;\n unprocessed?: number;\n}\n\nexport const AlertGroupsSummary = ({ active = 0, suppressed = 0, unprocessed = 0 }: Props) => {\n const statsComponents: React.ReactNode[] = [];\n const total = active + suppressed + unprocessed;\n\n if (active) {\n statsComponents.push();\n }\n\n if (suppressed) {\n statsComponents.push();\n }\n\n if (unprocessed) {\n statsComponents.push();\n }\n\n // if we only have one category it's not really necessary to repeat the total\n if (statsComponents.length > 1) {\n statsComponents.unshift(\n \n {total} {pluralize('instance', total)}\n \n );\n }\n\n const hasStats = Boolean(statsComponents.length);\n\n return hasStats ? {statsComponents} : null;\n};\n","import React, { ReactNode, useState } from 'react';\n\nimport { Collapse, Field, Form, InputControl, Link, MultiSelect, Select, useStyles2 } from '@grafana/ui';\nimport { RouteWithID } from 'app/plugins/datasource/alertmanager/types';\n\nimport { FormAmRoute } from '../../types/amroutes';\nimport {\n amRouteToFormAmRoute,\n commonGroupByOptions,\n mapMultiSelectValueToStrings,\n mapSelectValueToString,\n promDurationValidator,\n repeatIntervalValidator,\n stringsToSelectableValues,\n stringToSelectableValue,\n} from '../../utils/amroutes';\nimport { makeAMLink } from '../../utils/misc';\nimport { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';\n\nimport { PromDurationInput } from './PromDurationInput';\nimport { getFormStyles } from './formStyles';\nimport { TIMING_OPTIONS_DEFAULTS } from './timingOptions';\n\nexport interface AmRootRouteFormProps {\n alertManagerSourceName: string;\n actionButtons: ReactNode;\n onSubmit: (route: Partial) => void;\n receivers: AmRouteReceiver[];\n route: RouteWithID;\n}\n\nexport const AmRootRouteForm = ({\n actionButtons,\n alertManagerSourceName,\n onSubmit,\n receivers,\n route,\n}: AmRootRouteFormProps) => {\n const styles = useStyles2(getFormStyles);\n const [isTimingOptionsExpanded, setIsTimingOptionsExpanded] = useState(false);\n const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route.group_by));\n\n const defaultValues = amRouteToFormAmRoute(route);\n\n return (\n \n );\n};\n","import { useMemo } from 'react';\n\nimport { SelectableValue } from '@grafana/data';\n\nimport { useAlertmanager } from '../state/AlertmanagerContext';\nimport { timeIntervalToString } from '../utils/alertmanager';\n\nimport { useAlertmanagerConfig } from './useAlertmanagerConfig';\n\nexport function useMuteTimingOptions(): Array> {\n const { selectedAlertmanager } = useAlertmanager();\n const { currentData } = useAlertmanagerConfig(selectedAlertmanager);\n const config = currentData?.alertmanager_config;\n\n return useMemo(() => {\n const muteTimingsOptions: Array> =\n config?.mute_time_intervals?.map((value) => ({\n value: value.name,\n label: value.name,\n description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '),\n })) ?? [];\n\n return muteTimingsOptions;\n }, [config]);\n}\n","import { css } from '@emotion/css';\nimport React, { ReactNode, useState } from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport {\n Badge,\n Button,\n Field,\n FieldArray,\n FieldValidationMessage,\n Form,\n IconButton,\n Input,\n InputControl,\n MultiSelect,\n Select,\n Stack,\n Switch,\n useStyles2,\n} from '@grafana/ui';\nimport { MatcherOperator, RouteWithID } from 'app/plugins/datasource/alertmanager/types';\n\nimport { useMuteTimingOptions } from '../../hooks/useMuteTimingOptions';\nimport { FormAmRoute } from '../../types/amroutes';\nimport { SupportedPlugin } from '../../types/pluginBridges';\nimport { matcherFieldOptions } from '../../utils/alertmanager';\nimport {\n amRouteToFormAmRoute,\n commonGroupByOptions,\n emptyArrayFieldMatcher,\n mapMultiSelectValueToStrings,\n mapSelectValueToString,\n promDurationValidator,\n repeatIntervalValidator,\n stringToSelectableValue,\n stringsToSelectableValues,\n} from '../../utils/amroutes';\nimport { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';\n\nimport { PromDurationInput } from './PromDurationInput';\nimport { getFormStyles } from './formStyles';\nimport { routeTimingsFields } from './routeTimingsFields';\n\nexport interface AmRoutesExpandedFormProps {\n receivers: AmRouteReceiver[];\n route?: RouteWithID;\n onSubmit: (route: Partial) => void;\n actionButtons: ReactNode;\n defaults?: Partial;\n}\n\nexport const AmRoutesExpandedForm = ({\n actionButtons,\n receivers,\n route,\n onSubmit,\n defaults,\n}: AmRoutesExpandedFormProps) => {\n const styles = useStyles2(getStyles);\n const formStyles = useStyles2(getFormStyles);\n const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route?.group_by));\n const muteTimingOptions = useMuteTimingOptions();\n const emptyMatcher = [{ name: '', operator: MatcherOperator.equal, value: '' }];\n\n const receiversWithOnCallOnTop = receivers.sort(onCallFirst);\n\n const formAmRoute = {\n ...amRouteToFormAmRoute(route),\n ...defaults,\n };\n\n const defaultValues: Omit = {\n ...formAmRoute,\n // if we're adding a new route, show at least one empty matcher\n object_matchers: route ? formAmRoute.object_matchers : emptyMatcher,\n };\n\n return (\n \n );\n};\n\nfunction onCallFirst(receiver: AmRouteReceiver) {\n if (receiver.grafanaAppReceiverType === SupportedPlugin.OnCall) {\n return -1;\n } else {\n return 0;\n }\n}\n\nconst getStyles = (theme: GrafanaTheme2) => {\n const commonSpacing = theme.spacing(3.5);\n\n return {\n addMatcherBtn: css`\n margin-bottom: ${commonSpacing};\n `,\n matchersContainer: css`\n background-color: ${theme.colors.background.secondary};\n padding: ${theme.spacing(1.5)} ${theme.spacing(2)};\n padding-bottom: 0;\n width: fit-content;\n `,\n matchersOperator: css`\n min-width: 120px;\n `,\n noMatchersWarning: css`\n padding: ${theme.spacing(1)} ${theme.spacing(2)};\n margin-bottom: ${theme.spacing(1)};\n `,\n };\n};\n","import { groupBy } from 'lodash';\nimport React, { FC, useCallback, useMemo, useState } from 'react';\n\nimport { Button, Icon, Modal, ModalProps, Spinner, Stack } from '@grafana/ui';\nimport {\n AlertmanagerGroup,\n AlertState,\n ObjectMatcher,\n Receiver,\n RouteWithID,\n} from 'app/plugins/datasource/alertmanager/types';\n\nimport { FormAmRoute } from '../../types/amroutes';\nimport { MatcherFormatter } from '../../utils/matchers';\nimport { AlertGroup } from '../alert-groups/AlertGroup';\nimport { useGetAmRouteReceiverWithGrafanaAppTypes } from '../receivers/grafanaAppReceivers/grafanaApp';\n\nimport { AlertGroupsSummary } from './AlertGroupsSummary';\nimport { AmRootRouteForm } from './EditDefaultPolicyForm';\nimport { AmRoutesExpandedForm } from './EditNotificationPolicyForm';\nimport { Matchers } from './Matchers';\n\ntype ModalHook = [JSX.Element, (item: T) => void, () => void];\ntype EditModalHook = [JSX.Element, (item: RouteWithID, isDefaultRoute?: boolean) => void, () => void];\n\nconst useAddPolicyModal = (\n receivers: Receiver[] = [],\n handleAdd: (route: Partial, parentRoute: RouteWithID) => void,\n loading: boolean\n): ModalHook => {\n const [showModal, setShowModal] = useState(false);\n const [parentRoute, setParentRoute] = useState();\n const AmRouteReceivers = useGetAmRouteReceiverWithGrafanaAppTypes(receivers);\n\n const handleDismiss = useCallback(() => {\n setParentRoute(undefined);\n setShowModal(false);\n }, []);\n\n const handleShow = useCallback((parentRoute: RouteWithID) => {\n setParentRoute(parentRoute);\n setShowModal(true);\n }, []);\n\n const modalElement = useMemo(\n () =>\n loading ? (\n \n ) : (\n \n parentRoute && handleAdd(newRoute, parentRoute)}\n actionButtons={\n \n \n \n \n }\n />\n \n ),\n [AmRouteReceivers, handleAdd, handleDismiss, loading, parentRoute, showModal]\n );\n\n return [modalElement, handleShow, handleDismiss];\n};\n\nconst useEditPolicyModal = (\n alertManagerSourceName: string,\n receivers: Receiver[],\n handleSave: (route: Partial) => void,\n loading: boolean\n): EditModalHook => {\n const [showModal, setShowModal] = useState(false);\n const [isDefaultPolicy, setIsDefaultPolicy] = useState(false);\n const [route, setRoute] = useState();\n const AmRouteReceivers = useGetAmRouteReceiverWithGrafanaAppTypes(receivers);\n\n const handleDismiss = useCallback(() => {\n setRoute(undefined);\n setShowModal(false);\n }, []);\n\n const handleShow = useCallback((route: RouteWithID, isDefaultPolicy?: boolean) => {\n setIsDefaultPolicy(isDefaultPolicy ?? false);\n setRoute(route);\n setShowModal(true);\n }, []);\n\n const modalElement = useMemo(\n () =>\n loading ? (\n \n ) : (\n \n {isDefaultPolicy && route && (\n \n \n \n \n }\n />\n )}\n {!isDefaultPolicy && (\n \n \n \n \n }\n />\n )}\n \n ),\n [AmRouteReceivers, alertManagerSourceName, handleDismiss, handleSave, isDefaultPolicy, loading, route, showModal]\n );\n\n return [modalElement, handleShow, handleDismiss];\n};\n\nconst useDeletePolicyModal = (handleDelete: (route: RouteWithID) => void, loading: boolean): ModalHook => {\n const [showModal, setShowModal] = useState(false);\n const [route, setRoute] = useState();\n\n const handleDismiss = useCallback(() => {\n setRoute(undefined);\n setShowModal(false);\n }, [setRoute]);\n\n const handleShow = useCallback((route: RouteWithID) => {\n setRoute(route);\n setShowModal(true);\n }, []);\n\n const handleSubmit = useCallback(() => {\n if (route) {\n handleDelete(route);\n }\n }, [handleDelete, route]);\n\n const modalElement = useMemo(\n () =>\n loading ? (\n \n ) : (\n \n Deleting this notification policy will permanently remove it.
\n Are you sure you want to delete this policy?
\n\n \n \n \n \n \n ),\n [handleDismiss, handleSubmit, loading, showModal]\n );\n\n return [modalElement, handleShow, handleDismiss];\n};\n\nconst useAlertGroupsModal = (): [\n JSX.Element,\n (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => void,\n () => void,\n] => {\n const [showModal, setShowModal] = useState(false);\n const [alertGroups, setAlertGroups] = useState([]);\n const [matchers, setMatchers] = useState([]);\n const [formatter, setFormatter] = useState('default');\n\n const handleDismiss = useCallback(() => {\n setShowModal(false);\n setAlertGroups([]);\n setMatchers([]);\n }, []);\n\n const handleShow = useCallback(\n (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[], formatter?: MatcherFormatter) => {\n setAlertGroups(alertGroups);\n if (matchers) {\n setMatchers(matchers);\n }\n if (formatter) {\n setFormatter(formatter);\n }\n setShowModal(true);\n },\n []\n );\n\n const instancesByState = useMemo(() => {\n const instances = alertGroups.flatMap((group) => group.alerts);\n return groupBy(instances, (instance) => instance.status.state);\n }, [alertGroups]);\n\n const modalElement = useMemo(\n () => (\n \n \n Matchers\n \n \n \n }\n >\n \n \n \n {alertGroups.map((group, index) => (\n
\n ))}\n
\n \n \n \n \n \n ),\n [alertGroups, handleDismiss, instancesByState, matchers, formatter, showModal]\n );\n\n return [modalElement, handleShow, handleDismiss];\n};\n\nconst UpdatingModal: FC> = ({ isOpen }) => (\n {}}\n closeOnBackdropClick={false}\n closeOnEscape={false}\n title={\n \n Updating... \n \n }\n >\n Please wait while we update your notification policies.\n \n);\n\nexport { useAddPolicyModal, useDeletePolicyModal, useEditPolicyModal, useAlertGroupsModal };\n","import React, { useState } from 'react';\n\nimport { LoadingPlaceholder } from '@grafana/ui';\n\nimport { alertRuleApi } from '../../api/alertRuleApi';\n\nimport { FileExportPreview } from './FileExportPreview';\nimport { GrafanaExportDrawer } from './GrafanaExportDrawer';\nimport { allGrafanaExportProviders, ExportFormats } from './providers';\ninterface GrafanaPoliciesPreviewProps {\n exportFormat: ExportFormats;\n onClose: () => void;\n}\n\nconst GrafanaPoliciesExporterPreview = ({ exportFormat, onClose }: GrafanaPoliciesPreviewProps) => {\n const { currentData: policiesDefinition = '', isFetching } = alertRuleApi.useExportPoliciesQuery({\n format: exportFormat,\n });\n\n const downloadFileName = `policies-${new Date().getTime()}`;\n\n if (isFetching) {\n return ;\n }\n\n return (\n \n );\n};\n\ninterface GrafanaPoliciesExporterProps {\n onClose: () => void;\n}\n\nexport const GrafanaPoliciesExporter = ({ onClose }: GrafanaPoliciesExporterProps) => {\n const [activeTab, setActiveTab] = useState('yaml');\n\n return (\n \n \n \n );\n};\n","import { css } from '@emotion/css';\nimport { defaults, groupBy, isArray, sumBy, uniqueId, upperFirst } from 'lodash';\nimport pluralize from 'pluralize';\nimport React, { FC, Fragment, ReactNode, useState } from 'react';\nimport { Link } from 'react-router-dom';\nimport { useToggle } from 'react-use';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { config } from '@grafana/runtime';\nimport {\n Badge,\n Button,\n Dropdown,\n Icon,\n IconButton,\n Menu,\n Stack,\n Text,\n Tooltip,\n getTagColorsFromName,\n useStyles2,\n} from '@grafana/ui';\nimport ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';\nimport {\n AlertmanagerGroup,\n MatcherOperator,\n ObjectMatcher,\n Receiver,\n RouteWithID,\n} from 'app/plugins/datasource/alertmanager/types';\nimport { ReceiversState } from 'app/types';\n\nimport { RoutesMatchingFilters } from '../../NotificationPolicies';\nimport { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';\nimport { INTEGRATION_ICONS } from '../../types/contact-points';\nimport { getAmMatcherFormatter } from '../../utils/alertmanager';\nimport { MatcherFormatter, normalizeMatchers } from '../../utils/matchers';\nimport { createContactPointLink, createMuteTimingLink } from '../../utils/misc';\nimport { InheritableProperties, getInheritedProperties } from '../../utils/notification-policies';\nimport { Authorize } from '../Authorize';\nimport { HoverCard } from '../HoverCard';\nimport { Label } from '../Label';\nimport { MetaText } from '../MetaText';\nimport { ProvisioningBadge } from '../Provisioning';\nimport { Spacer } from '../Spacer';\nimport { Strong } from '../Strong';\nimport { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter';\n\nimport { Matchers } from './Matchers';\nimport { TIMING_OPTIONS_DEFAULTS, TimingOptions } from './timingOptions';\n\ninterface PolicyComponentProps {\n receivers?: Receiver[];\n alertGroups?: AlertmanagerGroup[];\n contactPointsState?: ReceiversState;\n readOnly?: boolean;\n provisioned?: boolean;\n inheritedProperties?: Partial;\n routesMatchingFilters?: RoutesMatchingFilters;\n\n matchingInstancesPreview?: {\n groupsMap?: Map;\n enabled: boolean;\n };\n\n routeTree: RouteWithID;\n currentRoute: RouteWithID;\n alertManagerSourceName: string;\n onEditPolicy: (route: RouteWithID, isDefault?: boolean, isAutogenerated?: boolean) => void;\n onAddPolicy: (route: RouteWithID) => void;\n onDeletePolicy: (route: RouteWithID) => void;\n onShowAlertInstances: (\n alertGroups: AlertmanagerGroup[],\n matchers?: ObjectMatcher[],\n formatter?: MatcherFormatter\n ) => void;\n isAutoGenerated?: boolean;\n}\n\nconst Policy = (props: PolicyComponentProps) => {\n const {\n receivers = [],\n contactPointsState,\n readOnly = false,\n provisioned = false,\n alertGroups = [],\n alertManagerSourceName,\n currentRoute,\n routeTree,\n inheritedProperties,\n routesMatchingFilters = {\n filtersApplied: false,\n matchedRoutesWithPath: new Map(),\n },\n matchingInstancesPreview = { enabled: false },\n onEditPolicy,\n onAddPolicy,\n onDeletePolicy,\n onShowAlertInstances,\n isAutoGenerated = false,\n } = props;\n\n const styles = useStyles2(getStyles);\n\n const isDefaultPolicy = currentRoute === routeTree;\n\n const contactPoint = currentRoute.receiver;\n const continueMatching = currentRoute.continue ?? false;\n\n const matchers = normalizeMatchers(currentRoute);\n const hasMatchers = Boolean(matchers && matchers.length);\n\n const { filtersApplied, matchedRoutesWithPath } = routesMatchingFilters;\n const matchedRoutes = Array.from(matchedRoutesWithPath.keys());\n\n // check if this route matches the filters\n const hasFocus = filtersApplied && matchedRoutes.some((route) => route.id === currentRoute.id);\n\n // check if this route belongs to a path that matches the filters\n const routesPath = Array.from(matchedRoutesWithPath.values()).flat();\n const belongsToMatchPath = routesPath.some((route: RouteWithID) => route.id === currentRoute.id);\n\n // gather errors here\n const errors: ReactNode[] = [];\n\n // if the route has no matchers, is not the default policy (that one has none) and it does not continue\n // then we should warn the user that it's a suspicious setup\n const showMatchesAllLabelsWarning = !hasMatchers && !isDefaultPolicy && !continueMatching;\n\n // if the receiver / contact point has any errors show it on the policy\n const actualContactPoint = contactPoint ?? inheritedProperties?.receiver ?? '';\n const contactPointErrors = contactPointsState ? getContactPointErrors(actualContactPoint, contactPointsState) : [];\n\n const allChildPolicies = currentRoute.routes ?? [];\n\n // filter chld policies that match\n const childPolicies = filtersApplied\n ? // filter by the ones that belong to the path that matches the filters\n allChildPolicies.filter((policy) => routesPath.some((route: RouteWithID) => route.id === policy.id))\n : allChildPolicies;\n\n const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);\n const matchingAlertGroups = matchingInstancesPreview?.groupsMap?.get(currentRoute.id);\n\n // sum all alert instances for all groups we're handling\n const numberOfAlertInstances = matchingAlertGroups\n ? sumBy(matchingAlertGroups, (group) => group.alerts.length)\n : undefined;\n\n // simplified routing permissions\n const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility(\n AlertmanagerAction.ViewAutogeneratedPolicyTree\n );\n\n // collapsible policies variables\n const isThisPolicyCollapsible = useShouldPolicyBeCollapsible(currentRoute);\n const [isBranchOpen, toggleBranchOpen] = useToggle(false);\n const renderChildPolicies = (isThisPolicyCollapsible && isBranchOpen) || !isThisPolicyCollapsible;\n\n const groupBy = currentRoute.group_by;\n const muteTimings = currentRoute.mute_time_intervals ?? [];\n\n const timingOptions: TimingOptions = {\n group_wait: currentRoute.group_wait,\n group_interval: currentRoute.group_interval,\n repeat_interval: currentRoute.repeat_interval,\n };\n\n contactPointErrors.forEach((error) => {\n errors.push(error);\n });\n\n const POLICIES_PER_PAGE = 20;\n\n const [visibleChildPolicies, setVisibleChildPolicies] = useState(POLICIES_PER_PAGE);\n\n const isAutogeneratedPolicyRoot = isAutoGeneratedRootAndSimplifiedEnabled(currentRoute);\n\n // build the menu actions for our policy\n const dropdownMenuActions: JSX.Element[] = useCreateDropdownMenuActions(\n isAutoGenerated,\n isDefaultPolicy,\n provisioned,\n onEditPolicy,\n currentRoute,\n toggleShowExportDrawer,\n onDeletePolicy\n );\n\n // check if this policy should be visible. If it's autogenerated and the user is not allowed to see autogenerated\n // policies then we should not show it. Same if the user is not supported to see autogenerated policies.\n const hideCurrentPolicy =\n isAutoGenerated && (!isAllowedToSeeAutogeneratedChunk || !isSupportedToSeeAutogeneratedChunk);\n const hideCurrentPolicyForFilters = filtersApplied && !belongsToMatchPath;\n\n if (hideCurrentPolicy || hideCurrentPolicyForFilters) {\n return null;\n }\n\n const isImmutablePolicy = isDefaultPolicy || isAutogeneratedPolicyRoot;\n // TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated\n\n const childPoliciesBelongingToMatchPath = childPolicies.filter((child) =>\n routesPath.some((route: RouteWithID) => route.id === child.id)\n );\n\n // child policies to render are the ones that belong to the path that matches the filters\n const childPoliciesToRender = filtersApplied ? childPoliciesBelongingToMatchPath : childPolicies;\n const pageOfChildren = childPoliciesToRender.slice(0, visibleChildPolicies);\n\n const moreCount = childPoliciesToRender.length - pageOfChildren.length;\n const showMore = moreCount > 0;\n\n return (\n <>\n \n \n {/* continueMatching and showMatchesAllLabelsWarning are mutually exclusive so the icons can't overlap */}\n {continueMatching &&
}\n {showMatchesAllLabelsWarning &&
}\n\n
\n
\n {/* Matchers and actions */}\n \n
\n {isThisPolicyCollapsible && (\n \n )}\n {isImmutablePolicy ? (\n isAutogeneratedPolicyRoot ? (\n \n ) : (\n \n )\n ) : hasMatchers ? (\n \n ) : (\n No matchers\n )}\n \n {/* TODO maybe we should move errors to the gutter instead? */}\n {errors.length > 0 && }\n {provisioned && }\n \n {!isAutoGenerated && !readOnly && (\n \n \n \n \n \n )}\n {dropdownMenuActions.length > 0 && (\n {dropdownMenuActions}}>\n \n \n )}\n \n \n
\n\n {/* Metadata row */}\n \n \n
\n
\n \n {renderChildPolicies && (\n <>\n {pageOfChildren.map((child) => {\n const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);\n // This child is autogenerated if it's the autogenerated root or if it's a child of an autogenerated policy.\n const isThisChildAutoGenerated = isAutoGeneratedRootAndSimplifiedEnabled(child) || isAutoGenerated;\n /* pass the \"readOnly\" prop from the parent, because for any child policy , if its parent it's not editable,\n then the child policy should not be editable either */\n const isThisChildReadOnly = readOnly || provisioned || isAutoGenerated;\n\n return (\n
\n );\n })}\n {showMore && (\n
\n )}\n >\n )}\n
\n {showExportDrawer && }\n \n >\n );\n};\n/**\n * This function returns if the policy should be collapsible or not.\n * Add here conditions for policies that should be collapsible.\n */\nfunction useShouldPolicyBeCollapsible(route: RouteWithID): boolean {\n const childrenCount = route.routes?.length ?? 0;\n const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility(\n AlertmanagerAction.ViewAutogeneratedPolicyTree\n );\n const isAutoGeneratedRoot =\n childrenCount > 0 &&\n isSupportedToSeeAutogeneratedChunk &&\n isAllowedToSeeAutogeneratedChunk &&\n isAutoGeneratedRootAndSimplifiedEnabled(route);\n // let's add here more conditions for policies that should be collapsible\n\n return isAutoGeneratedRoot;\n}\n\ninterface MetadataRowProps {\n matchingInstancesPreview: { groupsMap?: Map; enabled: boolean };\n numberOfAlertInstances?: number;\n contactPoint?: string;\n groupBy?: string[];\n muteTimings?: string[];\n timingOptions?: TimingOptions;\n inheritedProperties?: Partial;\n alertManagerSourceName: string;\n receivers: Receiver[];\n matchingAlertGroups?: AlertmanagerGroup[];\n matchers?: ObjectMatcher[];\n isDefaultPolicy: boolean;\n onShowAlertInstances: (\n alertGroups: AlertmanagerGroup[],\n matchers?: ObjectMatcher[],\n formatter?: MatcherFormatter\n ) => void;\n}\n\nfunction MetadataRow({\n numberOfAlertInstances,\n isDefaultPolicy,\n timingOptions,\n groupBy,\n muteTimings = [],\n matchingInstancesPreview,\n inheritedProperties,\n matchingAlertGroups,\n onShowAlertInstances,\n matchers,\n contactPoint,\n alertManagerSourceName,\n receivers,\n}: MetadataRowProps) {\n const styles = useStyles2(getStyles);\n\n const inheritedGrouping = inheritedProperties && inheritedProperties.group_by;\n const hasInheritedProperties = inheritedProperties && Object.keys(inheritedProperties).length > 0;\n\n const noGrouping = isArray(groupBy) && groupBy[0] === '...';\n const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0;\n const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0;\n\n const hasMuteTimings = Boolean(muteTimings.length);\n\n return (\n \n \n {matchingInstancesPreview.enabled && (\n {\n matchingAlertGroups &&\n onShowAlertInstances(matchingAlertGroups, matchers, getAmMatcherFormatter(alertManagerSourceName));\n }}\n data-testid=\"matching-instances\"\n >\n {numberOfAlertInstances ?? '-'}\n {pluralize('instance', numberOfAlertInstances)}\n \n )}\n {contactPoint && (\n \n Delivered to\n \n \n )}\n {!inheritedGrouping && (\n <>\n {customGrouping && (\n \n Grouped by\n {groupBy.join(', ')}\n \n )}\n {singleGroup && (\n \n Single group\n \n )}\n {noGrouping && (\n \n Not grouping\n \n )}\n >\n )}\n {hasMuteTimings && (\n \n Muted when\n \n \n )}\n {timingOptions && (\n // for the default policy we will also merge the default timings, that way a user can observe what the timing options would be\n \n )}\n {hasInheritedProperties && (\n <>\n \n Inherited\n \n \n >\n )}\n \n
\n );\n}\n\nexport const useCreateDropdownMenuActions = (\n isAutoGenerated: boolean,\n isDefaultPolicy: boolean,\n provisioned: boolean,\n onEditPolicy: (route: RouteWithID, isDefault?: boolean, readOnly?: boolean) => void,\n currentRoute: RouteWithID,\n toggleShowExportDrawer: (nextValue?: any) => void,\n onDeletePolicy: (route: RouteWithID) => void\n) => {\n const [\n [updatePoliciesSupported, updatePoliciesAllowed],\n [deletePolicySupported, deletePolicyAllowed],\n [exportPoliciesSupported, exportPoliciesAllowed],\n ] = useAlertmanagerAbilities([\n AlertmanagerAction.UpdateNotificationPolicyTree,\n AlertmanagerAction.DeleteNotificationPolicy,\n AlertmanagerAction.ExportNotificationPolicies,\n ]);\n const dropdownMenuActions = [];\n const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy && !isAutoGenerated;\n const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;\n const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy && !isAutoGenerated;\n\n if (showEditAction) {\n dropdownMenuActions.push(\n \n \n onEditPolicy(currentRoute, isDefaultPolicy)}\n />\n \n \n );\n }\n\n if (showExportAction) {\n dropdownMenuActions.push(\n \n );\n }\n\n if (showDeleteAction) {\n dropdownMenuActions.push(\n \n \n \n onDeletePolicy(currentRoute)}\n />\n \n \n );\n }\n return dropdownMenuActions;\n};\n\nexport const AUTOGENERATED_ROOT_LABEL_NAME = '__grafana_autogenerated__';\n\nexport function isAutoGeneratedRootAndSimplifiedEnabled(route: RouteWithID) {\n const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;\n if (!simplifiedRoutingToggleEnabled) {\n return false;\n }\n if (!route.object_matchers) {\n return false;\n }\n return (\n route.object_matchers.some((objectMatcher) => {\n return (\n objectMatcher[0] === AUTOGENERATED_ROOT_LABEL_NAME &&\n objectMatcher[1] === MatcherOperator.equal &&\n objectMatcher[2] === 'true'\n );\n }) ?? false\n );\n // return simplifiedRoutingToggleEnabled && route.receiver === 'contact_point_5';\n}\n\nconst ProvisionedTooltip = (children: ReactNode) => (\n \n {children}\n \n);\n\nconst Errors: FC<{ errors: React.ReactNode[] }> = ({ errors }) => (\n \n {errors.map((error) => (\n {error}\n ))}\n \n }\n >\n \n \n \n \n);\n\nconst ContinueMatchingIndicator: FC = () => {\n const styles = useStyles2(getStyles);\n return (\n \n \n \n
\n \n );\n};\n\nconst AllMatchesIndicator: FC = () => {\n const styles = useStyles2(getStyles);\n return (\n \n \n \n
\n \n );\n};\n\nfunction DefaultPolicyIndicator() {\n const styles = useStyles2(getStyles);\n return (\n <>\n Default policy\n \n All alert instances will be handled by the default policy if no other matching policies are found.\n \n >\n );\n}\n\nfunction AutogeneratedRootIndicator() {\n return Auto-generated policies;\n}\n\nconst InheritedProperties: FC<{ properties: InheritableProperties }> = ({ properties }) => (\n \n {Object.entries(properties).map(([key, value]) => {\n if (!value) {\n return null;\n }\n\n return (\n \n);\n\nconst MuteTimings: FC<{ timings: string[]; alertManagerSourceName: string }> = ({\n timings,\n alertManagerSourceName,\n}) => {\n /* TODO make a better mute timing overview, allow combining multiple in to one overview */\n /*\n Mute Timings}\n content={\n // TODO show a combined view of all mute timings here, combining the weekdays, years, months, etc\n \n \n \n }\n >\n \n {muteTimings.join(', ')}\n
\n \n */\n return (\n \n \n {timings.map((timing) => (\n \n {timing}\n \n ))}\n \n
\n );\n};\n\nconst TimingOptionsMeta: FC<{ timingOptions: TimingOptions }> = ({ timingOptions }) => {\n const groupWait = timingOptions.group_wait;\n const groupInterval = timingOptions.group_interval;\n\n // we don't have any timing options to show – we're inheriting everything from the parent\n // and those show up in a separate \"inherited properties\" component\n if (!groupWait && !groupInterval) {\n return null;\n }\n\n return (\n \n Wait\n {groupWait && (\n \n \n {groupWait} to group instances\n {groupWait && groupInterval && ','}\n \n \n )}\n {groupInterval && (\n \n \n {groupInterval} before sending updates\n \n \n )}\n \n );\n};\n\ninterface ContactPointDetailsProps {\n alertManagerSourceName: string;\n contactPoint: string;\n receivers: Receiver[];\n}\n\n// @TODO make this work for cloud AMs too\nconst ContactPointsHoverDetails: FC = ({\n alertManagerSourceName,\n contactPoint,\n receivers,\n}) => {\n const details = receivers.find((receiver) => receiver.name === contactPoint);\n if (!details) {\n return (\n \n {contactPoint}\n \n );\n }\n\n const integrations = details.grafana_managed_receiver_configs;\n if (!integrations) {\n return (\n \n {contactPoint}\n \n );\n }\n\n const groupedIntegrations = groupBy(details.grafana_managed_receiver_configs, (config) => config.type);\n\n return (\n \n Contact Point
\n {contactPoint}\n \n }\n key={uniqueId()}\n content={\n \n {/* use \"label\" to indicate how many of that type we have in the contact point */}\n {Object.entries(groupedIntegrations).map(([type, integrations]) => (\n \n }\n >\n \n {contactPoint}\n \n \n );\n};\n\nfunction getContactPointErrors(contactPoint: string, contactPointsState: ReceiversState): JSX.Element[] {\n const notifierStates = Object.entries(contactPointsState[contactPoint]?.notifiers ?? []);\n const contactPointErrors = notifierStates.reduce((acc: JSX.Element[] = [], [_, notifierStatuses]) => {\n const notifierErrors = notifierStatuses\n .filter((status) => status.lastNotifyAttemptError)\n .map((status) => (\n \n ));\n\n return acc.concat(notifierErrors);\n }, []);\n\n return contactPointErrors;\n}\n\nconst routePropertyToLabel = (key: keyof InheritableProperties | string): string => {\n switch (key) {\n case 'receiver':\n return 'Contact Point';\n case 'group_by':\n return 'Group by';\n case 'group_interval':\n return 'Group interval';\n case 'group_wait':\n return 'Group wait';\n case 'repeat_interval':\n return 'Repeat interval';\n default:\n return key;\n }\n};\n\nconst routePropertyToValue = (key: keyof InheritableProperties | string, value: string | string[]): React.ReactNode => {\n const isNotGrouping = key === 'group_by' && Array.isArray(value) && value[0] === '...';\n const isSingleGroup = key === 'group_by' && Array.isArray(value) && value.length === 0;\n\n if (isNotGrouping) {\n return (\n \n Not grouping\n \n );\n }\n\n if (isSingleGroup) {\n return (\n \n Single group\n \n );\n }\n\n return Array.isArray(value) ? value.join(', ') : value;\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n matcher: (label: string) => {\n const { color, borderColor } = getTagColorsFromName(label);\n\n return {\n wrapper: css({\n color: '#fff',\n background: color,\n padding: `${theme.spacing(0.33)} ${theme.spacing(0.66)}`,\n fontSize: theme.typography.bodySmall.fontSize,\n border: `solid 1px ${borderColor}`,\n borderRadius: theme.shape.radius.default,\n }),\n };\n },\n childPolicies: css({\n marginLeft: theme.spacing(4),\n position: 'relative',\n '&:before': {\n content: '\"\"',\n position: 'absolute',\n height: 'calc(100% - 10px)',\n borderLeft: `solid 1px ${theme.colors.border.weak}`,\n marginTop: 0,\n marginLeft: '-20px',\n },\n }),\n policyItemWrapper: css({\n padding: theme.spacing(1.5),\n }),\n metadataRow: css({\n borderBottomLeftRadius: theme.shape.borderRadius(2),\n borderBottomRightRadius: theme.shape.borderRadius(2),\n }),\n policyWrapper: (hasFocus = false) =>\n css({\n flex: 1,\n position: 'relative',\n background: theme.colors.background.secondary,\n borderRadius: theme.shape.radius.default,\n border: `solid 1px ${theme.colors.border.weak}`,\n ...(hasFocus && {\n borderColor: theme.colors.primary.border,\n background: theme.colors.primary.transparent,\n }),\n }),\n metadata: css({\n color: theme.colors.text.secondary,\n fontSize: theme.typography.bodySmall.fontSize,\n fontWeight: theme.typography.bodySmall.fontWeight,\n }),\n break: css({\n width: '100%',\n height: 0,\n marginBottom: theme.spacing(2),\n }),\n gutterIcon: css({\n position: 'absolute',\n top: 0,\n transform: 'translateY(50%)',\n left: `-${theme.spacing(4)}`,\n color: theme.colors.text.secondary,\n background: theme.colors.background.primary,\n width: '25px',\n height: '25px',\n textAlign: 'center',\n border: `solid 1px ${theme.colors.border.weak}`,\n borderRadius: theme.shape.radius.default,\n padding: 0,\n }),\n moreButtons: css({\n marginTop: theme.spacing(0.5),\n marginBottom: theme.spacing(1.5),\n }),\n});\n\nexport { Policy };\n","import { css } from '@emotion/css';\nimport React, { useEffect, useMemo, useState } from 'react';\nimport { useAsyncFn } from 'react-use';\n\nimport { GrafanaTheme2, UrlQueryMap } from '@grafana/data';\nimport { Alert, LoadingPlaceholder, Stack, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui';\nimport { useQueryParams } from 'app/core/hooks/useQueryParams';\nimport { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';\nimport { useDispatch } from 'app/types';\n\nimport { useCleanup } from '../../../core/hooks/useCleanup';\n\nimport { alertmanagerApi } from './api/alertmanagerApi';\nimport { useGetContactPointsState } from './api/receiversApi';\nimport { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';\nimport { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';\nimport { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable';\nimport {\n NotificationPoliciesFilter,\n findRoutesByMatchers,\n findRoutesMatchingPredicate,\n} from './components/notification-policies/Filters';\nimport {\n useAddPolicyModal,\n useAlertGroupsModal,\n useDeletePolicyModal,\n useEditPolicyModal,\n} from './components/notification-policies/Modals';\nimport { Policy } from './components/notification-policies/Policy';\nimport { useAlertmanagerConfig } from './hooks/useAlertmanagerConfig';\nimport { useAlertmanager } from './state/AlertmanagerContext';\nimport { updateAlertManagerConfigAction } from './state/actions';\nimport { FormAmRoute } from './types/amroutes';\nimport { useRouteGroupsMatcher } from './useRouteGroupsMatcher';\nimport { addUniqueIdentifierToRoute } from './utils/amroutes';\nimport { computeInheritedTree } from './utils/notification-policies';\nimport { initialAsyncRequestState } from './utils/redux';\nimport { addRouteToParentRoute, mergePartialAmRouteWithRouteTree, omitRouteFromRouteTree } from './utils/routeTree';\n\nenum ActiveTab {\n NotificationPolicies = 'notification_policies',\n MuteTimings = 'mute_timings',\n}\n\nconst AmRoutes = () => {\n const dispatch = useDispatch();\n const styles = useStyles2(getStyles);\n\n const { useGetAlertmanagerAlertGroupsQuery } = alertmanagerApi;\n\n const [queryParams, setQueryParams] = useQueryParams();\n const { tab } = getActiveTabFromUrl(queryParams);\n\n const [activeTab, setActiveTab] = useState(tab);\n const [updatingTree, setUpdatingTree] = useState(false);\n const [contactPointFilter, setContactPointFilter] = useState();\n const [labelMatchersFilter, setLabelMatchersFilter] = useState([]);\n\n const { selectedAlertmanager, hasConfigurationAPI, isGrafanaAlertmanager } = useAlertmanager();\n const { getRouteGroupsMap } = useRouteGroupsMatcher();\n\n const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? '');\n\n const {\n currentData: result,\n isLoading: resultLoading,\n error: resultError,\n } = useAlertmanagerConfig(selectedAlertmanager, {\n refetchOnFocus: true,\n refetchOnReconnect: true,\n });\n\n const config = result?.alertmanager_config;\n\n const { currentData: alertGroups, refetch: refetchAlertGroups } = useGetAlertmanagerAlertGroupsQuery(\n { amSourceName: selectedAlertmanager ?? '' },\n { skip: !selectedAlertmanager }\n );\n\n const receivers = config?.receivers ?? [];\n\n const rootRoute = useMemo(() => {\n if (config?.route) {\n return addUniqueIdentifierToRoute(config.route);\n }\n return;\n }, [config?.route]);\n\n // useAsync could also work but it's hard to wait until it's done in the tests\n // Combining with useEffect gives more predictable results because the condition is in useEffect\n const [{ value: routeAlertGroupsMap, error: instancesPreviewError }, triggerGetRouteGroupsMap] = useAsyncFn(\n getRouteGroupsMap,\n [getRouteGroupsMap]\n );\n\n useEffect(() => {\n if (rootRoute && alertGroups) {\n triggerGetRouteGroupsMap(rootRoute, alertGroups, { unquoteMatchers: !isGrafanaAlertmanager });\n }\n }, [rootRoute, alertGroups, triggerGetRouteGroupsMap, isGrafanaAlertmanager]);\n\n // these are computed from the contactPoint and labels matchers filter\n const routesMatchingFilters = useMemo(() => {\n if (!rootRoute) {\n const emptyResult: RoutesMatchingFilters = {\n filtersApplied: false,\n matchedRoutesWithPath: new Map(),\n };\n\n return emptyResult;\n }\n\n return findRoutesMatchingFilters(rootRoute, { contactPointFilter, labelMatchersFilter });\n }, [contactPointFilter, labelMatchersFilter, rootRoute]);\n\n const isProvisioned = Boolean(config?.route?.provenance);\n\n function handleSave(partialRoute: Partial) {\n if (!rootRoute) {\n return;\n }\n const newRouteTree = mergePartialAmRouteWithRouteTree(selectedAlertmanager ?? '', partialRoute, rootRoute);\n updateRouteTree(newRouteTree);\n }\n\n function handleDelete(route: RouteWithID) {\n if (!rootRoute) {\n return;\n }\n const newRouteTree = omitRouteFromRouteTree(route, rootRoute);\n updateRouteTree(newRouteTree);\n }\n\n function handleAdd(partialRoute: Partial, parentRoute: RouteWithID) {\n if (!rootRoute) {\n return;\n }\n\n const newRouteTree = addRouteToParentRoute(selectedAlertmanager ?? '', partialRoute, parentRoute, rootRoute);\n updateRouteTree(newRouteTree);\n }\n\n function updateRouteTree(routeTree: Route) {\n if (!result) {\n return;\n }\n\n setUpdatingTree(true);\n\n dispatch(\n updateAlertManagerConfigAction({\n newConfig: {\n ...result,\n alertmanager_config: {\n ...result.alertmanager_config,\n route: routeTree,\n },\n },\n oldConfig: result,\n alertManagerSourceName: selectedAlertmanager!,\n successMessage: 'Updated notification policies',\n })\n )\n .unwrap()\n .then(() => {\n if (selectedAlertmanager) {\n refetchAlertGroups();\n }\n closeEditModal();\n closeAddModal();\n closeDeleteModal();\n })\n .finally(() => {\n setUpdatingTree(false);\n });\n }\n\n // edit, add, delete modals\n const [addModal, openAddModal, closeAddModal] = useAddPolicyModal(receivers, handleAdd, updatingTree);\n const [editModal, openEditModal, closeEditModal] = useEditPolicyModal(\n selectedAlertmanager ?? '',\n receivers,\n handleSave,\n updatingTree\n );\n const [deleteModal, openDeleteModal, closeDeleteModal] = useDeletePolicyModal(handleDelete, updatingTree);\n const [alertInstancesModal, showAlertGroupsModal] = useAlertGroupsModal();\n\n useCleanup((state) => (state.unifiedAlerting.saveAMConfig = initialAsyncRequestState));\n\n if (!selectedAlertmanager) {\n return null;\n }\n\n const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0;\n const haveData = result && !resultError && !resultLoading;\n const isFetching = !result && resultLoading;\n const haveError = resultError && !resultLoading;\n\n const muteTimingsTabActive = activeTab === ActiveTab.MuteTimings;\n const policyTreeTabActive = activeTab === ActiveTab.NotificationPolicies;\n\n return (\n <>\n \n {\n setActiveTab(ActiveTab.NotificationPolicies);\n setQueryParams({ tab: ActiveTab.NotificationPolicies });\n }}\n />\n {\n setActiveTab(ActiveTab.MuteTimings);\n setQueryParams({ tab: ActiveTab.MuteTimings });\n }}\n />\n \n \n {isFetching && }\n {haveError && (\n \n {resultError.message || 'Unknown error.'}\n \n )}\n {haveData && (\n <>\n {policyTreeTabActive && (\n <>\n \n \n {rootRoute && (\n \n )}\n {rootRoute && (\n \n )}\n \n {addModal}\n {editModal}\n {deleteModal}\n {alertInstancesModal}\n >\n )}\n {muteTimingsTabActive && (\n \n )}\n >\n )}\n \n >\n );\n};\n\ntype RouteFilters = {\n contactPointFilter?: string;\n labelMatchersFilter?: ObjectMatcher[];\n};\n\ntype FilterResult = Map;\n\nexport interface RoutesMatchingFilters {\n filtersApplied: boolean;\n matchedRoutesWithPath: FilterResult;\n}\n\nexport const findRoutesMatchingFilters = (rootRoute: RouteWithID, filters: RouteFilters): RoutesMatchingFilters => {\n const { contactPointFilter, labelMatchersFilter = [] } = filters;\n const hasFilter = contactPointFilter || labelMatchersFilter.length > 0;\n const havebothFilters = Boolean(contactPointFilter) && labelMatchersFilter.length > 0;\n\n // if filters are empty we short-circuit this function\n if (!hasFilter) {\n return { filtersApplied: false, matchedRoutesWithPath: new Map() };\n }\n\n // we'll collect all of the routes matching the filters\n // we track an array of matching routes, each item in the array is for 1 type of filter\n //\n // [contactPointMatches, labelMatcherMatches] -> [[{ a: [], b: [] }], [{ a: [], c: [] }]]\n // later we'll use intersection to find results in all sets of filter matchers\n let matchedRoutes: RouteWithID[][] = [];\n\n // compute fully inherited tree so all policies have their inherited receiver\n const fullRoute = computeInheritedTree(rootRoute);\n\n // find all routes for our contact point filter\n const matchingRoutesForContactPoint = contactPointFilter\n ? findRoutesMatchingPredicate(fullRoute, (route) => route.receiver === contactPointFilter)\n : new Map();\n\n const routesMatchingContactPoint = Array.from(matchingRoutesForContactPoint.keys());\n if (routesMatchingContactPoint) {\n matchedRoutes.push(routesMatchingContactPoint);\n }\n\n // find all routes matching our label matchers\n const matchingRoutesForLabelMatchers = labelMatchersFilter.length\n ? findRoutesMatchingPredicate(fullRoute, (route) => findRoutesByMatchers(route, labelMatchersFilter))\n : new Map();\n\n const routesMatchingLabelFilters = Array.from(matchingRoutesForLabelMatchers.keys());\n if (matchingRoutesForLabelMatchers.size > 0) {\n matchedRoutes.push(routesMatchingLabelFilters);\n }\n\n // now that we have our maps for all filters, we just need to find the intersection of all maps by route if we have both filters\n const routesForAllFilterResults = havebothFilters\n ? findMapIntersection(matchingRoutesForLabelMatchers, matchingRoutesForContactPoint)\n : new Map([...matchingRoutesForLabelMatchers, ...matchingRoutesForContactPoint]);\n\n return {\n filtersApplied: true,\n matchedRoutesWithPath: routesForAllFilterResults,\n };\n};\n\n// this function takes multiple maps and creates a new map with routes that exist in all maps\n//\n// map 1: { a: [], b: [] }\n// map 2: { a: [], c: [] }\n// return: { a: [] }\nfunction findMapIntersection(...matchingRoutes: FilterResult[]): FilterResult {\n const result = new Map();\n\n // Iterate through the keys of the first map'\n for (const key of matchingRoutes[0].keys()) {\n // Check if the key exists in all other maps\n if (matchingRoutes.every((map) => map.has(key))) {\n // If yes, add the key to the result map\n // @ts-ignore\n result.set(key, matchingRoutes[0].get(key));\n }\n }\n\n return result;\n}\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n tabContent: css`\n margin-top: ${theme.spacing(2)};\n `,\n});\n\ninterface QueryParamValues {\n tab: ActiveTab;\n}\n\nfunction getActiveTabFromUrl(queryParams: UrlQueryMap): QueryParamValues {\n let tab = ActiveTab.NotificationPolicies; // default tab\n\n if (queryParams['tab'] === ActiveTab.NotificationPolicies) {\n tab = ActiveTab.NotificationPolicies;\n }\n\n if (queryParams['tab'] === ActiveTab.MuteTimings) {\n tab = ActiveTab.MuteTimings;\n }\n\n return {\n tab,\n };\n}\n\nconst NotificationPoliciesPage = () => (\n \n \n \n);\n\nexport default withErrorBoundary(NotificationPoliciesPage, { style: 'page' });\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { useStyles2 } from '@grafana/ui';\n\nexport const EmptyArea = ({ children }: React.PropsWithChildren<{}>) => {\n const styles = useStyles2(getStyles);\n\n return {children}
;\n};\n\nconst getStyles = (theme: GrafanaTheme2) => {\n return {\n container: css`\n background-color: ${theme.colors.background.secondary};\n color: ${theme.colors.text.secondary};\n padding: ${theme.spacing(4)};\n text-align: center;\n `,\n };\n};\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { LinkButton, useStyles2 } from '@grafana/ui';\nimport { contextSrv } from 'app/core/services/context_srv';\nimport { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';\nimport { AccessControlAction } from 'app/types';\n\nimport { AlertmanagerAction } from '../../hooks/useAbilities';\nimport { isGrafanaRulesSource } from '../../utils/datasource';\nimport { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';\nimport { AnnotationDetailsField } from '../AnnotationDetailsField';\nimport { Authorize } from '../Authorize';\n\ninterface AmNotificationsAlertDetailsProps {\n alertManagerSourceName: string;\n alert: AlertmanagerAlert;\n}\n\nexport const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsAlertDetailsProps) => {\n const styles = useStyles2(getStyles);\n\n // For Grafana Managed alerts the Generator URL redirects to the alert rule edit page, so update permission is required\n // For external alert manager the Generator URL redirects to an external service which we don't control\n const isGrafanaSource = isGrafanaRulesSource(alertManagerSourceName);\n const isSeeSourceButtonEnabled = isGrafanaSource\n ? contextSrv.hasPermission(AccessControlAction.AlertingRuleRead)\n : true;\n\n return (\n <>\n \n {alert.status.state === AlertState.Suppressed && (\n
\n \n Manage silences\n \n \n )}\n {alert.status.state === AlertState.Active && (\n
\n \n Silence\n \n \n )}\n {isSeeSourceButtonEnabled && alert.generatorURL && (\n
\n See source\n \n )}\n
\n {Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => (\n \n ))}\n \n Receivers:{' '}\n {alert.receivers\n .map(({ name }) => name)\n .filter((name) => !!name)\n .join(', ')}\n
\n >\n );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n button: css`\n & + & {\n margin-left: ${theme.spacing(1)};\n }\n `,\n actionsRow: css`\n padding: ${theme.spacing(2, 0)} !important;\n border-bottom: 1px solid ${theme.colors.border.medium};\n `,\n receivers: css`\n padding: ${theme.spacing(1, 0)};\n `,\n});\n","import { css } from '@emotion/css';\nimport React, { useMemo } from 'react';\n\nimport { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';\nimport { useStyles2 } from '@grafana/ui';\nimport { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';\n\nimport { AlertLabels } from '../AlertLabels';\nimport { DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';\nimport { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';\nimport { AmAlertStateTag } from '../silences/AmAlertStateTag';\n\nimport { AlertDetails } from './AlertDetails';\n\ninterface Props {\n alerts: AlertmanagerAlert[];\n alertManagerSourceName: string;\n}\n\ntype AlertGroupAlertsTableColumnProps = DynamicTableColumnProps;\ntype AlertGroupAlertsTableItemProps = DynamicTableItemProps;\n\nexport const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) => {\n const styles = useStyles2(getStyles);\n\n const columns = useMemo(\n (): AlertGroupAlertsTableColumnProps[] => [\n {\n id: 'state',\n label: 'State',\n // eslint-disable-next-line react/display-name\n renderCell: ({ data: alert }) => (\n <>\n \n \n for{' '}\n {intervalToAbbreviatedDurationString({\n start: new Date(alert.startsAt),\n end: new Date(alert.endsAt),\n })}\n \n >\n ),\n size: '220px',\n },\n {\n id: 'labels',\n label: 'Labels',\n // eslint-disable-next-line react/display-name\n renderCell: ({ data: { labels } }) => ,\n size: 1,\n },\n ],\n [styles]\n );\n\n const items = useMemo(\n (): AlertGroupAlertsTableItemProps[] =>\n alerts.map((alert) => ({\n id: alert.fingerprint,\n data: alert,\n })),\n [alerts]\n );\n\n return (\n \n );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n tableWrapper: css`\n margin-top: ${theme.spacing(3)};\n ${theme.breakpoints.up('md')} {\n margin-left: ${theme.spacing(4.5)};\n }\n `,\n duration: css`\n margin-left: ${theme.spacing(1)};\n font-size: ${theme.typography.bodySmall.fontSize};\n `,\n});\n","import { css } from '@emotion/css';\nimport React, { useState } from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { useStyles2, Stack } from '@grafana/ui';\nimport { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types';\n\nimport { AlertLabels } from '../AlertLabels';\nimport { CollapseToggle } from '../CollapseToggle';\nimport { MetaText } from '../MetaText';\nimport { Strong } from '../Strong';\n\nimport { AlertGroupAlertsTable } from './AlertGroupAlertsTable';\nimport { AlertGroupHeader } from './AlertGroupHeader';\n\ninterface Props {\n group: AlertmanagerGroup;\n alertManagerSourceName: string;\n}\n\nexport const AlertGroup = ({ alertManagerSourceName, group }: Props) => {\n const [isCollapsed, setIsCollapsed] = useState(true);\n const styles = useStyles2(getStyles);\n // When group is grouped, receiver.name is 'NONE' as it can contain multiple receivers\n const receiverInGroup = group.receiver.name !== 'NONE';\n return (\n \n
\n
\n
setIsCollapsed(!isCollapsed)}\n data-testid=\"alert-group-collapse-toggle\"\n />\n {Object.keys(group.labels).length ? (\n \n \n {receiverInGroup && (\n \n Delivered to {group.receiver.name}\n \n )}\n \n ) : (\n No grouping\n )}\n \n
\n
\n {!isCollapsed &&
}\n
\n );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n wrapper: css({\n '& + &': {\n marginTop: theme.spacing(2),\n },\n }),\n header: css({\n display: 'flex',\n flexDirection: 'row',\n flexWrap: 'wrap',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`,\n backgroundColor: theme.colors.background.secondary,\n width: '100%',\n }),\n group: css({\n display: 'flex',\n flexDirection: 'row',\n alignItems: 'center',\n }),\n summary: css({}),\n [AlertState.Active]: css({\n color: theme.colors.error.main,\n }),\n [AlertState.Suppressed]: css({\n color: theme.colors.primary.main,\n }),\n [AlertState.Unprocessed]: css({\n color: theme.colors.secondary.main,\n }),\n});\n","import moment from 'moment';\nimport React from 'react';\n\nimport { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';\n\nimport {\n getDaysOfMonthString,\n getMonthsString,\n getTimeString,\n getWeekdayString,\n getYearsString,\n} from '../../utils/alertmanager';\n\n// https://github.com/prometheus/alertmanager/blob/9de8ef36755298a68b6ab20244d4369d38bdea99/timeinterval/timeinterval.go#L443\nconst TIME_RANGE_REGEX = /^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)/;\n\nconst isvalidTimeFormat = (timeString: string): boolean => {\n return timeString ? TIME_RANGE_REGEX.test(timeString) : true;\n};\n\nconst isValidStartAndEndTime = (startTime?: string, endTime?: string): boolean => {\n // empty time range is perfactly valid for a mute timing\n if (!startTime && !endTime) {\n return true;\n }\n\n if ((!startTime && endTime) || (startTime && !endTime)) {\n return false;\n }\n\n const timeUnit = 'HH:mm';\n // @ts-ignore typescript types here incorrect, sigh\n const startDate = moment().startOf('day').add(startTime, timeUnit);\n // @ts-ignore typescript types here incorrect, sigh\n const endDate = moment().startOf('day').add(endTime, timeUnit);\n\n if (startTime && endTime && startDate.isBefore(endDate)) {\n return true;\n }\n\n if (startTime && endTime && endDate.isAfter(startDate)) {\n return true;\n }\n\n return false;\n};\n\nfunction renderTimeIntervals(muteTiming: MuteTimeInterval) {\n const timeIntervals = muteTiming.time_intervals;\n\n return timeIntervals.map((interval, index) => {\n const { times, weekdays, days_of_month, months, years, location } = interval;\n const timeString = getTimeString(times, location);\n const weekdayString = getWeekdayString(weekdays);\n const daysString = getDaysOfMonthString(days_of_month);\n const monthsString = getMonthsString(months);\n const yearsString = getYearsString(years);\n\n return (\n \n {`${timeString} ${weekdayString}`}\n
\n {[daysString, monthsString, yearsString].join(' | ')}\n
\n \n );\n });\n}\n\nexport { isvalidTimeFormat, isValidStartAndEndTime, renderTimeIntervals };\n","import { css } from '@emotion/css';\nimport { take, takeRight, uniqueId } from 'lodash';\nimport React, { FC } from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { getTagColorsFromName, useStyles2, Stack } from '@grafana/ui';\nimport { ObjectMatcher } from 'app/plugins/datasource/alertmanager/types';\n\nimport { MatcherFormatter, matcherFormatter } from '../../utils/matchers';\nimport { HoverCard } from '../HoverCard';\n\ntype MatchersProps = { matchers: ObjectMatcher[]; formatter?: MatcherFormatter };\n\n// renders the first N number of matchers\nconst Matchers: FC = ({ matchers, formatter = 'default' }) => {\n const styles = useStyles2(getStyles);\n\n const NUM_MATCHERS = 5;\n\n const firstFew = take(matchers, NUM_MATCHERS);\n const rest = takeRight(matchers, matchers.length - NUM_MATCHERS);\n const hasMoreMatchers = rest.length > 0;\n\n return (\n \n \n {firstFew.map((matcher) => (\n \n ))}\n {/* TODO hover state to show all matchers we're not showing */}\n {hasMoreMatchers && (\n \n {rest.map((matcher) => (\n \n ))}\n >\n }\n >\n \n {`and ${rest.length} more`}
\n \n \n )}\n \n \n );\n};\n\ninterface MatcherBadgeProps {\n matcher: ObjectMatcher;\n formatter?: MatcherFormatter;\n}\n\nconst MatcherBadge: FC = ({ matcher, formatter = 'default' }) => {\n const styles = useStyles2(getStyles);\n\n return (\n \n \n {matcherFormatter[formatter](matcher)}\n \n
\n );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n matcher: (label: string) => {\n const { color, borderColor } = getTagColorsFromName(label);\n\n return {\n wrapper: css`\n color: #fff;\n background: ${color};\n padding: ${theme.spacing(0.33)} ${theme.spacing(0.66)};\n font-size: ${theme.typography.bodySmall.fontSize};\n\n border: solid 1px ${borderColor};\n border-radius: ${theme.shape.borderRadius(2)};\n `,\n };\n },\n metadata: css`\n color: ${theme.colors.text.secondary};\n\n font-size: ${theme.typography.bodySmall.fontSize};\n font-weight: ${theme.typography.bodySmall.fontWeight};\n `,\n});\n\nexport { Matchers };\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { useStyles2 } from '@grafana/ui';\n\nimport { TimeOptions } from '../../types/time';\n\nexport function PromDurationDocs() {\n const styles = useStyles2(getPromDurationStyles);\n return (\n \n Prometheus duration format consist of a number followed by a time unit.\n
\n Different units can be combined for more granularity.\n
\n
\n
\n
Symbol
\n
Time unit
\n
Example
\n
\n
\n
\n
\n
\n
\n
\n
Multiple units combined
\n
1m30s, 2h30m20s, 1w2d
\n
\n
\n
\n );\n}\n\nfunction PromDurationDocsTimeUnit({ unit, name, example }: { unit: TimeOptions; name: string; example: string }) {\n const styles = useStyles2(getPromDurationStyles);\n\n return (\n <>\n {unit}
\n {name}
\n {example}
\n >\n );\n}\n\nconst getPromDurationStyles = (theme: GrafanaTheme2) => ({\n unit: css`\n font-weight: ${theme.typography.fontWeightBold};\n `,\n list: css`\n display: grid;\n grid-template-columns: max-content 1fr 2fr;\n gap: ${theme.spacing(1, 3)};\n `,\n header: css`\n display: contents;\n font-weight: ${theme.typography.fontWeightBold};\n `,\n examples: css`\n display: contents;\n & > div {\n grid-column: 1 / span 2;\n }\n `,\n});\n","import React from 'react';\n\nimport { Icon, Input } from '@grafana/ui';\n\nimport { HoverCard } from '../HoverCard';\n\nimport { PromDurationDocs } from './PromDurationDocs';\n\nexport const PromDurationInput = React.forwardRef>(\n (props, ref) => {\n return (\n } disabled={false}>\n \n \n }\n {...props}\n ref={ref}\n />\n );\n }\n);\n\nPromDurationInput.displayName = 'PromDurationInput';\n","import { css } from '@emotion/css';\n\nimport { GrafanaTheme2 } from '@grafana/data';\n\nexport const getFormStyles = (theme: GrafanaTheme2) => {\n return {\n container: css`\n align-items: center;\n display: flex;\n flex-flow: row nowrap;\n\n & > * + * {\n margin-left: ${theme.spacing(1)};\n }\n `,\n input: css`\n flex: 1;\n `,\n promDurationInput: css`\n max-width: ${theme.spacing(32)};\n `,\n timingFormContainer: css`\n padding: ${theme.spacing(1)};\n `,\n linkText: css`\n text-decoration: underline;\n `,\n collapse: css`\n border: none;\n background: none;\n color: ${theme.colors.text.primary};\n `,\n };\n};\n","export const routeTimingsFields = {\n groupWait: {\n label: 'Group wait',\n description:\n 'The waiting time until the initial notification is sent for a new group created by an incoming alert. If empty it will be inherited from the parent policy.',\n ariaLabel: 'Group wait value',\n },\n groupInterval: {\n label: 'Group interval',\n description:\n 'The waiting time to send a batch of new alerts for that group after the first notification was sent. If empty it will be inherited from the parent policy.',\n ariaLabel: 'Group interval value',\n },\n repeatInterval: {\n label: 'Repeat interval',\n description: 'The waiting time to resend an alert after they have successfully been sent.',\n ariaLabel: 'Repeat interval value',\n },\n};\n","export type TimingOptions = {\n group_wait?: string;\n group_interval?: string;\n repeat_interval?: string;\n};\n\nexport const TIMING_OPTIONS_DEFAULTS: Required = {\n group_wait: '30s',\n group_interval: '5m',\n repeat_interval: '4h',\n};\n","import React from 'react';\n\nimport { AlertState } from 'app/plugins/datasource/alertmanager/types';\n\nimport { State, StateTag } from '../StateTag';\n\nconst alertStateToState: Record = {\n [AlertState.Active]: 'bad',\n [AlertState.Unprocessed]: 'neutral',\n [AlertState.Suppressed]: 'info',\n};\n\ninterface Props {\n state: AlertState;\n}\n\nexport const AmAlertStateTag = ({ state }: Props) => {state};\n","import { SerializedError } from '@reduxjs/toolkit';\n\nimport { alertmanagerApi } from '../api/alertmanagerApi';\n\ntype Options = {\n refetchOnFocus: boolean;\n refetchOnReconnect: boolean;\n};\n\n// TODO refactor this so we can just call \"alertmanagerApi.endpoints.getAlertmanagerConfiguration\" everywhere\n// and remove this hook since it adds little value\nexport function useAlertmanagerConfig(amSourceName?: string, options?: Options) {\n const fetchConfig = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(amSourceName ?? '', {\n ...options,\n skip: !amSourceName,\n });\n\n return {\n ...fetchConfig,\n // TODO refactor to get rid of this type assertion\n error: fetchConfig.error as SerializedError,\n };\n}\n","import { CorsWorker as Worker } from 'app/core/utils/CorsWorker';\n\n// CorsWorker is needed as a workaround for CORS issue caused\n// by static assets served from an url different from origin\nexport const createWorker = () => new Worker(new URL('./routeGroupsMatcher.worker.ts', import.meta.url));\n","import * as comlink from 'comlink';\nimport { useCallback, useEffect } from 'react';\n\nimport { AlertmanagerGroup, RouteWithID } from '../../../plugins/datasource/alertmanager/types';\nimport { Labels } from '../../../types/unified-alerting-dto';\n\nimport { logError, logInfo } from './Analytics';\nimport { createWorker } from './createRouteGroupsMatcherWorker';\nimport type { MatchOptions, RouteGroupsMatcher } from './routeGroupsMatcher';\n\nlet routeMatcher: comlink.Remote | undefined;\n\n// Load worker loads the worker if it's not loaded yet\n// and returns a function to dispose of the worker\n// We do it to enable feature toggling. If the feature is disabled we don't wont to load the worker code at all\n// An alternative way would be to move all this code to the hook below, but it will create and terminate the worker much more often\nfunction loadWorker() {\n let worker: Worker | undefined;\n\n if (routeMatcher === undefined) {\n try {\n worker = createWorker();\n routeMatcher = comlink.wrap(worker);\n } catch (e: unknown) {\n if (e instanceof Error) {\n logError(e);\n }\n }\n }\n\n const disposeWorker = () => {\n if (worker && routeMatcher) {\n routeMatcher[comlink.releaseProxy]();\n worker.terminate();\n\n routeMatcher = undefined;\n worker = undefined;\n }\n };\n\n return { disposeWorker };\n}\n\nfunction validateWorker(matcher: typeof routeMatcher): asserts matcher is comlink.Remote {\n if (!routeMatcher) {\n throw new Error('Route Matcher has not been initialized');\n }\n}\n\nexport function useRouteGroupsMatcher() {\n useEffect(() => {\n const { disposeWorker } = loadWorker();\n return disposeWorker;\n\n return () => null;\n }, []);\n\n const getRouteGroupsMap = useCallback(\n async (rootRoute: RouteWithID, alertGroups: AlertmanagerGroup[], options?: MatchOptions) => {\n validateWorker(routeMatcher);\n\n const startTime = performance.now();\n\n const result = await routeMatcher.getRouteGroupsMap(rootRoute, alertGroups, options);\n\n const timeSpent = performance.now() - startTime;\n\n logInfo(`Route Groups Matched in ${timeSpent} ms`, {\n matchingTime: timeSpent.toString(),\n alertGroupsCount: alertGroups.length.toString(),\n // Counting all nested routes might be too time-consuming, so we only count the first level\n topLevelRoutesCount: rootRoute.routes?.length.toString() ?? '0',\n });\n\n return result;\n },\n []\n );\n\n const matchInstancesToRoute = useCallback(\n async (rootRoute: RouteWithID, instancesToMatch: Labels[], options?: MatchOptions) => {\n validateWorker(routeMatcher);\n\n const startTime = performance.now();\n\n const result = await routeMatcher.matchInstancesToRoute(rootRoute, instancesToMatch, options);\n\n const timeSpent = performance.now() - startTime;\n\n logInfo(`Instances Matched in ${timeSpent} ms`, {\n matchingTime: timeSpent.toString(),\n instancesToMatchCount: instancesToMatch.length.toString(),\n // Counting all nested routes might be too time-consuming, so we only count the first level\n topLevelRoutesCount: rootRoute.routes?.length.toString() ?? '0',\n });\n\n return result;\n },\n []\n );\n\n return { getRouteGroupsMap, matchInstancesToRoute };\n}\n","import { uniqueId } from 'lodash';\n\nimport { SelectableValue } from '@grafana/data';\nimport { MatcherOperator, ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';\n\nimport { FormAmRoute } from '../types/amroutes';\nimport { MatcherFieldValue } from '../types/silence-form';\n\nimport { matcherToMatcherField } from './alertmanager';\nimport { GRAFANA_RULES_SOURCE_NAME } from './datasource';\nimport { normalizeMatchers, parseMatcher, quoteWithEscape, unquoteWithUnescape } from './matchers';\nimport { findExistingRoute } from './routeTree';\nimport { isValidPrometheusDuration, safeParseDurationstr } from './time';\n\nconst matchersToArrayFieldMatchers = (\n matchers: Record | undefined,\n isRegex: boolean\n): MatcherFieldValue[] =>\n Object.entries(matchers ?? {}).reduce(\n (acc, [name, value]) => [\n ...acc,\n {\n name,\n value,\n operator: isRegex ? MatcherOperator.regex : MatcherOperator.equal,\n },\n ],\n []\n );\n\nconst selectableValueToString = (selectableValue: SelectableValue): string => selectableValue.value!;\n\nconst selectableValuesToStrings = (arr: Array> | undefined): string[] =>\n (arr ?? []).map(selectableValueToString);\n\nexport const emptyArrayFieldMatcher: MatcherFieldValue = {\n name: '',\n value: '',\n operator: MatcherOperator.equal,\n};\n\n// Default route group_by labels for newly created routes.\nexport const defaultGroupBy = ['grafana_folder', 'alertname'];\n\n// Common route group_by options for multiselect drop-down\nexport const commonGroupByOptions = [\n { label: 'grafana_folder', value: 'grafana_folder', isFixed: true },\n { label: 'alertname', value: 'alertname', isFixed: true },\n { label: 'Disable (...)', value: '...' },\n];\n\nexport const emptyRoute: FormAmRoute = {\n id: '',\n overrideGrouping: false,\n groupBy: defaultGroupBy,\n object_matchers: [],\n routes: [],\n continue: false,\n receiver: '',\n overrideTimings: false,\n groupWaitValue: '',\n groupIntervalValue: '',\n repeatIntervalValue: '',\n muteTimeIntervals: [],\n};\n\n// add unique identifiers to each route in the route tree, that way we can figure out what route we've edited / deleted\nexport function addUniqueIdentifierToRoute(route: Route): RouteWithID {\n return {\n id: uniqueId('route-'),\n ...route,\n routes: (route.routes ?? []).map(addUniqueIdentifierToRoute),\n };\n}\n\n//returns route, and a record mapping id to existing route\nexport const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): FormAmRoute => {\n if (!route) {\n return emptyRoute;\n }\n\n const id = 'id' in route ? route.id : uniqueId('route-');\n\n if (Object.keys(route).length === 0) {\n const formAmRoute = { ...emptyRoute, id };\n return formAmRoute;\n }\n\n const formRoutes: FormAmRoute[] = [];\n route.routes?.forEach((subRoute) => {\n const subFormRoute = amRouteToFormAmRoute(subRoute);\n formRoutes.push(subFormRoute);\n });\n\n const objectMatchers =\n route.object_matchers?.map((matcher) => ({ name: matcher[0], operator: matcher[1], value: matcher[2] })) ?? [];\n const matchers =\n route.matchers\n ?.map((matcher) => matcherToMatcherField(parseMatcher(matcher)))\n .map(({ name, operator, value }) => ({\n name,\n operator,\n value: unquoteWithUnescape(value),\n })) ?? [];\n\n return {\n id,\n // Frontend migration to use object_matchers instead of matchers, match, and match_re\n object_matchers: [\n ...matchers,\n ...objectMatchers,\n ...matchersToArrayFieldMatchers(route.match, false),\n ...matchersToArrayFieldMatchers(route.match_re, true),\n ],\n continue: route.continue ?? false,\n receiver: route.receiver ?? '',\n overrideGrouping: Array.isArray(route.group_by) && route.group_by.length > 0,\n groupBy: route.group_by ?? undefined,\n overrideTimings: [route.group_wait, route.group_interval, route.repeat_interval].some(Boolean),\n groupWaitValue: route.group_wait ?? '',\n groupIntervalValue: route.group_interval ?? '',\n repeatIntervalValue: route.repeat_interval ?? '',\n routes: formRoutes,\n muteTimeIntervals: route.mute_time_intervals ?? [],\n };\n};\n\n// convert a FormAmRoute to a Route\nexport const formAmRouteToAmRoute = (\n alertManagerSourceName: string,\n formAmRoute: Partial,\n routeTree: RouteWithID\n): Route => {\n const existing = findExistingRoute(formAmRoute.id ?? '', routeTree);\n\n const {\n overrideGrouping,\n groupBy,\n overrideTimings,\n groupWaitValue,\n groupIntervalValue,\n repeatIntervalValue,\n receiver,\n } = formAmRoute;\n\n // \"undefined\" means \"inherit from the parent policy\", currently supported by group_by, group_wait, group_interval, and repeat_interval\n const INHERIT_FROM_PARENT = undefined;\n\n const group_by = overrideGrouping ? groupBy : INHERIT_FROM_PARENT;\n\n const overrideGroupWait = overrideTimings && groupWaitValue;\n const group_wait = overrideGroupWait ? groupWaitValue : INHERIT_FROM_PARENT;\n\n const overrideGroupInterval = overrideTimings && groupIntervalValue;\n const group_interval = overrideGroupInterval ? groupIntervalValue : INHERIT_FROM_PARENT;\n\n const overrideRepeatInterval = overrideTimings && repeatIntervalValue;\n const repeat_interval = overrideRepeatInterval ? repeatIntervalValue : INHERIT_FROM_PARENT;\n\n // Empty matcher values are valid. Such matchers require specified label to not exists\n const object_matchers: ObjectMatcher[] | undefined = formAmRoute.object_matchers\n ?.filter((route) => route.name && route.operator && route.value !== null && route.value !== undefined)\n .map(({ name, operator, value }) => [name, operator, value]);\n\n const routes = formAmRoute.routes?.map((subRoute) =>\n formAmRouteToAmRoute(alertManagerSourceName, subRoute, routeTree)\n );\n\n const amRoute: Route = {\n ...(existing ?? {}),\n continue: formAmRoute.continue,\n group_by: group_by,\n object_matchers: object_matchers,\n match: undefined, // DEPRECATED: Use matchers\n match_re: undefined, // DEPRECATED: Use matchers\n group_wait,\n group_interval,\n repeat_interval,\n routes: routes,\n mute_time_intervals: formAmRoute.muteTimeIntervals,\n receiver: receiver,\n };\n\n // non-Grafana managed rules should use \"matchers\", Grafana-managed rules should use \"object_matchers\"\n // Grafana maintains a fork of AM to support all utf-8 characters in the \"object_matchers\" property values but this\n // does not exist in upstream AlertManager\n if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) {\n amRoute.matchers = formAmRoute.object_matchers?.map(\n ({ name, operator, value }) => `${name}${operator}${quoteWithEscape(value)}`\n );\n amRoute.object_matchers = undefined;\n } else {\n amRoute.object_matchers = normalizeMatchers(amRoute);\n amRoute.matchers = undefined;\n }\n\n if (formAmRoute.receiver) {\n amRoute.receiver = formAmRoute.receiver;\n }\n\n return amRoute;\n};\n\nexport const stringToSelectableValue = (str: string): SelectableValue => ({\n label: str,\n value: str,\n});\n\nexport const stringsToSelectableValues = (arr: string[] | undefined): Array> =>\n (arr ?? []).map(stringToSelectableValue);\n\nexport const mapSelectValueToString = (selectableValue: SelectableValue): string | null => {\n // this allows us to deal with cleared values\n if (selectableValue === null) {\n return null;\n }\n\n if (!selectableValue) {\n return '';\n }\n\n return selectableValueToString(selectableValue) ?? '';\n};\n\nexport const mapMultiSelectValueToStrings = (\n selectableValues: Array> | undefined\n): string[] => {\n if (!selectableValues) {\n return [];\n }\n\n return selectableValuesToStrings(selectableValues);\n};\n\nexport function promDurationValidator(duration?: string) {\n if (!duration || duration.length === 0) {\n return true;\n }\n\n return isValidPrometheusDuration(duration) || 'Invalid duration format. Must be {number}{time_unit}';\n}\n\n// function to convert ObjectMatchers to a array of strings\nexport const objectMatchersToString = (matchers: ObjectMatcher[]): string[] => {\n return matchers.map((matcher) => {\n const [name, operator, value] = matcher;\n return `${name}${operator}${value}`;\n });\n};\n\nexport const repeatIntervalValidator = (repeatInterval: string, groupInterval = '') => {\n if (repeatInterval.length === 0) {\n return true;\n }\n\n const validRepeatInterval = promDurationValidator(repeatInterval);\n const validGroupInterval = promDurationValidator(groupInterval);\n\n if (validRepeatInterval !== true) {\n return validRepeatInterval;\n }\n\n if (validGroupInterval !== true) {\n return validGroupInterval;\n }\n\n const repeatDuration = safeParseDurationstr(repeatInterval);\n const groupDuration = safeParseDurationstr(groupInterval);\n\n const isRepeatLowerThanGroupDuration = groupDuration !== 0 && repeatDuration < groupDuration;\n\n return isRepeatLowerThanGroupDuration ? 'Repeat interval should be higher or equal to Group interval' : true;\n};\n","/**\n * Various helper functions to modify (immutably) the route tree, aka \"notification policies\"\n */\n\nimport { omit } from 'lodash';\n\nimport { Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';\n\nimport { FormAmRoute } from '../types/amroutes';\n\nimport { formAmRouteToAmRoute } from './amroutes';\n\n// add a form submission to the route tree\nexport const mergePartialAmRouteWithRouteTree = (\n alertManagerSourceName: string,\n partialFormRoute: Partial,\n routeTree: RouteWithID\n): Route => {\n const existing = findExistingRoute(partialFormRoute.id ?? '', routeTree);\n if (!existing) {\n throw new Error(`No such route with ID '${partialFormRoute.id}'`);\n }\n\n function findAndReplace(currentRoute: RouteWithID): Route {\n let updatedRoute: Route = currentRoute;\n\n if (currentRoute.id === partialFormRoute.id) {\n const newRoute = formAmRouteToAmRoute(alertManagerSourceName, partialFormRoute, routeTree);\n updatedRoute = omit(\n {\n ...currentRoute,\n ...newRoute,\n },\n 'id'\n );\n }\n\n return omit(\n {\n ...updatedRoute,\n routes: currentRoute.routes?.map(findAndReplace),\n },\n 'id'\n );\n }\n\n return findAndReplace(routeTree);\n};\n\n// remove a route from the policy tree, returns a new tree\n// make sure to omit the \"id\" because Prometheus / Loki / Mimir will reject the payload\nexport const omitRouteFromRouteTree = (findRoute: RouteWithID, routeTree: RouteWithID): Route => {\n if (findRoute.id === routeTree.id) {\n throw new Error('You cant remove the root policy');\n }\n\n function findAndOmit(currentRoute: RouteWithID): Route {\n return omit(\n {\n ...currentRoute,\n routes: currentRoute.routes?.reduce((acc: Route[] = [], route) => {\n if (route.id === findRoute.id) {\n return acc;\n }\n\n acc.push(findAndOmit(route));\n return acc;\n }, []),\n },\n 'id'\n );\n }\n\n return findAndOmit(routeTree);\n};\n\n// add a new route to a parent route\nexport const addRouteToParentRoute = (\n alertManagerSourceName: string,\n partialFormRoute: Partial,\n parentRoute: RouteWithID,\n routeTree: RouteWithID\n): Route => {\n const newRoute = formAmRouteToAmRoute(alertManagerSourceName, partialFormRoute, routeTree);\n\n function findAndAdd(currentRoute: RouteWithID): RouteWithID {\n if (currentRoute.id === parentRoute.id) {\n return {\n ...currentRoute,\n // TODO fix this typescript exception, it's... complicated\n // @ts-ignore\n routes: currentRoute.routes?.concat(newRoute),\n };\n }\n\n return {\n ...currentRoute,\n routes: currentRoute.routes?.map(findAndAdd),\n };\n }\n\n function findAndOmitId(currentRoute: RouteWithID): Route {\n return omit(\n {\n ...currentRoute,\n routes: currentRoute.routes?.map(findAndOmitId),\n },\n 'id'\n );\n }\n\n return findAndOmitId(findAndAdd(routeTree));\n};\n\nexport function findExistingRoute(id: string, routeTree: RouteWithID): RouteWithID | undefined {\n return routeTree.id === id ? routeTree : routeTree.routes?.find((route) => findExistingRoute(id, route));\n}\n"],"names":["useCleanup","cleanupAction","dispatch","selectorRef","receiversApi","alertingApi","build","amSourceName","error","useGetContactPointsState","alertManagerSourceName","contactPointsStateEmpty","contactPointsState","EmptyAreaWithCTA","buttonIcon","buttonLabel","buttonSize","buttonVariant","onButtonClick","text","href","showButton","styles","getStyles","commonProps","EmptyArea","Button","theme","GrafanaMuteTimingsExporterPreview","exportFormat","onClose","muteTimingsDefinition","isFetching","alertRuleApi","downloadFileName","LoadingPlaceholder","FileExportPreview","GrafanaMuteTimingExporterPreview","muteTimingName","GrafanaMuteTimingsExporter","activeTab","setActiveTab","GrafanaExportDrawer","ALL_MUTE_TIMINGS","useExportMuteTiming","setMuteTimingName","isExportDrawerOpen","toggleShowExportDrawer","useToggle","handleClose","handleOpen","receiverName","MuteTimingsTable","muteTimingNames","hideActions","currentData","useAlertmanagerConfig","config","items","muteTimings","muteTimingsProvenances","name","mute","_","allowedToCreateMuteTiming","ExportDrawer","showExportDrawer","exportMuteTimingsSupported","exportMuteTimingsAllowed","columns","useColumns","Stack","Spacer","Authorize","DynamicTable","ConfirmModal","openExportDrawer","_editSupported","allowedToEdit","_deleteSupported","allowedToDelete","showActions","exportSupported","exportAllowed","data","Provisioning","Link","IconButton","Menu","NotificationPoliciesFilter","receivers","onChangeReceiver","onChangeMatchers","matchingCount","searchParams","setSearchParams","useURLSearchParams","searchInputRef","queryString","contactPoint","getNotificationPoliciesFilters","handleChangeLabels","matchers","clearFilters","receiverOptions","toOption","selectedContactPoint","option","hasFilters","inputInvalid","Field","Label","Tooltip","Icon","Input","event","Select","Text","findRoutesMatchingPredicate","routeTree","predicateFn","matchingRouteIdsWithPath","findMatch","route","path","newPath","previousPath","findRoutesByMatchers","labelMatchersFilter","routeMatchers","filter","matcher","receiver","useGetGrafanaReceiverTypeChecker","isOnCallEnabled","usePluginBridge","onCallApi","onCallIntegrations","useGetAmRouteReceiverWithGrafanaAppTypes","getGrafanaReceiverType","receiverToSelectableContactPointValue","AlertGroupsSummary","active","suppressed","unprocessed","statsComponents","total","Badge","AmRootRouteForm","actionButtons","onSubmit","isTimingOptionsExpanded","setIsTimingOptionsExpanded","groupByOptions","setGroupByOptions","defaultValues","Form","register","control","errors","setValue","getValues","InputControl","onChange","ref","field","value","opt","opts","Collapse","PromDurationInput","groupInterval","useMuteTimingOptions","selectedAlertmanager","interval","AmRoutesExpandedForm","defaults","formStyles","muteTimingOptions","emptyMatcher","receiversWithOnCallOnTop","onCallFirst","formAmRoute","watch","FieldArray","fields","append","remove","index","Switch","FieldValidationMessage","routeTimingsFields","commonSpacing","useAddPolicyModal","handleAdd","loading","showModal","setShowModal","parentRoute","setParentRoute","AmRouteReceivers","handleDismiss","handleShow","UpdatingModal","Modal","newRoute","useEditPolicyModal","handleSave","isDefaultPolicy","setIsDefaultPolicy","setRoute","useDeletePolicyModal","handleDelete","handleSubmit","useAlertGroupsModal","alertGroups","setAlertGroups","setMatchers","formatter","setFormatter","instancesByState","instances","group","instance","Matchers","AlertGroup","isOpen","Spinner","GrafanaPoliciesExporterPreview","policiesDefinition","GrafanaPoliciesExporter","Policy","props","readOnly","provisioned","currentRoute","inheritedProperties","routesMatchingFilters","matchingInstancesPreview","onEditPolicy","onAddPolicy","onDeletePolicy","onShowAlertInstances","isAutoGenerated","continueMatching","hasMatchers","filtersApplied","matchedRoutesWithPath","matchedRoutes","hasFocus","routesPath","belongsToMatchPath","showMatchesAllLabelsWarning","actualContactPoint","contactPointErrors","getContactPointErrors","allChildPolicies","childPolicies","policy","matchingAlertGroups","numberOfAlertInstances","isSupportedToSeeAutogeneratedChunk","isAllowedToSeeAutogeneratedChunk","isThisPolicyCollapsible","useShouldPolicyBeCollapsible","isBranchOpen","toggleBranchOpen","renderChildPolicies","groupBy","timingOptions","POLICIES_PER_PAGE","visibleChildPolicies","setVisibleChildPolicies","isAutogeneratedPolicyRoot","isAutoGeneratedRootAndSimplifiedEnabled","dropdownMenuActions","useCreateDropdownMenuActions","isImmutablePolicy","childPoliciesBelongingToMatchPath","child","childPoliciesToRender","pageOfChildren","moreCount","showMore","ContinueMatchingIndicator","AllMatchesIndicator","AutogeneratedRootIndicator","DefaultPolicyIndicator","Errors","ConditionalWrap","ProvisionedTooltip","Dropdown","MetadataRow","childInheritedProperties","isThisChildAutoGenerated","isThisChildReadOnly","childrenCount","inheritedGrouping","hasInheritedProperties","noGrouping","customGrouping","singleGroup","hasMuteTimings","MetaText","Strong","ContactPointsHoverDetails","MuteTimings","TimingOptionsMeta","InheritedProperties","updatePoliciesSupported","updatePoliciesAllowed","deletePolicySupported","deletePolicyAllowed","exportPoliciesSupported","exportPoliciesAllowed","showExportAction","showEditAction","showDeleteAction","AUTOGENERATED_ROOT_LABEL_NAME","objectMatcher","children","HoverCard","properties","key","routePropertyToLabel","routePropertyToValue","timings","timing","groupWait","details","groupedIntegrations","type","integrations","acc","notifierStatuses","notifierErrors","status","isNotGrouping","isSingleGroup","label","color","borderColor","ActiveTab","AmRoutes","useGetAlertmanagerAlertGroupsQuery","alertmanagerApi","queryParams","setQueryParams","useQueryParams","tab","getActiveTabFromUrl","updatingTree","setUpdatingTree","contactPointFilter","setContactPointFilter","setLabelMatchersFilter","hasConfigurationAPI","isGrafanaAlertmanager","getRouteGroupsMap","useRouteGroupsMatcher","result","resultLoading","resultError","refetchAlertGroups","rootRoute","routeAlertGroupsMap","instancesPreviewError","triggerGetRouteGroupsMap","useAsyncFn","findRoutesMatchingFilters","isProvisioned","partialRoute","newRouteTree","updateRouteTree","closeEditModal","closeAddModal","closeDeleteModal","addModal","openAddModal","editModal","openEditModal","deleteModal","openDeleteModal","alertInstancesModal","showAlertGroupsModal","state","numberOfMuteTimings","haveData","haveError","muteTimingsTabActive","policyTreeTabActive","TabsBar","Tab","TabContent","Alert","GrafanaAlertmanagerDeliveryWarning","filters","hasFilter","havebothFilters","fullRoute","matchingRoutesForContactPoint","routesMatchingContactPoint","matchingRoutesForLabelMatchers","routesMatchingLabelFilters","findMapIntersection","matchingRoutes","map","NotificationPoliciesPage","AlertDetails","alert","isSeeSourceButtonEnabled","annotationKey","annotationValue","AnnotationDetailsField","AlertGroupAlertsTable","alerts","AmAlertStateTag","labels","AlertLabels","DynamicTableWithGuidelines","isCollapsed","setIsCollapsed","receiverInGroup","CollapseToggle","AlertGroupHeader","TIME_RANGE_REGEX","isvalidTimeFormat","timeString","isValidStartAndEndTime","startTime","endTime","timeUnit","startDate","endDate","renderTimeIntervals","muteTiming","times","weekdays","days_of_month","months","years","location","weekdayString","daysString","monthsString","yearsString","NUM_MATCHERS","firstFew","rest","hasMoreMatchers","MatcherBadge","PromDurationDocs","getPromDurationStyles","PromDurationDocsTimeUnit","unit","example","getFormStyles","TIMING_OPTIONS_DEFAULTS","alertStateToState","options","fetchConfig","createWorker","routeMatcher","loadWorker","worker","comlink","e","validateWorker","disposeWorker","timeSpent","matchInstancesToRoute","instancesToMatch","matchersToArrayFieldMatchers","isRegex","selectableValueToString","selectableValue","selectableValuesToStrings","arr","emptyArrayFieldMatcher","defaultGroupBy","commonGroupByOptions","emptyRoute","addUniqueIdentifierToRoute","amRouteToFormAmRoute","id","formRoutes","subRoute","subFormRoute","objectMatchers","operator","formAmRouteToAmRoute","existing","overrideGrouping","overrideTimings","groupWaitValue","groupIntervalValue","repeatIntervalValue","INHERIT_FROM_PARENT","group_by","group_wait","group_interval","repeat_interval","object_matchers","routes","amRoute","stringToSelectableValue","str","stringsToSelectableValues","mapSelectValueToString","mapMultiSelectValueToStrings","selectableValues","promDurationValidator","duration","objectMatchersToString","repeatIntervalValidator","repeatInterval","validRepeatInterval","validGroupInterval","repeatDuration","groupDuration","mergePartialAmRouteWithRouteTree","partialFormRoute","findExistingRoute","findAndReplace","updatedRoute","omitRouteFromRouteTree","findRoute","findAndOmit","addRouteToParentRoute","findAndAdd","findAndOmitId"],"sourceRoot":""}