Alerting: Paginate data source managed rules within groups (#108803)

This commit is contained in:
Gilles De Mey
2025-07-30 11:44:03 +02:00
committed by GitHub
parent d5bcc606b0
commit 5ef3201939
5 changed files with 59 additions and 36 deletions
@@ -27,3 +27,21 @@ export function usePagination<T>(items: T[], initialPage = 1, itemsPerPage: numb
return { page, onPageChange, numberOfPages, pageItems, pageStart, pageEnd, nextPage, previousPage };
}
export function useContinuousPagination<T>(items: T[], itemsPerPage: number) {
const [pageIndex, setPageIndex] = useState(1);
const hasMore = items.length > itemsPerPage * pageIndex;
const pageItems = items.slice(0, itemsPerPage * pageIndex);
const loadMore = useCallback(() => {
setPageIndex((index) => index + 1);
}, []);
return {
pageItems,
pageIndex,
loadMore,
hasMore,
};
}
@@ -1,9 +1,11 @@
import { css } from '@emotion/css';
import { skipToken } from '@reduxjs/toolkit/query';
import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { isFetchError } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import { Alert, useStyles2 } from '@grafana/ui';
import { DataSourceRuleGroupIdentifier } from 'app/types/unified-alerting';
import {
PromRuleDTO,
@@ -16,13 +18,15 @@ import {
import { alertRuleApi } from '../api/alertRuleApi';
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
import { prometheusApi } from '../api/prometheusApi';
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
import { useContinuousPagination } from '../hooks/usePagination';
import { DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP, RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
import { hashRule } from '../utils/rule-id';
import { getRuleName, isCloudRulerGroup } from '../utils/rules';
import { DataSourceRuleListItem } from './DataSourceRuleListItem';
import { RuleOperationListItem } from './components/AlertRuleListItem';
import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader';
import { LoadMoreButton } from './components/LoadMoreButton';
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
import { RuleOperation } from './components/RuleListIcon';
import { matchRulesGroup } from './ruleMatching';
@@ -174,15 +178,21 @@ export function RulerBasedGroupRules({
promGroup,
rulerGroup,
}: RulerBasedGroupRulesProps) {
const styles = useStyles2(getStyles);
const { namespace, groupName } = groupIdentifier;
const { matches, promOnlyRules } = useMemo(() => {
return matchRulesGroup(rulerGroup, promGroup);
}, [promGroup, rulerGroup]);
const { pageItems, hasMore, loadMore } = useContinuousPagination(
rulerGroup.rules,
DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP
);
return (
<>
{rulerGroup.rules.map((rulerRule) => {
{pageItems.map((rulerRule) => {
const promRule = matches.get(rulerRule);
return promRule ? (
@@ -222,6 +232,18 @@ export function RulerBasedGroupRules({
showLocation={false}
/>
))}
{hasMore && (
<li aria-selected="false" role="treeitem" className={styles.loadMoreWrapper}>
<LoadMoreButton onClick={loadMore} />
</li>
)}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
loadMoreWrapper: css({
listStyle: 'none',
paddingTop: theme.spacing(1),
}),
});
@@ -3,17 +3,16 @@ import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { Alert, Pagination, Stack, useStyles2 } from '@grafana/ui';
import { Alert, Stack, useStyles2 } from '@grafana/ui';
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
import { prometheusApi } from '../api/prometheusApi';
import { usePagination } from '../hooks/usePagination';
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
import { useContinuousPagination } from '../hooks/usePagination';
import { DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP, RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
import { GrafanaRuleListItem } from './GrafanaRuleListItem';
import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader';
const DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP_LIST_VIEW_V2 = 100;
import { LoadMoreButton } from './components/LoadMoreButton';
const { useGetGrafanaGroupsQuery } = prometheusApi;
@@ -53,11 +52,7 @@ export function GrafanaGroupLoader({
return promResponse?.data.groups.at(0)?.rules ?? [];
}, [promResponse]);
const { pageItems, page, numberOfPages, onPageChange } = usePagination(
rules,
1,
DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP_LIST_VIEW_V2
);
const { pageItems, hasMore, loadMore } = useContinuousPagination(rules, DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP);
if (isPromResponseLoading) {
return (
@@ -101,34 +96,18 @@ export function GrafanaGroupLoader({
/>
);
})}
<div className={styles.paginationWrapper}>
{numberOfPages > 1 && (
<Pagination
currentPage={page}
numberOfPages={numberOfPages}
onNavigate={onPageChange}
hideWhenSinglePage
className={styles.pagination}
/>
)}
</div>
{hasMore && (
<li aria-selected="false" role="treeitem" className={styles.loadMoreWrapper}>
<LoadMoreButton onClick={loadMore} />
</li>
)}
</Stack>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
pagination: css({
display: 'flex',
margin: 0,
loadMoreWrapper: css({
listStyle: 'none',
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(0.25),
justifyContent: 'center',
float: 'none',
}),
paginationWrapper: css({
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
marginLeft: theme.spacing(2.5),
}),
});
@@ -65,6 +65,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
// unfortunately we have to resort to this since we can't overwrite the styles of the list items individually
// unless we clone the React Elements and modify className
'li[role=treeitem]': {
listStyle: 'none',
position: 'relative',
paddingLeft: theme.spacing(6.5),
'&:before': {
@@ -8,6 +8,8 @@ export const SILENCES_POLL_INTERVAL_MS = 20000;
export const NOTIFICATIONS_POLL_INTERVAL_MS = 20000;
export const CONTACT_POINTS_STATE_INTERVAL_MS = 20000;
export const DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP = 100;
export const TIMESERIES = 'timeseries';
export const TABLE = 'table';
export const STAT = 'stat';