diff --git a/public/app/features/alerting/unified/api/routesApi.test.ts b/public/app/features/alerting/unified/api/routesApi.test.ts deleted file mode 100644 index dbc91cce4c6..00000000000 --- a/public/app/features/alerting/unified/api/routesApi.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types'; - -import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route } from '../openapi/routesApi.gen'; - -import { k8sSubRouteToRoute, routeToK8sSubRoute } from './routesApi'; - -test('k8sSubRouteToRoute', () => { - const input: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route = { - continue: false, - group_by: ['label1'], - group_interval: '5m', - group_wait: '30s', - matchers: [{ label: 'label1', type: '=', value: 'value1' }], - mute_time_intervals: ['mt-1'], - receiver: 'my-receiver', - repeat_interval: '4h', - routes: [ - { - receiver: 'receiver2', - matchers: [{ label: 'label2', type: '!=', value: 'value2' }], - }, - ], - }; - - const expected: Route = { - name: 'test-name', - continue: false, - group_by: ['label1'], - group_interval: '5m', - group_wait: '30s', - matchers: undefined, // matchers -> object_matchers - object_matchers: [['label1', MatcherOperator.equal, 'value1']], - mute_time_intervals: ['mt-1'], - receiver: 'my-receiver', - repeat_interval: '4h', - routes: [ - { - name: 'test-name', - receiver: 'receiver2', - matchers: undefined, - object_matchers: [['label2', MatcherOperator.notEqual, 'value2']], - routes: undefined, - }, - ], - }; - - expect(k8sSubRouteToRoute(input, 'test-name')).toStrictEqual(expected); -}); - -test('routeToK8sSubRoute', () => { - const input: Route = { - continue: false, - group_by: ['label1'], - group_interval: '5m', - group_wait: '30s', - matchers: undefined, // matchers -> object_matchers - object_matchers: [['label1', MatcherOperator.equal, 'value1']], - mute_time_intervals: ['mt-1'], - receiver: 'my-receiver', - repeat_interval: '4h', - routes: [ - { - receiver: 'receiver2', - matchers: undefined, - object_matchers: [['label2', MatcherOperator.notEqual, 'value2']], - }, - ], - }; - - const expected: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route = { - continue: false, - group_by: ['label1'], - group_interval: '5m', - group_wait: '30s', - matchers: [{ label: 'label1', type: '=', value: 'value1' }], - mute_time_intervals: ['mt-1'], - receiver: 'my-receiver', - repeat_interval: '4h', - routes: [ - { - receiver: 'receiver2', - matchers: [{ label: 'label2', type: '!=', value: 'value2' }], - routes: undefined, - }, - ], - }; - - expect(routeToK8sSubRoute(input)).toStrictEqual(expected); -}); diff --git a/public/app/features/alerting/unified/api/routesApi.ts b/public/app/features/alerting/unified/api/routesApi.ts deleted file mode 100644 index a4e0918f721..00000000000 --- a/public/app/features/alerting/unified/api/routesApi.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree, - ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route, - generatedRoutesApi, - ReadNamespacedRoutingTreeApiResponse, - ListNamespacedRoutingTreeApiResponse, -} from 'app/features/alerting/unified/openapi/routesApi.gen'; -import { MatcherOperator, ROUTES_META_SYMBOL, Route } from 'app/plugins/datasource/alertmanager/types'; -import { ROOT_ROUTE_NAME } from '../utils/k8s/constants'; -import { isK8sEntityProvisioned } from '../utils/k8s/utils'; -import { DefinitionsFromApi, OverrideResultType, TagTypesFromApi } from '@reduxjs/toolkit/query'; - -type Definitions = DefinitionsFromApi; -type TagTypes = TagTypesFromApi; - -type UpdatedDefinitions = Omit & { - readNamespacedRoutingTree: OverrideResultType; - listNamespacedRoutingTree: OverrideResultType; -}; - -export const routesApi = generatedRoutesApi.enhanceEndpoints({ - endpoints: { - readNamespacedRoutingTree: (endpoint) => { - // We transform the response here instead of in `selectFromResult` so that memoization of the transformed Route - // is automatically handled. - endpoint.transformResponse = (response: ReadNamespacedRoutingTreeApiResponse): Route => { - return k8sRouteToRoute(response); - }; - }, - listNamespacedRoutingTree: (endpoint) => { - endpoint.transformResponse = (response: ListNamespacedRoutingTreeApiResponse): Route[] => { - return k8sRoutesToRoutes(response.items); - }; - }, - }, -}); - -export const NAMED_ROOT_LABEL_NAME = '__grafana_managed_route__'; - -function k8sRouteToRoute(route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree): Route { - return { - ...route.spec.defaults, - name: route.metadata.name, - routes: route.spec.routes?.map((subroute) => k8sSubRouteToRoute(subroute, route.metadata.name)), - // This assumes if a `NAMED_ROOT_LABEL_NAME` label exists, it will NOT go to the default route, which is a fair but - // not perfect assumption since we don't yet protect the label. - object_matchers: - route.metadata.name == ROOT_ROUTE_NAME || !route.metadata.name - ? [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, '']] - : [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, route.metadata.name]], - [ROUTES_META_SYMBOL]: { - provisioned: isK8sEntityProvisioned(route), - resourceVersion: route.metadata.resourceVersion, - name: route.metadata.name, - metadata: route.metadata, - }, - }; -} - -function k8sRoutesToRoutes(routes: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree[]): Route[] { - return routes?.map((route) => { - return k8sRouteToRoute(route); - }); -} - -/** Helper to provide type safety for matcher operators from API */ -function isValidMatcherOperator(type: string): type is MatcherOperator { - return Object.values(MatcherOperator).includes(type); -} - -export function k8sSubRouteToRoute( - route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route, - rootName?: string -): Route { - return { - ...route, - name: rootName, - routes: route.routes?.map((subroute) => k8sSubRouteToRoute(subroute, rootName)), - matchers: undefined, - object_matchers: route.matchers?.map(({ label, type, value }) => { - if (!isValidMatcherOperator(type)) { - throw new Error(`Invalid matcher operator from API: ${type}`); - } - return [label, type, value]; - }), - }; -} - -export function routeToK8sSubRoute(route: Route): ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route { - const { object_matchers, ...rest } = route; - return { - ...rest, - receiver: route.receiver ?? undefined, - matchers: object_matchers?.map(([label, type, value]) => ({ - label, - type, - value, - })), - routes: route.routes?.map(routeToK8sSubRoute), - }; -} diff --git a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts index 43b9ce970f8..8d2d978cc6f 100644 --- a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts +++ b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts @@ -3,7 +3,7 @@ import memoize from 'micro-memoize'; import { INHERITABLE_KEYS, type InheritableProperties } from '@grafana/alerting/internal'; import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; -import { ROUTES_META_SYMBOL, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; +import { ROUTES_META_SYMBOL, Route, RouteWithID, MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; import { getAPINamespace } from '../../../../../api/utils'; import { alertmanagerApi } from '../../api/alertmanagerApi'; @@ -12,8 +12,9 @@ import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNe import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RouteDefaults, ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree, + ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route, + generatedRoutesApi as routingTreeApi, } from '../../openapi/routesApi.gen'; -import { routesApi as routingTreeApi, routeToK8sSubRoute } from '../../api/routesApi'; import { addRouteAction, deleteRouteAction, @@ -22,7 +23,7 @@ import { import { FormAmRoute } from '../../types/amroutes'; import { addUniqueIdentifierToRoute } from '../../utils/amroutes'; import { PROVENANCE_NONE, ROOT_ROUTE_NAME } from '../../utils/k8s/constants'; -import { shouldUseK8sApi } from '../../utils/k8s/utils'; +import { isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils'; import { routeAdapter } from '../../utils/routeAdapter'; import { InsertPosition, @@ -57,11 +58,21 @@ export const useNotificationPolicyRoute = ( { skip: skip || !k8sApiSupported, selectFromResult: (result) => { - return { - ...result, - currentData: result.currentData, - data: result.data, - }; + const { data, currentData, ...rest } = result; + + const transformed = useMemo(() => { + return data ? k8sRouteToRoute(data) : data; + }, [data]); + + const transformedCurrent = useMemo(() => { + return currentData ? k8sRouteToRoute(currentData) : currentData; + }, [currentData]); + + return { + ...rest, + data: transformed, + currentData: transformedCurrent, + }; }, } ); @@ -90,10 +101,20 @@ export const useListNotificationPolicyRoutes = ({ skip }: Skippable = {}) => { { skip: skip, selectFromResult: (result) => { + const { data, currentData, ...rest } = result; + + const transformed = useMemo(() => { + return data ? data.items.map(k8sRouteToRoute) : data; + }, [data]); + + const transformedCurrent = useMemo(() => { + return currentData ? currentData.items.map(k8sRouteToRoute) : currentData; + }, [currentData]); + return { - ...result, - currentData: result.currentData, - data: result.data, + ...rest, + data: transformed, + currentData: transformedCurrent, }; }, } @@ -125,7 +146,7 @@ export function useUpdateExistingNotificationPolicy({ alertmanager }: BaseAlertm throw new Error(`no root route found for namespace ${namespace} and name ${name}`); } - const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree); + const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(k8sRouteToRoute(rootTree)); const newRouteTree = mergePartialAmRouteWithRouteTree(alertmanager, update, rootRouteWithIdentifiers); // Create the K8s route object @@ -162,7 +183,7 @@ export function useDeleteNotificationPolicy({ alertmanager }: BaseAlertmanagerAr throw new Error(`no root route found for namespace ${namespace}`); } - const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree); + const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(k8sRouteToRoute(rootTree)); const newRouteTree = omitRouteFromRouteTree(route.id, rootRouteWithIdentifiers); // Create the K8s route object @@ -208,7 +229,7 @@ export function useAddNotificationPolicy({ alertmanager }: BaseAlertmanagerArgs) throw new Error(`no root route found for namespace ${namespace}`); } - const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree); + const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(k8sRouteToRoute(rootTree)); const newRouteTree = addRouteToReferenceRoute( alertmanager ?? '', partialRoute, @@ -369,3 +390,63 @@ export function createKubernetesRoutingTreeSpec( }, }; } + + +export const NAMED_ROOT_LABEL_NAME = '__grafana_managed_route__'; + +function k8sRouteToRoute(route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree): Route { + return { + ...route.spec.defaults, + name: route.metadata.name, + routes: route.spec.routes?.map((subroute) => k8sSubRouteToRoute(subroute, route.metadata.name)), + // This assumes if a `NAMED_ROOT_LABEL_NAME` label exists, it will NOT go to the default route, which is a fair but + // not perfect assumption since we don't yet protect the label. + object_matchers: + route.metadata.name == ROOT_ROUTE_NAME || !route.metadata.name + ? [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, '']] + : [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, route.metadata.name]], + [ROUTES_META_SYMBOL]: { + provisioned: isK8sEntityProvisioned(route), + resourceVersion: route.metadata.resourceVersion, + name: route.metadata.name, + metadata: route.metadata, + }, + }; +} + +/** Helper to provide type safety for matcher operators from API */ +function isValidMatcherOperator(type: string): type is MatcherOperator { + return Object.values(MatcherOperator).includes(type); +} + +export function k8sSubRouteToRoute( + route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route, + rootName?: string +): Route { + return { + ...route, + name: rootName, + routes: route.routes?.map((subroute) => k8sSubRouteToRoute(subroute, rootName)), + matchers: undefined, + object_matchers: route.matchers?.map(({ label, type, value }) => { + if (!isValidMatcherOperator(type)) { + throw new Error(`Invalid matcher operator from API: ${type}`); + } + return [label, type, value]; + }), + }; +} + +export function routeToK8sSubRoute(route: Route): ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route { + const { object_matchers, ...rest } = route; + return { + ...rest, + receiver: route.receiver ?? undefined, + matchers: object_matchers?.map(([label, type, value]) => ({ + label, + type, + value, + })), + routes: route.routes?.map(routeToK8sSubRoute), + }; +} diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts index 548a4b442bc..82271f7a304 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts @@ -1,14 +1,16 @@ import { useMemo } from 'react'; import { useAsync } from 'react-use'; -import { useNotificationPolicyRoute } from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute'; +import { + NAMED_ROOT_LABEL_NAME, + useNotificationPolicyRoute, +} from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute'; import { Labels } from '../../../../../../types/unified-alerting-dto'; import { useRouteGroupsMatcher } from '../../../useRouteGroupsMatcher'; import { addUniqueIdentifierToRoute } from '../../../utils/amroutes'; import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; import { normalizeRoute } from '../../../utils/notification-policies'; -import { NAMED_ROOT_LABEL_NAME } from '../../../api/routesApi'; export const useAlertmanagerNotificationRoutingPreview = (alertmanager: string, instances: Labels[]) => { // if a NAMED_ROOT_LABEL_NAME label exists, then we only match to that route.