From 5ef320193958bc2520c8f1e4561303a957bb29ea Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 30 Jul 2025 11:44:03 +0200 Subject: [PATCH] Alerting: Paginate data source managed rules within groups (#108803) --- .../alerting/unified/hooks/usePagination.ts | 18 ++++++++ .../rule-list/DataSourceGroupLoader.tsx | 28 ++++++++++-- .../unified/rule-list/GrafanaGroupLoader.tsx | 45 +++++-------------- .../rule-list/components/ListSection.tsx | 2 + .../alerting/unified/utils/constants.ts | 2 + 5 files changed, 59 insertions(+), 36 deletions(-) diff --git a/public/app/features/alerting/unified/hooks/usePagination.ts b/public/app/features/alerting/unified/hooks/usePagination.ts index a57955fa5bb..d1d685c2df7 100644 --- a/public/app/features/alerting/unified/hooks/usePagination.ts +++ b/public/app/features/alerting/unified/hooks/usePagination.ts @@ -27,3 +27,21 @@ export function usePagination(items: T[], initialPage = 1, itemsPerPage: numb return { page, onPageChange, numberOfPages, pageItems, pageStart, pageEnd, nextPage, previousPage }; } + +export function useContinuousPagination(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, + }; +} diff --git a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx index a9430c488e1..9842f651ec3 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx @@ -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 && ( +
  • + +
  • + )} ); } + +const getStyles = (theme: GrafanaTheme2) => ({ + loadMoreWrapper: css({ + listStyle: 'none', + paddingTop: theme.spacing(1), + }), +}); diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx index 7d9a8d184de..a92b67822d2 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx @@ -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({ /> ); })} -
    - {numberOfPages > 1 && ( - - )} -
    + {hasMore && ( +
  • + +
  • + )} ); } 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), }), }); diff --git a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx index 4feb8535036..8157525e1c6 100644 --- a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx +++ b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx @@ -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': { diff --git a/public/app/features/alerting/unified/utils/constants.ts b/public/app/features/alerting/unified/utils/constants.ts index eaeeb23c590..1c901e56800 100644 --- a/public/app/features/alerting/unified/utils/constants.ts +++ b/public/app/features/alerting/unified/utils/constants.ts @@ -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';