Alerting: Add pagination and improved search for notification policies (#81535)

pull/82920/head
Sonia Aguilar 1 year ago committed by GitHub
parent 7422a90e8b
commit 5de17432f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .betterer.results
  2. 6
      .betterer.results.json
  3. 32
      public/app/features/alerting/unified/NotificationPolicies.test.tsx
  4. 87
      public/app/features/alerting/unified/NotificationPolicies.tsx
  5. 281
      public/app/features/alerting/unified/__snapshots__/NotificationPolicies.test.tsx.snap
  6. 73
      public/app/features/alerting/unified/components/notification-policies/Filters.tsx
  7. 88
      public/app/features/alerting/unified/components/notification-policies/Policy.tsx

@ -1816,9 +1816,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"]
],
"public/app/features/alerting/unified/components/notification-policies/Filters.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/alerting/unified/components/notification-policies/Matchers.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]

@ -2084,12 +2084,6 @@
"count": 4
}
],
"/public/app/features/alerting/unified/components/notification-policies/Filters.tsx": [
{
"message": "Styles should be written using objects.",
"count": 1
}
],
"/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx": [
{
"message": "Styles should be written using objects.",

@ -23,7 +23,7 @@ import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from '
import { alertmanagerApi } from './api/alertmanagerApi';
import { discoverAlertmanagerFeatures } from './api/buildInfo';
import * as grafanaApp from './components/receivers/grafanaAppReceivers/grafanaApp';
import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
import { MockDataSourceSrv, mockDataSource, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
import { defaultGroupBy } from './utils/amroutes';
import { getAllDataSources } from './utils/config';
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
@ -759,18 +759,22 @@ describe('findRoutesMatchingFilters', () => {
],
};
it('should not filter when we do not have any valid filters', () => {
expect(findRoutesMatchingFilters(simpleRouteTree, {})).toHaveProperty('filtersApplied', false);
});
it('should not match non-existing', () => {
expect(
findRoutesMatchingFilters(simpleRouteTree, {
labelMatchersFilter: [['foo', MatcherOperator.equal, 'bar']],
})
).toHaveLength(0);
}).matchedRoutesWithPath.size
).toBe(0);
expect(
findRoutesMatchingFilters(simpleRouteTree, {
contactPointFilter: 'does-not-exist',
})
).toHaveLength(0);
const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
contactPointFilter: 'does-not-exist',
});
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with only label matchers', () => {
@ -778,8 +782,7 @@ describe('findRoutesMatchingFilters', () => {
labelMatchersFilter: [['hello', MatcherOperator.equal, 'world']],
});
expect(matchingRoutes).toHaveLength(1);
expect(matchingRoutes[0]).toHaveProperty('id', '1');
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with only contact point and inheritance', () => {
@ -787,9 +790,7 @@ describe('findRoutesMatchingFilters', () => {
contactPointFilter: 'simple-receiver',
});
expect(matchingRoutes).toHaveLength(2);
expect(matchingRoutes[0]).toHaveProperty('id', '1');
expect(matchingRoutes[1]).toHaveProperty('id', '2');
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with non-intersecting filters', () => {
@ -798,7 +799,7 @@ describe('findRoutesMatchingFilters', () => {
contactPointFilter: 'does-not-exist',
});
expect(matchingRoutes).toHaveLength(0);
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with all filters', () => {
@ -807,8 +808,7 @@ describe('findRoutesMatchingFilters', () => {
contactPointFilter: 'simple-receiver',
});
expect(matchingRoutes).toHaveLength(1);
expect(matchingRoutes[0]).toHaveProperty('id', '1');
expect(matchingRoutes).toMatchSnapshot();
});
});

@ -1,5 +1,4 @@
import { css } from '@emotion/css';
import { intersectionBy, isEqual } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useAsyncFn } from 'react-use';
@ -16,7 +15,11 @@ import { useGetContactPointsState } from './api/receiversApi';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable';
import { NotificationPoliciesFilter, findRoutesMatchingPredicate } from './components/notification-policies/Filters';
import {
NotificationPoliciesFilter,
findRoutesByMatchers,
findRoutesMatchingPredicate,
} from './components/notification-policies/Filters';
import {
useAddPolicyModal,
useAlertGroupsModal,
@ -30,7 +33,6 @@ import { updateAlertManagerConfigAction } from './state/actions';
import { FormAmRoute } from './types/amroutes';
import { useRouteGroupsMatcher } from './useRouteGroupsMatcher';
import { addUniqueIdentifierToRoute } from './utils/amroutes';
import { normalizeMatchers } from './utils/matchers';
import { computeInheritedTree } from './utils/notification-policies';
import { initialAsyncRequestState } from './utils/redux';
import { addRouteToParentRoute, mergePartialAmRouteWithRouteTree, omitRouteFromRouteTree } from './utils/routeTree';
@ -100,8 +102,14 @@ const AmRoutes = () => {
// these are computed from the contactPoint and labels matchers filter
const routesMatchingFilters = useMemo(() => {
if (!rootRoute) {
return [];
const emptyResult: RoutesMatchingFilters = {
filtersApplied: false,
matchedRoutesWithPath: new Map(),
};
return emptyResult;
}
return findRoutesMatchingFilters(rootRoute, { contactPointFilter, labelMatchersFilter });
}, [contactPointFilter, labelMatchersFilter, rootRoute]);
@ -231,6 +239,7 @@ const AmRoutes = () => {
receivers={receivers}
onChangeMatchers={setLabelMatchersFilter}
onChangeReceiver={setContactPointFilter}
matchingCount={routesMatchingFilters.matchedRoutesWithPath.size}
/>
)}
{rootRoute && (
@ -274,41 +283,85 @@ type RouteFilters = {
labelMatchersFilter?: ObjectMatcher[];
};
export const findRoutesMatchingFilters = (rootRoute: RouteWithID, filters: RouteFilters): RouteWithID[] => {
type FilterResult = Map<RouteWithID, RouteWithID[]>;
export interface RoutesMatchingFilters {
filtersApplied: boolean;
matchedRoutesWithPath: FilterResult;
}
export const findRoutesMatchingFilters = (rootRoute: RouteWithID, filters: RouteFilters): RoutesMatchingFilters => {
const { contactPointFilter, labelMatchersFilter = [] } = filters;
const hasFilter = contactPointFilter || labelMatchersFilter.length > 0;
const havebothFilters = Boolean(contactPointFilter) && labelMatchersFilter.length > 0;
// if filters are empty we short-circuit this function
if (!hasFilter) {
return [];
return { filtersApplied: false, matchedRoutesWithPath: new Map() };
}
// we'll collect all of the routes matching the filters
// we track an array of matching routes, each item in the array is for 1 type of filter
//
// [contactPointMatches, labelMatcherMatches] -> [[{ a: [], b: [] }], [{ a: [], c: [] }]]
// later we'll use intersection to find results in all sets of filter matchers
let matchedRoutes: RouteWithID[][] = [];
// compute fully inherited tree so all policies have their inherited receiver
const fullRoute = computeInheritedTree(rootRoute);
const routesMatchingContactPoint = contactPointFilter
// find all routes for our contact point filter
const matchingRoutesForContactPoint = contactPointFilter
? findRoutesMatchingPredicate(fullRoute, (route) => route.receiver === contactPointFilter)
: undefined;
: new Map();
const routesMatchingContactPoint = Array.from(matchingRoutesForContactPoint.keys());
if (routesMatchingContactPoint) {
matchedRoutes.push(routesMatchingContactPoint);
}
const routesMatchingLabelMatchers = labelMatchersFilter.length
? findRoutesMatchingPredicate(fullRoute, (route) => {
const routeMatchers = normalizeMatchers(route);
return labelMatchersFilter.every((filter) => routeMatchers.some((matcher) => isEqual(filter, matcher)));
})
: undefined;
// find all routes matching our label matchers
const matchingRoutesForLabelMatchers = labelMatchersFilter.length
? findRoutesMatchingPredicate(fullRoute, (route) => findRoutesByMatchers(route, labelMatchersFilter))
: new Map();
if (routesMatchingLabelMatchers) {
matchedRoutes.push(routesMatchingLabelMatchers);
const routesMatchingLabelFilters = Array.from(matchingRoutesForLabelMatchers.keys());
if (matchingRoutesForLabelMatchers.size > 0) {
matchedRoutes.push(routesMatchingLabelFilters);
}
return intersectionBy(...matchedRoutes, 'id');
// 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
const routesForAllFilterResults = havebothFilters
? findMapIntersection(matchingRoutesForLabelMatchers, matchingRoutesForContactPoint)
: new Map([...matchingRoutesForLabelMatchers, ...matchingRoutesForContactPoint]);
return {
filtersApplied: true,
matchedRoutesWithPath: routesForAllFilterResults,
};
};
// this function takes multiple maps and creates a new map with routes that exist in all maps
//
// map 1: { a: [], b: [] }
// map 2: { a: [], c: [] }
// return: { a: [] }
function findMapIntersection(...matchingRoutes: FilterResult[]): FilterResult {
const result = new Map<RouteWithID, RouteWithID[]>();
// Iterate through the keys of the first map'
for (const key of matchingRoutes[0].keys()) {
// Check if the key exists in all other maps
if (matchingRoutes.every((map) => map.has(key))) {
// If yes, add the key to the result map
// @ts-ignore
result.set(key, matchingRoutes[0].get(key));
}
}
return result;
}
const getStyles = (theme: GrafanaTheme2) => ({
tabContent: css`
margin-top: ${theme.spacing(2)};

@ -0,0 +1,281 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`findRoutesMatchingFilters should not match non-existing 1`] = `
{
"filtersApplied": true,
"matchedRoutesWithPath": Map {},
}
`;
exports[`findRoutesMatchingFilters should work with all filters 1`] = `
{
"filtersApplied": true,
"matchedRoutesWithPath": Map {
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
} => [
{
"id": "0",
"receiver": "default-receiver",
"routes": [
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
],
},
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
],
},
}
`;
exports[`findRoutesMatchingFilters should work with non-intersecting filters 1`] = `
{
"filtersApplied": true,
"matchedRoutesWithPath": Map {},
}
`;
exports[`findRoutesMatchingFilters should work with only contact point and inheritance 1`] = `
{
"filtersApplied": true,
"matchedRoutesWithPath": Map {
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
} => [
{
"id": "0",
"receiver": "default-receiver",
"routes": [
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
],
},
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
],
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
} => [
{
"id": "0",
"receiver": "default-receiver",
"routes": [
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
],
},
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
}
`;
exports[`findRoutesMatchingFilters should work with only label matchers 1`] = `
{
"filtersApplied": true,
"matchedRoutesWithPath": Map {
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
} => [
{
"id": "0",
"receiver": "default-receiver",
"routes": [
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
],
},
{
"id": "1",
"matchers": [
"hello=world",
"foo!=bar",
],
"receiver": "simple-receiver",
"routes": [
{
"id": "2",
"matchers": [
"bar=baz",
],
"receiver": "simple-receiver",
"routes": undefined,
},
],
},
],
},
}
`;

@ -1,24 +1,27 @@
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import { debounce, isEqual } from 'lodash';
import React, { useCallback, useEffect, useRef } from 'react';
import { SelectableValue } from '@grafana/data';
import { Button, Field, Icon, Input, Label as LabelElement, Select, Tooltip, useStyles2, Stack } from '@grafana/ui';
import { Button, Field, Icon, Input, Label, Select, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
import { ObjectMatcher, Receiver, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
import { matcherToObjectMatcher, parseMatchers } from '../../utils/alertmanager';
import { normalizeMatchers } from '../../utils/matchers';
interface NotificationPoliciesFilterProps {
receivers: Receiver[];
onChangeMatchers: (labels: ObjectMatcher[]) => void;
onChangeReceiver: (receiver: string | undefined) => void;
matchingCount: number;
}
const NotificationPoliciesFilter = ({
receivers,
onChangeReceiver,
onChangeMatchers,
matchingCount,
}: NotificationPoliciesFilterProps) => {
const [searchParams, setSearchParams] = useURLSearchParams();
const searchInputRef = useRef<HTMLInputElement | null>(null);
@ -50,11 +53,11 @@ const NotificationPoliciesFilter = ({
const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
return (
<Stack direction="row" alignItems="flex-start" gap={0.5}>
<Stack direction="row" alignItems="flex-end" gap={1}>
<Field
className={styles.noBottom}
label={
<LabelElement>
<Label>
<Stack gap={0.5}>
<span>Search by matchers</span>
<Tooltip
@ -68,7 +71,7 @@ const NotificationPoliciesFilter = ({
<Icon name="info-circle" size="sm" />
</Tooltip>
</Stack>
</LabelElement>
</Label>
}
invalid={inputInvalid}
error={inputInvalid ? 'Query must use valid matcher syntax' : null}
@ -99,9 +102,16 @@ const NotificationPoliciesFilter = ({
/>
</Field>
{hasFilters && (
<Button variant="secondary" icon="times" onClick={clearFilters} style={{ marginTop: 19 }}>
Clear filters
</Button>
<Stack alignItems="center">
<Button variant="secondary" icon="times" onClick={clearFilters}>
Clear filters
</Button>
<Text variant="bodySmall" color="secondary">
{matchingCount === 0 && 'No policies matching filters.'}
{matchingCount === 1 && `${matchingCount} policy matches the filters.`}
{matchingCount > 1 && `${matchingCount} policies match the filters.`}
</Text>
</Stack>
)}
</Stack>
);
@ -112,19 +122,46 @@ const NotificationPoliciesFilter = ({
*/
type FilterPredicate = (route: RouteWithID) => boolean;
export function findRoutesMatchingPredicate(routeTree: RouteWithID, predicateFn: FilterPredicate): RouteWithID[] {
const matches: RouteWithID[] = [];
/**
* Find routes int the tree that match the given predicate function
* @param routeTree the route tree to search
* @param predicateFn the predicate function to match routes
* @returns
* - matches: list of routes that match the predicate
* - matchingRouteIdsWithPath: map with routeids that are part of the path of a matching route
* key is the route id, value is an array of route ids that are part of its path
*/
export function findRoutesMatchingPredicate(
routeTree: RouteWithID,
predicateFn: FilterPredicate
): Map<RouteWithID, RouteWithID[]> {
// map with routids that are part of the path of a matching route
// key is the route id, value is an array of route ids that are part of the path
const matchingRouteIdsWithPath = new Map<RouteWithID, RouteWithID[]>();
function findMatch(route: RouteWithID, path: RouteWithID[]) {
const newPath = [...path, route];
function findMatch(route: RouteWithID) {
if (predicateFn(route)) {
matches.push(route);
// if the route matches the predicate, we need to add the path to the map of matching routes
const previousPath = matchingRouteIdsWithPath.get(route) ?? [];
// add the current route id to the map with its path
matchingRouteIdsWithPath.set(route, [...previousPath, ...newPath]);
}
route.routes?.forEach(findMatch);
// if the route has subroutes, call findMatch recursively
route.routes?.forEach((route) => findMatch(route, newPath));
}
findMatch(routeTree);
return matches;
findMatch(routeTree, []);
return matchingRouteIdsWithPath;
}
export function findRoutesByMatchers(route: RouteWithID, labelMatchersFilter: ObjectMatcher[]): boolean {
const routeMatchers = normalizeMatchers(route);
return labelMatchersFilter.every((filter) => routeMatchers.some((matcher) => isEqual(filter, matcher)));
}
const toOption = (receiver: Receiver) => ({
@ -138,9 +175,9 @@ const getNotificationPoliciesFilters = (searchParams: URLSearchParams) => ({
});
const getStyles = () => ({
noBottom: css`
margin-bottom: 0;
`,
noBottom: css({
marginBottom: 0,
}),
});
export { NotificationPoliciesFilter };

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { defaults, groupBy, isArray, sumBy, uniqueId, upperFirst } from 'lodash';
import pluralize from 'pluralize';
import React, { FC, Fragment, ReactNode } from 'react';
import React, { FC, Fragment, ReactNode, useState } from 'react';
import { Link } from 'react-router-dom';
import { useToggle } from 'react-use';
@ -30,6 +30,7 @@ import {
} from 'app/plugins/datasource/alertmanager/types';
import { ReceiversState } from 'app/types';
import { RoutesMatchingFilters } from '../../NotificationPolicies';
import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { INTEGRATION_ICONS } from '../../types/contact-points';
import { getAmMatcherFormatter } from '../../utils/alertmanager';
@ -55,9 +56,12 @@ interface PolicyComponentProps {
readOnly?: boolean;
provisioned?: boolean;
inheritedProperties?: Partial<InheritableProperties>;
routesMatchingFilters?: RouteWithID[];
routesMatchingFilters?: RoutesMatchingFilters;
matchingInstancesPreview?: { groupsMap?: Map<string, AlertmanagerGroup[]>; enabled: boolean };
matchingInstancesPreview?: {
groupsMap?: Map<string, AlertmanagerGroup[]>;
enabled: boolean;
};
routeTree: RouteWithID;
currentRoute: RouteWithID;
@ -84,7 +88,10 @@ const Policy = (props: PolicyComponentProps) => {
currentRoute,
routeTree,
inheritedProperties,
routesMatchingFilters = [],
routesMatchingFilters = {
filtersApplied: false,
matchedRoutesWithPath: new Map<RouteWithID, RouteWithID[]>(),
},
matchingInstancesPreview = { enabled: false },
onEditPolicy,
onAddPolicy,
@ -102,7 +109,16 @@ const Policy = (props: PolicyComponentProps) => {
const matchers = normalizeMatchers(currentRoute);
const hasMatchers = Boolean(matchers && matchers.length);
const hasFocus = routesMatchingFilters.some((route) => route.id === currentRoute.id);
const { filtersApplied, matchedRoutesWithPath } = routesMatchingFilters;
const matchedRoutes = Array.from(matchedRoutesWithPath.keys());
// check if this route matches the filters
const hasFocus = filtersApplied && matchedRoutes.some((route) => route.id === currentRoute.id);
// check if this route belongs to a path that matches the filters
const routesPath = Array.from(matchedRoutesWithPath.values()).flat();
const belongsToMatchPath = routesPath.some((route: RouteWithID) => route.id === currentRoute.id);
// gather errors here
const errors: ReactNode[] = [];
@ -115,9 +131,17 @@ const Policy = (props: PolicyComponentProps) => {
const actualContactPoint = contactPoint ?? inheritedProperties?.receiver ?? '';
const contactPointErrors = contactPointsState ? getContactPointErrors(actualContactPoint, contactPointsState) : [];
const childPolicies = currentRoute.routes ?? [];
const allChildPolicies = currentRoute.routes ?? [];
// filter chld 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 [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
const matchingAlertGroups = matchingInstancesPreview?.groupsMap?.get(currentRoute.id);
// sum all alert instances for all groups we're handling
const numberOfAlertInstances = matchingAlertGroups
? sumBy(matchingAlertGroups, (group) => group.alerts.length)
@ -127,6 +151,7 @@ const Policy = (props: PolicyComponentProps) => {
const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility(
AlertmanagerAction.ViewAutogeneratedPolicyTree
);
// collapsible policies variables
const isThisPolicyCollapsible = useShouldPolicyBeCollapsible(currentRoute);
const [isBranchOpen, toggleBranchOpen] = useToggle(false);
@ -145,6 +170,10 @@ const Policy = (props: PolicyComponentProps) => {
errors.push(error);
});
const POLICIES_PER_PAGE = 20;
const [visibleChildPolicies, setVisibleChildPolicies] = useState(POLICIES_PER_PAGE);
const isAutogeneratedPolicyRoot = isAutoGeneratedRootAndSimplifiedEnabled(currentRoute);
// build the menu actions for our policy
@ -162,12 +191,26 @@ const Policy = (props: PolicyComponentProps) => {
// policies then we should not show it. Same if the user is not supported to see autogenerated policies.
const hideCurrentPolicy =
isAutoGenerated && (!isAllowedToSeeAutogeneratedChunk || !isSupportedToSeeAutogeneratedChunk);
const hideCurrentPolicyForFilters = filtersApplied && !belongsToMatchPath;
if (hideCurrentPolicy) {
if (hideCurrentPolicy || hideCurrentPolicyForFilters) {
return null;
}
const isImmutablePolicy = isDefaultPolicy || isAutogeneratedPolicyRoot;
// TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated
const childPoliciesBelongingToMatchPath = childPolicies.filter((child) =>
routesPath.some((route: RouteWithID) => route.id === child.id)
);
// child policies to render are the ones that belong to the path that matches the filters
const childPoliciesToRender = filtersApplied ? childPoliciesBelongingToMatchPath : childPolicies;
const pageOfChildren = childPoliciesToRender.slice(0, visibleChildPolicies);
const moreCount = childPoliciesToRender.length - pageOfChildren.length;
const showMore = moreCount > 0;
return (
<>
<Stack direction="column" gap={1.5}>
@ -261,7 +304,7 @@ const Policy = (props: PolicyComponentProps) => {
<div className={styles.childPolicies}>
{renderChildPolicies && (
<>
{childPolicies.map((child) => {
{pageOfChildren.map((child) => {
const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);
// This child is autogenerated if it's the autogenerated root or if it's a child of an autogenerated policy.
const isThisChildAutoGenerated = isAutoGeneratedRootAndSimplifiedEnabled(child) || isAutoGenerated;
@ -291,6 +334,17 @@ const Policy = (props: PolicyComponentProps) => {
/>
);
})}
{showMore && (
<Button
size="sm"
icon="angle-down"
variant="secondary"
className={styles.moreButtons}
onClick={() => setVisibleChildPolicies(visibleChildPolicies + POLICIES_PER_PAGE)}
>
{moreCount} additional {pluralize('policy', moreCount)}
</Button>
)}
</>
)}
</div>
@ -308,12 +362,14 @@ function useShouldPolicyBeCollapsible(route: RouteWithID): boolean {
const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility(
AlertmanagerAction.ViewAutogeneratedPolicyTree
);
return (
const isAutoGeneratedRoot =
childrenCount > 0 &&
isSupportedToSeeAutogeneratedChunk &&
isAllowedToSeeAutogeneratedChunk &&
isAutoGeneratedRootAndSimplifiedEnabled(route)
);
isAutoGeneratedRootAndSimplifiedEnabled(route);
// let's add here more conditions for policies that should be collapsible
return isAutoGeneratedRoot;
}
interface MetadataRowProps {
@ -836,7 +892,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: theme.spacing(1.5),
}),
metadataRow: css({
background: theme.colors.background.secondary,
borderBottomLeftRadius: theme.shape.borderRadius(2),
borderBottomRightRadius: theme.shape.borderRadius(2),
}),
@ -847,7 +902,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
background: theme.colors.background.secondary,
borderRadius: theme.shape.radius.default,
border: `solid 1px ${theme.colors.border.weak}`,
...(hasFocus && { borderColor: theme.colors.primary.border }),
...(hasFocus && {
borderColor: theme.colors.primary.border,
background: theme.colors.primary.transparent,
}),
}),
metadata: css({
color: theme.colors.text.secondary,
@ -873,6 +931,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
borderRadius: theme.shape.radius.default,
padding: 0,
}),
moreButtons: css({
marginTop: theme.spacing(0.5),
marginBottom: theme.spacing(1.5),
}),
});
export { Policy };

Loading…
Cancel
Save