mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix notification policies inheritance algorithm (#69304)
This commit is contained in:
parent
a221e1d226
commit
f94e07f5a4
@ -6,7 +6,7 @@ interface Props {}
|
|||||||
|
|
||||||
const Strong = ({ children }: React.PropsWithChildren<Props>) => {
|
const Strong = ({ children }: React.PropsWithChildren<Props>) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
return <strong style={{ color: theme.colors.text.maxContrast }}>{children}</strong>;
|
return <strong style={{ color: theme.colors.text.primary }}>{children}</strong>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Strong };
|
export { Strong };
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
useStyles2,
|
useStyles2,
|
||||||
Badge,
|
Badge,
|
||||||
|
FieldValidationMessage,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { MatcherOperator, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
import { MatcherOperator, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
@ -186,10 +187,20 @@ export const AmRoutesExpandedForm = ({
|
|||||||
description="Group alerts when you receive a notification based on labels. If empty it will be inherited from the parent policy."
|
description="Group alerts when you receive a notification based on labels. If empty it will be inherited from the parent policy."
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
rules={{
|
||||||
|
validate: (value) => {
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return 'At least one group by option is required.';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
|
||||||
|
<>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
aria-label="Group by"
|
aria-label="Group by"
|
||||||
{...field}
|
{...field}
|
||||||
|
invalid={Boolean(error)}
|
||||||
allowCustomValue
|
allowCustomValue
|
||||||
className={formStyles.input}
|
className={formStyles.input}
|
||||||
onCreateOption={(opt: string) => {
|
onCreateOption={(opt: string) => {
|
||||||
@ -201,6 +212,8 @@ export const AmRoutesExpandedForm = ({
|
|||||||
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
|
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
|
||||||
options={[...commonGroupByOptions, ...groupByOptions]}
|
options={[...commonGroupByOptions, ...groupByOptions]}
|
||||||
/>
|
/>
|
||||||
|
{error && <FieldValidationMessage>{error.message}</FieldValidationMessage>}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name="groupBy"
|
name="groupBy"
|
||||||
|
@ -56,7 +56,7 @@ describe('Policy', () => {
|
|||||||
expect(within(defaultPolicy).getByText('Default policy')).toBeVisible();
|
expect(within(defaultPolicy).getByText('Default policy')).toBeVisible();
|
||||||
|
|
||||||
// click "more actions" and check if we can edit and delete
|
// click "more actions" and check if we can edit and delete
|
||||||
expect(await within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
||||||
await userEvent.click(within(defaultPolicy).getByTestId('more-actions'));
|
await userEvent.click(within(defaultPolicy).getByTestId('more-actions'));
|
||||||
|
|
||||||
// should be editable
|
// should be editable
|
||||||
@ -102,8 +102,8 @@ describe('Policy', () => {
|
|||||||
|
|
||||||
// click "more actions" and check if we can delete
|
// click "more actions" and check if we can delete
|
||||||
await userEvent.click(policy.getByTestId('more-actions'));
|
await userEvent.click(policy.getByTestId('more-actions'));
|
||||||
expect(await screen.queryByRole('menuitem', { name: 'Edit' })).not.toBeDisabled();
|
expect(screen.queryByRole('menuitem', { name: 'Edit' })).not.toBeDisabled();
|
||||||
expect(await screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeDisabled();
|
expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeDisabled();
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
||||||
expect(onDeletePolicy).toHaveBeenCalled();
|
expect(onDeletePolicy).toHaveBeenCalled();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { uniqueId, pick, groupBy, upperFirst, merge, reduce, sumBy } from 'lodash';
|
import { uniqueId, groupBy, upperFirst, sumBy, isArray } from 'lodash';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import React, { FC, Fragment, ReactNode } from 'react';
|
import React, { FC, Fragment, ReactNode } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -7,19 +7,15 @@ import { Link } from 'react-router-dom';
|
|||||||
import { GrafanaTheme2, IconName } from '@grafana/data';
|
import { GrafanaTheme2, IconName } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Badge, Button, Dropdown, getTagColorsFromName, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Badge, Button, Dropdown, getTagColorsFromName, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
import { Span } from '@grafana/ui/src/unstable';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import {
|
import { RouteWithID, Receiver, ObjectMatcher, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||||
RouteWithID,
|
|
||||||
Receiver,
|
|
||||||
ObjectMatcher,
|
|
||||||
Route,
|
|
||||||
AlertmanagerGroup,
|
|
||||||
} from 'app/plugins/datasource/alertmanager/types';
|
|
||||||
import { ReceiversState } from 'app/types';
|
import { ReceiversState } from 'app/types';
|
||||||
|
|
||||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
import { normalizeMatchers } from '../../utils/matchers';
|
import { normalizeMatchers } from '../../utils/matchers';
|
||||||
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
||||||
|
import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies';
|
||||||
import { HoverCard } from '../HoverCard';
|
import { HoverCard } from '../HoverCard';
|
||||||
import { Label } from '../Label';
|
import { Label } from '../Label';
|
||||||
import { MetaText } from '../MetaText';
|
import { MetaText } from '../MetaText';
|
||||||
@ -29,17 +25,12 @@ import { Strong } from '../Strong';
|
|||||||
import { Matchers } from './Matchers';
|
import { Matchers } from './Matchers';
|
||||||
import { TimingOptions, TIMING_OPTIONS_DEFAULTS } from './timingOptions';
|
import { TimingOptions, TIMING_OPTIONS_DEFAULTS } from './timingOptions';
|
||||||
|
|
||||||
type InhertitableProperties = Pick<
|
|
||||||
Route,
|
|
||||||
'receiver' | 'group_by' | 'group_wait' | 'group_interval' | 'repeat_interval' | 'mute_time_intervals'
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface PolicyComponentProps {
|
interface PolicyComponentProps {
|
||||||
receivers?: Receiver[];
|
receivers?: Receiver[];
|
||||||
alertGroups?: AlertmanagerGroup[];
|
alertGroups?: AlertmanagerGroup[];
|
||||||
contactPointsState?: ReceiversState;
|
contactPointsState?: ReceiversState;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
inheritedProperties?: InhertitableProperties;
|
inheritedProperties?: Partial<InhertitableProperties>;
|
||||||
routesMatchingFilters?: RouteWithID[];
|
routesMatchingFilters?: RouteWithID[];
|
||||||
// routeAlertGroupsMap?: Map<string, AlertmanagerGroup[]>;
|
// routeAlertGroupsMap?: Map<string, AlertmanagerGroup[]>;
|
||||||
|
|
||||||
@ -79,7 +70,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
|
|
||||||
const contactPoint = currentRoute.receiver;
|
const contactPoint = currentRoute.receiver;
|
||||||
const continueMatching = currentRoute.continue ?? false;
|
const continueMatching = currentRoute.continue ?? false;
|
||||||
const groupBy = currentRoute.group_by ?? [];
|
const groupBy = currentRoute.group_by;
|
||||||
const muteTimings = currentRoute.mute_time_intervals ?? [];
|
const muteTimings = currentRoute.mute_time_intervals ?? [];
|
||||||
const timingOptions: TimingOptions = {
|
const timingOptions: TimingOptions = {
|
||||||
group_wait: currentRoute.group_wait,
|
group_wait: currentRoute.group_wait,
|
||||||
@ -107,10 +98,15 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
errors.push(error);
|
errors.push(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const childPolicies = currentRoute.routes ?? [];
|
|
||||||
const isGrouping = Array.isArray(groupBy) && groupBy.length > 0;
|
|
||||||
const hasInheritedProperties = inheritedProperties && Object.keys(inheritedProperties).length > 0;
|
const hasInheritedProperties = inheritedProperties && Object.keys(inheritedProperties).length > 0;
|
||||||
|
|
||||||
|
const childPolicies = currentRoute.routes ?? [];
|
||||||
|
|
||||||
|
const inheritedGrouping = hasInheritedProperties && inheritedProperties.group_by;
|
||||||
|
const noGrouping = isArray(groupBy) && groupBy[0] === '...';
|
||||||
|
const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0;
|
||||||
|
const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0;
|
||||||
|
|
||||||
const isEditable = canEditRoutes;
|
const isEditable = canEditRoutes;
|
||||||
const isDeletable = canDeleteRoutes && !isDefaultPolicy;
|
const isDeletable = canDeleteRoutes && !isDefaultPolicy;
|
||||||
|
|
||||||
@ -218,18 +214,26 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
/>
|
/>
|
||||||
</MetaText>
|
</MetaText>
|
||||||
)}
|
)}
|
||||||
{isGrouping && (
|
{!inheritedGrouping && (
|
||||||
|
<>
|
||||||
|
{customGrouping && (
|
||||||
<MetaText icon="layer-group" data-testid="grouping">
|
<MetaText icon="layer-group" data-testid="grouping">
|
||||||
<span>Grouped by</span>
|
<span>Grouped by</span>
|
||||||
<Strong>{groupBy.join(', ')}</Strong>
|
<Strong>{groupBy.join(', ')}</Strong>
|
||||||
</MetaText>
|
</MetaText>
|
||||||
)}
|
)}
|
||||||
{/* we only want to show "no grouping" on the root policy, children with empty groupBy will inherit from the parent policy */}
|
{singleGroup && (
|
||||||
{!isGrouping && isDefaultPolicy && (
|
<MetaText icon="layer-group">
|
||||||
|
<span>Single group</span>
|
||||||
|
</MetaText>
|
||||||
|
)}
|
||||||
|
{noGrouping && (
|
||||||
<MetaText icon="layer-group">
|
<MetaText icon="layer-group">
|
||||||
<span>Not grouping</span>
|
<span>Not grouping</span>
|
||||||
</MetaText>
|
</MetaText>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{hasMuteTimings && (
|
{hasMuteTimings && (
|
||||||
<MetaText icon="calendar-slash" data-testid="mute-timings">
|
<MetaText icon="calendar-slash" data-testid="mute-timings">
|
||||||
<span>Muted when</span>
|
<span>Muted when</span>
|
||||||
@ -253,44 +257,18 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.childPolicies}>
|
<div className={styles.childPolicies}>
|
||||||
{/* pass the "readOnly" prop from the parent, because if you can't edit the parent you can't edit children */}
|
{/* pass the "readOnly" prop from the parent, because if you can't edit the parent you can't edit children */}
|
||||||
{childPolicies.map((route) => {
|
{childPolicies.map((child) => {
|
||||||
// inherited properties are config properties that exist on the parent but not on currentRoute
|
const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);
|
||||||
const inheritableProperties: InhertitableProperties = pick(currentRoute, [
|
|
||||||
'receiver',
|
|
||||||
'group_by',
|
|
||||||
'group_wait',
|
|
||||||
'group_interval',
|
|
||||||
'repeat_interval',
|
|
||||||
'mute_time_intervals',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// TODO how to solve this TypeScript mystery
|
|
||||||
const inherited = merge(
|
|
||||||
reduce(
|
|
||||||
inheritableProperties,
|
|
||||||
(acc: Partial<Route> = {}, value, key) => {
|
|
||||||
// @ts-ignore
|
|
||||||
if (value !== undefined && route[key] === undefined) {
|
|
||||||
// @ts-ignore
|
|
||||||
acc[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
inheritedProperties
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Policy
|
<Policy
|
||||||
key={uniqueId()}
|
key={uniqueId()}
|
||||||
routeTree={routeTree}
|
routeTree={routeTree}
|
||||||
currentRoute={route}
|
currentRoute={child}
|
||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
contactPointsState={contactPointsState}
|
contactPointsState={contactPointsState}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
inheritedProperties={inherited}
|
inheritedProperties={childInheritedProperties}
|
||||||
onAddPolicy={onAddPolicy}
|
onAddPolicy={onAddPolicy}
|
||||||
onEditPolicy={onEditPolicy}
|
onEditPolicy={onEditPolicy}
|
||||||
onDeletePolicy={onDeletePolicy}
|
onDeletePolicy={onDeletePolicy}
|
||||||
@ -366,13 +344,14 @@ const InheritedProperties: FC<{ properties: InhertitableProperties }> = ({ prope
|
|||||||
content={
|
content={
|
||||||
<Stack direction="row" gap={0.5}>
|
<Stack direction="row" gap={0.5}>
|
||||||
{Object.entries(properties).map(([key, value]) => {
|
{Object.entries(properties).map(([key, value]) => {
|
||||||
// no idea how to do this with TypeScript
|
// no idea how to do this with TypeScript without type casting...
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={key}
|
key={key}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
label={routePropertyToLabel(key)}
|
label={routePropertyToLabel(key)}
|
||||||
value={<Strong>{Array.isArray(value) ? value.join(', ') : value}</Strong>}
|
// @ts-ignore
|
||||||
|
value={<Strong>{routePropertyToValue(key, value)}</Strong>}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -560,6 +539,29 @@ const routePropertyToLabel = (key: keyof InhertitableProperties): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const routePropertyToValue = (key: keyof InhertitableProperties, value: string | string[]): React.ReactNode => {
|
||||||
|
const isNotGrouping = key === 'group_by' && Array.isArray(value) && value[0] === '...';
|
||||||
|
const isSingleGroup = key === 'group_by' && Array.isArray(value) && value.length === 0;
|
||||||
|
|
||||||
|
if (isNotGrouping) {
|
||||||
|
return (
|
||||||
|
<Span variant="bodySmall" color="secondary">
|
||||||
|
Not grouping
|
||||||
|
</Span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSingleGroup) {
|
||||||
|
return (
|
||||||
|
<Span variant="bodySmall" color="secondary">
|
||||||
|
Single group
|
||||||
|
</Span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(value) ? value.join(', ') : value;
|
||||||
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
matcher: (label: string) => {
|
matcher: (label: string) => {
|
||||||
const { color, borderColor } = getTagColorsFromName(label);
|
const { color, borderColor } = getTagColorsFromName(label);
|
||||||
|
@ -6,7 +6,7 @@ export interface FormAmRoute {
|
|||||||
continue: boolean;
|
continue: boolean;
|
||||||
receiver: string;
|
receiver: string;
|
||||||
overrideGrouping: boolean;
|
overrideGrouping: boolean;
|
||||||
groupBy: string[];
|
groupBy?: string[];
|
||||||
overrideTimings: boolean;
|
overrideTimings: boolean;
|
||||||
groupWaitValue: string;
|
groupWaitValue: string;
|
||||||
groupIntervalValue: string;
|
groupIntervalValue: string;
|
||||||
|
@ -37,7 +37,7 @@ describe('formAmRouteToAmRoute', () => {
|
|||||||
const amRoute = formAmRouteToAmRoute('test', route, { id: 'root' });
|
const amRoute = formAmRouteToAmRoute('test', route, { id: 'root' });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(amRoute.group_by).toStrictEqual([]);
|
expect(amRoute.group_by).toStrictEqual(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,10 +56,23 @@ describe('formAmRouteToAmRoute', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('amRouteToFormAmRoute', () => {
|
describe('amRouteToFormAmRoute', () => {
|
||||||
|
describe('when called with empty group_by array', () => {
|
||||||
|
it('should set overrideGrouping true and groupBy empty', () => {
|
||||||
|
// Arrange
|
||||||
|
const amRoute = buildAmRoute({ group_by: [] });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const formRoute = amRouteToFormAmRoute(amRoute);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(formRoute.groupBy).toStrictEqual([]);
|
||||||
|
expect(formRoute.overrideGrouping).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when called with empty group_by', () => {
|
describe('when called with empty group_by', () => {
|
||||||
it.each`
|
it.each`
|
||||||
group_by
|
group_by
|
||||||
${[]}
|
|
||||||
${null}
|
${null}
|
||||||
${undefined}
|
${undefined}
|
||||||
`("when group_by is '$group_by', should set overrideGrouping false", ({ group_by }) => {
|
`("when group_by is '$group_by', should set overrideGrouping false", ({ group_by }) => {
|
||||||
@ -70,7 +83,7 @@ describe('amRouteToFormAmRoute', () => {
|
|||||||
const formRoute = amRouteToFormAmRoute(amRoute);
|
const formRoute = amRouteToFormAmRoute(amRoute);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(formRoute.groupBy).toStrictEqual([]);
|
expect(formRoute.groupBy).toStrictEqual(undefined);
|
||||||
expect(formRoute.overrideGrouping).toBe(false);
|
expect(formRoute.overrideGrouping).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -108,8 +108,8 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo
|
|||||||
],
|
],
|
||||||
continue: route.continue ?? false,
|
continue: route.continue ?? false,
|
||||||
receiver: route.receiver ?? '',
|
receiver: route.receiver ?? '',
|
||||||
overrideGrouping: Array.isArray(route.group_by) && route.group_by.length !== 0,
|
overrideGrouping: Array.isArray(route.group_by) && route.group_by.length > 0,
|
||||||
groupBy: route.group_by ?? [],
|
groupBy: route.group_by ?? undefined,
|
||||||
overrideTimings: [route.group_wait, route.group_interval, route.repeat_interval].some(Boolean),
|
overrideTimings: [route.group_wait, route.group_interval, route.repeat_interval].some(Boolean),
|
||||||
groupWaitValue: route.group_wait ?? '',
|
groupWaitValue: route.group_wait ?? '',
|
||||||
groupIntervalValue: route.group_interval ?? '',
|
groupIntervalValue: route.group_interval ?? '',
|
||||||
@ -137,16 +137,19 @@ export const formAmRouteToAmRoute = (
|
|||||||
receiver,
|
receiver,
|
||||||
} = formAmRoute;
|
} = formAmRoute;
|
||||||
|
|
||||||
const group_by = overrideGrouping && groupBy ? groupBy : [];
|
// "undefined" means "inherit from the parent policy", currently supported by group_by, group_wait, group_interval, and repeat_interval
|
||||||
|
const INHERIT_FROM_PARENT = undefined;
|
||||||
|
|
||||||
|
const group_by = overrideGrouping ? groupBy : INHERIT_FROM_PARENT;
|
||||||
|
|
||||||
const overrideGroupWait = overrideTimings && groupWaitValue;
|
const overrideGroupWait = overrideTimings && groupWaitValue;
|
||||||
const group_wait = overrideGroupWait ? groupWaitValue : undefined;
|
const group_wait = overrideGroupWait ? groupWaitValue : INHERIT_FROM_PARENT;
|
||||||
|
|
||||||
const overrideGroupInterval = overrideTimings && groupIntervalValue;
|
const overrideGroupInterval = overrideTimings && groupIntervalValue;
|
||||||
const group_interval = overrideGroupInterval ? groupIntervalValue : undefined;
|
const group_interval = overrideGroupInterval ? groupIntervalValue : INHERIT_FROM_PARENT;
|
||||||
|
|
||||||
const overrideRepeatInterval = overrideTimings && repeatIntervalValue;
|
const overrideRepeatInterval = overrideTimings && repeatIntervalValue;
|
||||||
const repeat_interval = overrideRepeatInterval ? repeatIntervalValue : undefined;
|
const repeat_interval = overrideRepeatInterval ? repeatIntervalValue : INHERIT_FROM_PARENT;
|
||||||
const object_matchers = formAmRoute.object_matchers
|
const object_matchers = formAmRoute.object_matchers
|
||||||
?.filter((route) => route.name && route.value && route.operator)
|
?.filter((route) => route.name && route.value && route.operator)
|
||||||
.map(({ name, operator, value }) => [name, operator, value] as ObjectMatcher);
|
.map(({ name, operator, value }) => [name, operator, value] as ObjectMatcher);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { MatcherOperator, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
import { MatcherOperator, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
import { findMatchingRoutes, normalizeRoute } from './notification-policies';
|
import { findMatchingRoutes, normalizeRoute, getInheritedProperties } from './notification-policies';
|
||||||
|
|
||||||
import 'core-js/stable/structured-clone';
|
import 'core-js/stable/structured-clone';
|
||||||
|
|
||||||
@ -121,6 +121,130 @@ describe('findMatchingRoutes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getInheritedProperties()', () => {
|
||||||
|
describe('group_by: []', () => {
|
||||||
|
it('should get group_by: [] from parent', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
group_by: ['label'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
group_by: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child);
|
||||||
|
expect(childInherited).toHaveProperty('group_by', ['label']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get group_by: [] from parent inherited properties', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
group_by: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
group_by: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const parentInherited = { group_by: ['label'] };
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child, parentInherited);
|
||||||
|
expect(childInherited).toHaveProperty('group_by', ['label']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not inherit if the child overrides an inheritable value (group_by)', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
group_by: ['parentLabel'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
group_by: ['childLabel'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child);
|
||||||
|
expect(childInherited).not.toHaveProperty('group_by');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should inherit if group_by is undefined', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
group_by: ['label'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
group_by: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child);
|
||||||
|
expect(childInherited).toHaveProperty('group_by', ['label']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('regular "undefined" values', () => {
|
||||||
|
it('should compute inherited properties being undefined', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
group_wait: '10s',
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
};
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child);
|
||||||
|
expect(childInherited).toHaveProperty('group_wait', '10s');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compute inherited properties being undefined from parent inherited properties', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
};
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child, { group_wait: '10s' });
|
||||||
|
expect(childInherited).toHaveProperty('group_wait', '10s');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not inherit if the child overrides an inheritable value', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
group_wait: '10s',
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
group_wait: '30s',
|
||||||
|
};
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child);
|
||||||
|
expect(childInherited).not.toHaveProperty('group_wait');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not inherit if the child overrides an inheritable value and the parent inherits', () => {
|
||||||
|
const parent: Route = {
|
||||||
|
receiver: 'PARENT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: Route = {
|
||||||
|
receiver: 'CHILD',
|
||||||
|
group_wait: '30s',
|
||||||
|
};
|
||||||
|
|
||||||
|
const childInherited = getInheritedProperties(parent, child, { group_wait: '60s' });
|
||||||
|
expect(childInherited).not.toHaveProperty('group_wait');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('normalizeRoute', () => {
|
describe('normalizeRoute', () => {
|
||||||
it('should map matchers property to object_matchers', function () {
|
it('should map matchers property to object_matchers', function () {
|
||||||
const route: RouteWithID = {
|
const route: RouteWithID = {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { isArray, merge, pick, reduce } from 'lodash';
|
||||||
|
|
||||||
import { AlertmanagerGroup, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertmanagerGroup, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
import { Label, normalizeMatchers, labelsMatchObjectMatchers } from './matchers';
|
import { Label, normalizeMatchers, labelsMatchObjectMatchers } from './matchers';
|
||||||
@ -82,4 +84,53 @@ function findMatchingAlertGroups(
|
|||||||
}, matchingGroups);
|
}, matchingGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { findMatchingAlertGroups, findMatchingRoutes };
|
export type InhertitableProperties = Pick<
|
||||||
|
Route,
|
||||||
|
'receiver' | 'group_by' | 'group_wait' | 'group_interval' | 'repeat_interval' | 'mute_time_intervals'
|
||||||
|
>;
|
||||||
|
|
||||||
|
// inherited properties are config properties that exist on the parent route (or its inherited properties) but not on the child route
|
||||||
|
function getInheritedProperties(
|
||||||
|
parentRoute: Route,
|
||||||
|
childRoute: Route,
|
||||||
|
propertiesParentInherited?: Partial<InhertitableProperties>
|
||||||
|
) {
|
||||||
|
const fullParentProperties = merge({}, parentRoute, propertiesParentInherited);
|
||||||
|
|
||||||
|
const inheritableProperties: InhertitableProperties = pick(fullParentProperties, [
|
||||||
|
'receiver',
|
||||||
|
'group_by',
|
||||||
|
'group_wait',
|
||||||
|
'group_interval',
|
||||||
|
'repeat_interval',
|
||||||
|
'mute_time_intervals',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO how to solve this TypeScript mystery?
|
||||||
|
const inherited = reduce(
|
||||||
|
inheritableProperties,
|
||||||
|
(inheritedProperties: Partial<Route> = {}, parentValue, property) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const inheritFromParent = parentValue !== undefined && childRoute[property] === undefined;
|
||||||
|
const inheritEmptyGroupByFromParent =
|
||||||
|
property === 'group_by' && isArray(childRoute[property]) && childRoute[property]?.length === 0;
|
||||||
|
|
||||||
|
if (inheritFromParent) {
|
||||||
|
// @ts-ignore
|
||||||
|
inheritedProperties[property] = parentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inheritEmptyGroupByFromParent) {
|
||||||
|
// @ts-ignore
|
||||||
|
inheritedProperties[property] = parentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inheritedProperties;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
return inherited;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { findMatchingAlertGroups, findMatchingRoutes, getInheritedProperties };
|
||||||
|
Loading…
Reference in New Issue
Block a user