mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Allow inserting before or after existing policy (#83704)
This commit is contained in:
parent
78478dc235
commit
388e0c27f2
@ -1,6 +1,6 @@
|
||||
import { SortOrder } from '@grafana/schema';
|
||||
|
||||
import { sortValues } from './arrayUtils';
|
||||
import { insertAfterImmutably, insertBeforeImmutably, sortValues } from './arrayUtils';
|
||||
|
||||
describe('arrayUtils', () => {
|
||||
describe('sortValues', () => {
|
||||
@ -30,4 +30,52 @@ describe('arrayUtils', () => {
|
||||
expect(sorted).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertBeforeImmutably', () => {
|
||||
const original = [1, 2, 3];
|
||||
|
||||
it.each`
|
||||
item | index | expected
|
||||
${4} | ${1} | ${[1, 4, 2, 3]}
|
||||
${4} | ${2} | ${[1, 2, 4, 3]}
|
||||
${0} | ${0} | ${[0, 1, 2, 3]}
|
||||
`('add $item before $index', ({ item, index, expected }) => {
|
||||
const output = insertBeforeImmutably(original, item, index);
|
||||
expect(output).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should throw when out of bounds', () => {
|
||||
expect(() => {
|
||||
insertBeforeImmutably([], 1, -1);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
insertBeforeImmutably([], 1, 3);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertAfterImmutably', () => {
|
||||
const original = [1, 2, 3];
|
||||
|
||||
it.each`
|
||||
item | index | expected
|
||||
${4} | ${1} | ${[1, 2, 4, 3]}
|
||||
${4} | ${0} | ${[1, 4, 2, 3]}
|
||||
${4} | ${2} | ${[1, 2, 3, 4]}
|
||||
`('add $item after $index', ({ item, index, expected }) => {
|
||||
const output = insertAfterImmutably(original, item, index);
|
||||
expect(output).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should throw when out of bounds', () => {
|
||||
expect(() => {
|
||||
insertAfterImmutably([], 1, -1);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
insertAfterImmutably([], 1, 3);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -7,6 +7,30 @@ export function moveItemImmutably<T>(arr: T[], from: number, to: number) {
|
||||
return clone;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function insertBeforeImmutably<T>(array: T[], item: T, index: number): T[] {
|
||||
if (index < 0 || index > array.length) {
|
||||
throw new Error('Index out of bounds');
|
||||
}
|
||||
|
||||
const clone = [...array];
|
||||
clone.splice(index, 0, item);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function insertAfterImmutably<T>(array: T[], item: T, index: number): T[] {
|
||||
if (index < 0 || index > array.length) {
|
||||
throw new Error('Index out of bounds');
|
||||
}
|
||||
|
||||
const clone = [...array];
|
||||
clone.splice(index + 1, 0, item);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a sort order and a value, return a function that can be used to sort values
|
||||
* Null/undefined/empty string values are always sorted to the end regardless of the sort order provided
|
||||
|
@ -36,7 +36,12 @@ import { useRouteGroupsMatcher } from './useRouteGroupsMatcher';
|
||||
import { addUniqueIdentifierToRoute } from './utils/amroutes';
|
||||
import { computeInheritedTree } from './utils/notification-policies';
|
||||
import { initialAsyncRequestState } from './utils/redux';
|
||||
import { addRouteToParentRoute, mergePartialAmRouteWithRouteTree, omitRouteFromRouteTree } from './utils/routeTree';
|
||||
import {
|
||||
InsertPosition,
|
||||
addRouteToReferenceRoute,
|
||||
mergePartialAmRouteWithRouteTree,
|
||||
omitRouteFromRouteTree,
|
||||
} from './utils/routeTree';
|
||||
|
||||
enum ActiveTab {
|
||||
NotificationPolicies = 'notification_policies',
|
||||
@ -132,12 +137,18 @@ const AmRoutes = () => {
|
||||
updateRouteTree(newRouteTree);
|
||||
}
|
||||
|
||||
function handleAdd(partialRoute: Partial<FormAmRoute>, parentRoute: RouteWithID) {
|
||||
function handleAdd(partialRoute: Partial<FormAmRoute>, referenceRoute: RouteWithID, insertPosition: InsertPosition) {
|
||||
if (!rootRoute) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRouteTree = addRouteToParentRoute(selectedAlertmanager ?? '', partialRoute, parentRoute, rootRoute);
|
||||
const newRouteTree = addRouteToReferenceRoute(
|
||||
selectedAlertmanager ?? '',
|
||||
partialRoute,
|
||||
referenceRoute,
|
||||
rootRoute,
|
||||
insertPosition
|
||||
);
|
||||
updateRouteTree(newRouteTree);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
|
||||
import { FormAmRoute } from '../../types/amroutes';
|
||||
import { MatcherFormatter } from '../../utils/matchers';
|
||||
import { InsertPosition } from '../../utils/routeTree';
|
||||
import { AlertGroup } from '../alert-groups/AlertGroup';
|
||||
import { useGetAmRouteReceiverWithGrafanaAppTypes } from '../receivers/grafanaAppReceivers/grafanaApp';
|
||||
|
||||
@ -21,24 +22,28 @@ import { AmRoutesExpandedForm } from './EditNotificationPolicyForm';
|
||||
import { Matchers } from './Matchers';
|
||||
|
||||
type ModalHook<T = undefined> = [JSX.Element, (item: T) => void, () => void];
|
||||
type AddModalHook<T = undefined> = [JSX.Element, (item: T, position: InsertPosition) => void, () => void];
|
||||
type EditModalHook = [JSX.Element, (item: RouteWithID, isDefaultRoute?: boolean) => void, () => void];
|
||||
|
||||
const useAddPolicyModal = (
|
||||
receivers: Receiver[] = [],
|
||||
handleAdd: (route: Partial<FormAmRoute>, parentRoute: RouteWithID) => void,
|
||||
handleAdd: (route: Partial<FormAmRoute>, referenceRoute: RouteWithID, position: InsertPosition) => void,
|
||||
loading: boolean
|
||||
): ModalHook<RouteWithID> => {
|
||||
): AddModalHook<RouteWithID> => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [parentRoute, setParentRoute] = useState<RouteWithID>();
|
||||
const [insertPosition, setInsertPosition] = useState<InsertPosition | undefined>(undefined);
|
||||
const [referenceRoute, setReferenceRoute] = useState<RouteWithID>();
|
||||
const AmRouteReceivers = useGetAmRouteReceiverWithGrafanaAppTypes(receivers);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setParentRoute(undefined);
|
||||
setReferenceRoute(undefined);
|
||||
setInsertPosition(undefined);
|
||||
setShowModal(false);
|
||||
}, []);
|
||||
|
||||
const handleShow = useCallback((parentRoute: RouteWithID) => {
|
||||
setParentRoute(parentRoute);
|
||||
const handleShow = useCallback((referenceRoute: RouteWithID, position: InsertPosition) => {
|
||||
setReferenceRoute(referenceRoute);
|
||||
setInsertPosition(position);
|
||||
setShowModal(true);
|
||||
}, []);
|
||||
|
||||
@ -57,9 +62,13 @@ const useAddPolicyModal = (
|
||||
<AmRoutesExpandedForm
|
||||
receivers={AmRouteReceivers}
|
||||
defaults={{
|
||||
groupBy: parentRoute?.group_by,
|
||||
groupBy: referenceRoute?.group_by,
|
||||
}}
|
||||
onSubmit={(newRoute) => {
|
||||
if (referenceRoute && insertPosition) {
|
||||
handleAdd(newRoute, referenceRoute, insertPosition);
|
||||
}
|
||||
}}
|
||||
onSubmit={(newRoute) => parentRoute && handleAdd(newRoute, parentRoute)}
|
||||
actionButtons={
|
||||
<Modal.ButtonRow>
|
||||
<Button type="button" variant="secondary" onClick={handleDismiss} fill="outline">
|
||||
@ -71,7 +80,7 @@ const useAddPolicyModal = (
|
||||
/>
|
||||
</Modal>
|
||||
),
|
||||
[AmRouteReceivers, handleAdd, handleDismiss, loading, parentRoute, showModal]
|
||||
[AmRouteReceivers, handleAdd, handleDismiss, insertPosition, loading, referenceRoute, showModal]
|
||||
);
|
||||
|
||||
return [modalElement, handleShow, handleDismiss];
|
||||
|
@ -37,6 +37,7 @@ import { getAmMatcherFormatter } from '../../utils/alertmanager';
|
||||
import { MatcherFormatter, normalizeMatchers } from '../../utils/matchers';
|
||||
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
||||
import { InheritableProperties, getInheritedProperties } from '../../utils/notification-policies';
|
||||
import { InsertPosition } from '../../utils/routeTree';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { HoverCard } from '../HoverCard';
|
||||
import { Label } from '../Label';
|
||||
@ -67,7 +68,7 @@ interface PolicyComponentProps {
|
||||
currentRoute: RouteWithID;
|
||||
alertManagerSourceName: string;
|
||||
onEditPolicy: (route: RouteWithID, isDefault?: boolean, isAutogenerated?: boolean) => void;
|
||||
onAddPolicy: (route: RouteWithID) => void;
|
||||
onAddPolicy: (route: RouteWithID, position: InsertPosition) => void;
|
||||
onDeletePolicy: (route: RouteWithID) => void;
|
||||
onShowAlertInstances: (
|
||||
alertGroups: AlertmanagerGroup[],
|
||||
@ -133,12 +134,14 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
|
||||
const allChildPolicies = currentRoute.routes ?? [];
|
||||
|
||||
// filter chld policies that match
|
||||
// filter child policies that match
|
||||
const childPolicies = filtersApplied
|
||||
? // filter by the ones that belong to the path that matches the filters
|
||||
allChildPolicies.filter((policy) => routesPath.some((route: RouteWithID) => route.id === policy.id))
|
||||
: allChildPolicies;
|
||||
|
||||
const hasChildPolicies = childPolicies.length > 0;
|
||||
|
||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||
const matchingAlertGroups = matchingInstancesPreview?.groupsMap?.get(currentRoute.id);
|
||||
|
||||
@ -152,10 +155,9 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
AlertmanagerAction.ViewAutogeneratedPolicyTree
|
||||
);
|
||||
|
||||
// collapsible policies variables
|
||||
const isThisPolicyCollapsible = useShouldPolicyBeCollapsible(currentRoute);
|
||||
const [isBranchOpen, toggleBranchOpen] = useToggle(false);
|
||||
const renderChildPolicies = (isThisPolicyCollapsible && isBranchOpen) || !isThisPolicyCollapsible;
|
||||
// we collapse the auto-generated policies by default
|
||||
const isAutogeneratedPolicyRoot = isAutoGeneratedRootAndSimplifiedEnabled(currentRoute);
|
||||
const [showPolicyChildren, togglePolicyChildren] = useToggle(isAutogeneratedPolicyRoot ? false : true);
|
||||
|
||||
const groupBy = currentRoute.group_by;
|
||||
const muteTimings = currentRoute.mute_time_intervals ?? [];
|
||||
@ -174,8 +176,6 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
|
||||
const [visibleChildPolicies, setVisibleChildPolicies] = useState(POLICIES_PER_PAGE);
|
||||
|
||||
const isAutogeneratedPolicyRoot = isAutoGeneratedRootAndSimplifiedEnabled(currentRoute);
|
||||
|
||||
// build the menu actions for our policy
|
||||
const dropdownMenuActions: JSX.Element[] = useCreateDropdownMenuActions(
|
||||
isAutoGenerated,
|
||||
@ -227,13 +227,13 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
{/* Matchers and actions */}
|
||||
<div>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
{isThisPolicyCollapsible && (
|
||||
{hasChildPolicies ? (
|
||||
<IconButton
|
||||
name={isBranchOpen ? 'angle-down' : 'angle-right'}
|
||||
onClick={toggleBranchOpen}
|
||||
aria-label={isBranchOpen ? 'Collapse' : 'Expand'}
|
||||
name={showPolicyChildren ? 'angle-down' : 'angle-right'}
|
||||
onClick={togglePolicyChildren}
|
||||
aria-label={showPolicyChildren ? 'Collapse' : 'Expand'}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
{isImmutablePolicy ? (
|
||||
isAutogeneratedPolicyRoot ? (
|
||||
<AutogeneratedRootIndicator />
|
||||
@ -253,16 +253,51 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
{!isAutoGenerated && !readOnly && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
size="sm"
|
||||
onClick={() => onAddPolicy(currentRoute)}
|
||||
disabled={provisioned}
|
||||
type="button"
|
||||
>
|
||||
New nested policy
|
||||
</Button>
|
||||
{isDefaultPolicy ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
size="sm"
|
||||
disabled={provisioned}
|
||||
type="button"
|
||||
onClick={() => onAddPolicy(currentRoute, 'child')}
|
||||
>
|
||||
New child policy
|
||||
</Button>
|
||||
) : (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
label="Insert above"
|
||||
icon="arrow-up"
|
||||
onClick={() => onAddPolicy(currentRoute, 'above')}
|
||||
/>
|
||||
<Menu.Item
|
||||
label="Insert below"
|
||||
icon="arrow-down"
|
||||
onClick={() => onAddPolicy(currentRoute, 'below')}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
label="New child policy"
|
||||
icon="plus"
|
||||
onClick={() => onAddPolicy(currentRoute, 'child')}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={provisioned}
|
||||
icon="angle-down"
|
||||
type="button"
|
||||
>
|
||||
Add new policy
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</ConditionalWrap>
|
||||
</Authorize>
|
||||
)}
|
||||
@ -302,7 +337,7 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.childPolicies}>
|
||||
{renderChildPolicies && (
|
||||
{showPolicyChildren && (
|
||||
<>
|
||||
{pageOfChildren.map((child) => {
|
||||
const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);
|
||||
@ -353,24 +388,6 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* This function returns if the policy should be collapsible or not.
|
||||
* Add here conditions for policies that should be collapsible.
|
||||
*/
|
||||
function useShouldPolicyBeCollapsible(route: RouteWithID): boolean {
|
||||
const childrenCount = route.routes?.length ?? 0;
|
||||
const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.ViewAutogeneratedPolicyTree
|
||||
);
|
||||
const isAutoGeneratedRoot =
|
||||
childrenCount > 0 &&
|
||||
isSupportedToSeeAutogeneratedChunk &&
|
||||
isAllowedToSeeAutogeneratedChunk &&
|
||||
isAutoGeneratedRootAndSimplifiedEnabled(route);
|
||||
// let's add here more conditions for policies that should be collapsible
|
||||
|
||||
return isAutoGeneratedRoot;
|
||||
}
|
||||
|
||||
interface MetadataRowProps {
|
||||
matchingInstancesPreview: { groupsMap?: Map<string, AlertmanagerGroup[]>; enabled: boolean };
|
||||
|
@ -0,0 +1,87 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`addRouteToReferenceRoute should be able to add above 1`] = `
|
||||
{
|
||||
"id": "route-1",
|
||||
"routes": [
|
||||
{
|
||||
"id": "route-2",
|
||||
},
|
||||
{
|
||||
"continue": undefined,
|
||||
"group_by": undefined,
|
||||
"group_interval": undefined,
|
||||
"group_wait": undefined,
|
||||
"match": undefined,
|
||||
"match_re": undefined,
|
||||
"matchers": undefined,
|
||||
"mute_time_intervals": undefined,
|
||||
"object_matchers": undefined,
|
||||
"receiver": "new-route",
|
||||
"repeat_interval": undefined,
|
||||
"routes": undefined,
|
||||
},
|
||||
{
|
||||
"id": "route-3",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`addRouteToReferenceRoute should be able to add as child 1`] = `
|
||||
{
|
||||
"id": "route-1",
|
||||
"routes": [
|
||||
{
|
||||
"id": "route-2",
|
||||
},
|
||||
{
|
||||
"id": "route-3",
|
||||
"routes": [
|
||||
{
|
||||
"continue": undefined,
|
||||
"group_by": undefined,
|
||||
"group_interval": undefined,
|
||||
"group_wait": undefined,
|
||||
"match": undefined,
|
||||
"match_re": undefined,
|
||||
"matchers": undefined,
|
||||
"mute_time_intervals": undefined,
|
||||
"object_matchers": undefined,
|
||||
"receiver": "new-route",
|
||||
"repeat_interval": undefined,
|
||||
"routes": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`addRouteToReferenceRoute should be able to add below 1`] = `
|
||||
{
|
||||
"id": "route-1",
|
||||
"routes": [
|
||||
{
|
||||
"id": "route-2",
|
||||
},
|
||||
{
|
||||
"id": "route-3",
|
||||
},
|
||||
{
|
||||
"continue": undefined,
|
||||
"group_by": undefined,
|
||||
"group_interval": undefined,
|
||||
"group_wait": undefined,
|
||||
"match": undefined,
|
||||
"match_re": undefined,
|
||||
"matchers": undefined,
|
||||
"mute_time_intervals": undefined,
|
||||
"object_matchers": undefined,
|
||||
"receiver": "new-route",
|
||||
"repeat_interval": undefined,
|
||||
"routes": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
87
public/app/features/alerting/unified/utils/routeTree.test.ts
Normal file
87
public/app/features/alerting/unified/utils/routeTree.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { FormAmRoute } from '../types/amroutes';
|
||||
|
||||
import { GRAFANA_DATASOURCE_NAME } from './datasource';
|
||||
import { addRouteToReferenceRoute, findRouteInTree, omitRouteFromRouteTree } from './routeTree';
|
||||
|
||||
describe('findRouteInTree', () => {
|
||||
it('should find the correct route', () => {
|
||||
const needle: RouteWithID = { id: 'route-2' };
|
||||
|
||||
const root: RouteWithID = {
|
||||
id: 'route-0',
|
||||
routes: [{ id: 'route-1' }, needle, { id: 'route-3', routes: [{ id: 'route-4' }] }],
|
||||
};
|
||||
|
||||
expect(findRouteInTree(root, { id: 'route-2' })).toStrictEqual([needle, root, 1]);
|
||||
});
|
||||
|
||||
it('should return undefined for unknown route', () => {
|
||||
const root: RouteWithID = {
|
||||
id: 'route-0',
|
||||
routes: [{ id: 'route-1' }],
|
||||
};
|
||||
|
||||
expect(findRouteInTree(root, { id: 'none' })).toStrictEqual([undefined, undefined, undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRouteToReferenceRoute', () => {
|
||||
const targetRoute = { id: 'route-3' };
|
||||
const root: RouteWithID = {
|
||||
id: 'route-1',
|
||||
routes: [{ id: 'route-2' }, targetRoute],
|
||||
};
|
||||
|
||||
const newRoute: Partial<FormAmRoute> = {
|
||||
id: 'new-route',
|
||||
receiver: 'new-route',
|
||||
};
|
||||
|
||||
it('should be able to add above', () => {
|
||||
expect(addRouteToReferenceRoute(GRAFANA_DATASOURCE_NAME, newRoute, targetRoute, root, 'above')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be able to add below', () => {
|
||||
expect(addRouteToReferenceRoute(GRAFANA_DATASOURCE_NAME, newRoute, targetRoute, root, 'below')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be able to add as child', () => {
|
||||
expect(addRouteToReferenceRoute(GRAFANA_DATASOURCE_NAME, newRoute, targetRoute, root, 'child')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should throw if target route does not exist', () => {
|
||||
expect(() =>
|
||||
addRouteToReferenceRoute(GRAFANA_DATASOURCE_NAME, newRoute, { id: 'unknown' }, root, 'child')
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('omitRouteFromRouteTree', () => {
|
||||
it('should omit route from tree', () => {
|
||||
const tree: RouteWithID = {
|
||||
id: 'route-1',
|
||||
receiver: 'root',
|
||||
routes: [
|
||||
{ id: 'route-2', receiver: 'receiver-2' },
|
||||
{ id: 'route-3', receiver: 'receiver-3' },
|
||||
],
|
||||
};
|
||||
|
||||
expect(omitRouteFromRouteTree({ id: 'route-2' }, tree)).toStrictEqual({
|
||||
receiver: 'root',
|
||||
routes: [{ receiver: 'receiver-3', routes: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when removing root route from tree', () => {
|
||||
const tree: RouteWithID = {
|
||||
id: 'route-1',
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
omitRouteFromRouteTree(tree, { id: 'route-1' });
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
@ -2,8 +2,10 @@
|
||||
* Various helper functions to modify (immutably) the route tree, aka "notification policies"
|
||||
*/
|
||||
|
||||
import { produce } from 'immer';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { insertAfterImmutably, insertBeforeImmutably } from '@grafana/data/src/utils/arrayUtils';
|
||||
import { Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { FormAmRoute } from '../types/amroutes';
|
||||
@ -74,44 +76,78 @@ export const omitRouteFromRouteTree = (findRoute: RouteWithID, routeTree: RouteW
|
||||
return findAndOmit(routeTree);
|
||||
};
|
||||
|
||||
export type InsertPosition = 'above' | 'below' | 'child';
|
||||
|
||||
// add a new route to a parent route
|
||||
export const addRouteToParentRoute = (
|
||||
export const addRouteToReferenceRoute = (
|
||||
alertManagerSourceName: string,
|
||||
partialFormRoute: Partial<FormAmRoute>,
|
||||
parentRoute: RouteWithID,
|
||||
routeTree: RouteWithID
|
||||
referenceRoute: RouteWithID,
|
||||
routeTree: RouteWithID,
|
||||
position: InsertPosition
|
||||
): Route => {
|
||||
const newRoute = formAmRouteToAmRoute(alertManagerSourceName, partialFormRoute, routeTree);
|
||||
|
||||
function findAndAdd(currentRoute: RouteWithID): RouteWithID {
|
||||
if (currentRoute.id === parentRoute.id) {
|
||||
return {
|
||||
...currentRoute,
|
||||
// TODO fix this typescript exception, it's... complicated
|
||||
// @ts-ignore
|
||||
routes: currentRoute.routes?.concat(newRoute),
|
||||
};
|
||||
return produce(routeTree, (draftTree) => {
|
||||
const [routeInTree, parentRoute, positionInParent] = findRouteInTree(draftTree, referenceRoute);
|
||||
|
||||
if (routeInTree === undefined || parentRoute === undefined || positionInParent === undefined) {
|
||||
throw new Error(`could not find reference route "${referenceRoute.id}" in tree`);
|
||||
}
|
||||
|
||||
return {
|
||||
...currentRoute,
|
||||
routes: currentRoute.routes?.map(findAndAdd),
|
||||
};
|
||||
}
|
||||
// if user wants to insert new child policy, append to the bottom of children
|
||||
if (position === 'child') {
|
||||
if (routeInTree.routes) {
|
||||
routeInTree.routes.push(newRoute);
|
||||
} else {
|
||||
routeInTree.routes = [newRoute];
|
||||
}
|
||||
}
|
||||
|
||||
function findAndOmitId(currentRoute: RouteWithID): Route {
|
||||
return omit(
|
||||
{
|
||||
...currentRoute,
|
||||
routes: currentRoute.routes?.map(findAndOmitId),
|
||||
},
|
||||
'id'
|
||||
);
|
||||
}
|
||||
// insert new policy before / above the referenceRoute
|
||||
if (position === 'above') {
|
||||
parentRoute.routes = insertBeforeImmutably(parentRoute.routes ?? [], newRoute, positionInParent);
|
||||
}
|
||||
|
||||
return findAndOmitId(findAndAdd(routeTree));
|
||||
// insert new policy after / below the referenceRoute
|
||||
if (position === 'below') {
|
||||
parentRoute.routes = insertAfterImmutably(parentRoute.routes ?? [], newRoute, positionInParent);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
type RouteMatch = Route | undefined;
|
||||
|
||||
export function findRouteInTree(
|
||||
routeTree: RouteWithID,
|
||||
referenceRoute: RouteWithID
|
||||
): [matchingRoute: RouteMatch, parentRoute: RouteMatch, positionInParent: number | undefined] {
|
||||
let matchingRoute: RouteMatch;
|
||||
let matchingRouteParent: RouteMatch;
|
||||
let matchingRoutePositionInParent: number | undefined;
|
||||
|
||||
// recurse through the tree to find the matching route, its parent and the position of the route in the parent
|
||||
function findRouteInTree(currentRoute: RouteWithID, index: number, parentRoute: RouteWithID) {
|
||||
if (matchingRoute) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentRoute.id === referenceRoute.id) {
|
||||
matchingRoute = currentRoute;
|
||||
matchingRouteParent = parentRoute;
|
||||
matchingRoutePositionInParent = index;
|
||||
}
|
||||
|
||||
if (currentRoute.routes) {
|
||||
currentRoute.routes.forEach((route, index) => findRouteInTree(route, index, currentRoute));
|
||||
}
|
||||
}
|
||||
|
||||
findRouteInTree(routeTree, 0, routeTree);
|
||||
|
||||
return [matchingRoute, matchingRouteParent, matchingRoutePositionInParent];
|
||||
}
|
||||
|
||||
export function findExistingRoute(id: string, routeTree: RouteWithID): RouteWithID | undefined {
|
||||
return routeTree.id === id ? routeTree : routeTree.routes?.find((route) => findExistingRoute(id, route));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user