From 765b995b1bcafe8b94d0bbe53737ea6c751d63c9 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 22 Jun 2022 11:33:39 +0200 Subject: [PATCH] Alerting: Alert rules pagination (#50612) --- .../src/components/Pagination/Pagination.tsx | 6 +- .../features/alerting/unified/RuleViewer.tsx | 3 +- .../unified/components/DynamicTable.tsx | 130 ++++++++++++------ .../rules/AlertInstanceStateFilter.tsx | 59 ++++++-- .../components/rules/AlertInstancesTable.tsx | 33 +---- .../unified/components/rules/CloudRules.tsx | 43 ++++-- .../unified/components/rules/GrafanaRules.tsx | 43 ++++-- .../unified/components/rules/RuleDetails.tsx | 7 +- .../RuleDetailsMatchingInstances.test.tsx | 14 +- .../rules/RuleDetailsMatchingInstances.tsx | 65 ++++++++- .../unified/components/rules/RulesTable.tsx | 14 +- .../rules/useCombinedGroupNamespace.tsx | 16 +++ .../alerting/unified/hooks/usePagination.ts | 20 ++- .../alerting/unified/styles/pagination.ts | 12 ++ .../alerting/unified/utils/datasource.ts | 4 + .../panel/alertlist/AlertInstances.tsx | 9 +- public/app/types/unified-alerting.ts | 4 + 17 files changed, 344 insertions(+), 138 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/useCombinedGroupNamespace.tsx create mode 100644 public/app/features/alerting/unified/styles/pagination.ts diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.tsx index df605c60156..a15cd40b81c 100644 --- a/packages/grafana-ui/src/components/Pagination/Pagination.tsx +++ b/packages/grafana-ui/src/components/Pagination/Pagination.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import React, { useMemo } from 'react'; import { useStyles2 } from '../../themes'; @@ -16,6 +16,7 @@ export interface Props { hideWhenSinglePage?: boolean; /** Small version only shows the current page and the navigation buttons. */ showSmallVersion?: boolean; + className?: string; } export const Pagination: React.FC = ({ @@ -24,6 +25,7 @@ export const Pagination: React.FC = ({ onNavigate, hideWhenSinglePage, showSmallVersion, + className, }) => { const styles = useStyles2(getStyles); const pageLengthToCondense = showSmallVersion ? 1 : 8; @@ -96,7 +98,7 @@ export const Pagination: React.FC = ({ } return ( -
+
- +
{!isFederatedRule && data && Object.keys(data).length > 0 && ( diff --git a/public/app/features/alerting/unified/components/DynamicTable.tsx b/public/app/features/alerting/unified/components/DynamicTable.tsx index f34bc989e87..a627f8eb1c8 100644 --- a/public/app/features/alerting/unified/components/DynamicTable.tsx +++ b/public/app/features/alerting/unified/components/DynamicTable.tsx @@ -2,7 +2,14 @@ import { css, cx } from '@emotion/css'; import React, { ReactNode, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { IconButton, useStyles2 } from '@grafana/ui'; +import { IconButton, Pagination, useStyles2 } from '@grafana/ui'; + +import { usePagination } from '../hooks/usePagination'; +import { getPaginationStyles } from '../styles/pagination'; + +interface DynamicTablePagination { + itemsPerPage: number; +} export interface DynamicTableColumnProps { id: string | number; @@ -23,6 +30,8 @@ export interface DynamicTableProps { items: Array>; isExpandable?: boolean; + pagination?: DynamicTablePagination; + paginationStyles?: string; // provide these to manually control expanded status onCollapse?: (item: DynamicTableItemProps) => void; @@ -41,6 +50,8 @@ export interface DynamicTableProps { index: number, items: Array> ) => ReactNode; + + footerRow?: JSX.Element; } export const DynamicTable = ({ @@ -52,12 +63,16 @@ export const DynamicTable = ({ isExpanded, renderExpandedContent, testIdGenerator, - + pagination, + paginationStyles, // render a cell BEFORE expand icon for header/ each row. // currently use by RuleList to render guidelines renderPrefixCell, renderPrefixHeader, + footerRow, }: DynamicTableProps) => { + const defaultPaginationStyles = useStyles2(getPaginationStyles); + if ((onCollapse || onExpand || isExpanded) && !(onCollapse && onExpand && isExpanded)) { throw new Error('either all of onCollapse, onExpand, isExpanded must be provided, or none'); } @@ -77,50 +92,70 @@ export const DynamicTable = ({ ); } }; - return ( -
-
- {renderPrefixHeader && renderPrefixHeader()} - {isExpandable &&
} - {cols.map((col) => ( -
- {col.label} -
- ))} -
- {items.map((item, index) => { - const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id); - return ( -
- {renderPrefixCell && renderPrefixCell(item, index, items)} - {isExpandable && ( -
- toggleExpanded(item)} - type="button" - /> -
- )} - {cols.map((col) => ( -
- {col.renderCell(item, index)} -
- ))} - {isItemExpanded && renderExpandedContent && ( -
- {renderExpandedContent(item, index, items)} -
- )} -
- ); - })} -
+ const itemsPerPage = pagination?.itemsPerPage ?? items.length; + const { page, numberOfPages, onPageChange, pageItems } = usePagination(items, 1, itemsPerPage); + + return ( + <> +
+
+ {renderPrefixHeader && renderPrefixHeader()} + {isExpandable &&
} + {cols.map((col) => ( +
+ {col.label} +
+ ))} +
+ + {pageItems.map((item, index) => { + const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id); + return ( +
+ {renderPrefixCell && renderPrefixCell(item, index, items)} + {isExpandable && ( +
+ toggleExpanded(item)} + type="button" + /> +
+ )} + {cols.map((col) => ( +
+ {col.renderCell(item, index)} +
+ ))} + {isItemExpanded && renderExpandedContent && ( +
+ {renderExpandedContent(item, index, items)} +
+ )} +
+ ); + })} + {footerRow &&
{footerRow}
} +
+ {pagination && ( + + )} + ); }; @@ -186,6 +221,10 @@ const getStyles = ( : ''} } `, + footerRow: css` + display: flex; + padding: ${theme.spacing(1)}; + `, cell: css` align-items: center; padding: ${theme.spacing(1)}; @@ -197,6 +236,7 @@ const getStyles = ( `, bodyCell: css` overflow: hidden; + ${theme.breakpoints.down('sm')} { grid-column-end: right; grid-column-start: right; diff --git a/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx index ada64a58b58..9ec35c9e6e9 100644 --- a/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx @@ -1,7 +1,9 @@ +import { css } from '@emotion/css'; import { capitalize } from 'lodash'; -import React, { useMemo } from 'react'; +import React from 'react'; -import { Label, RadioButtonGroup } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data/src'; +import { Label, RadioButtonGroup, Tag, useStyles2 } from '@grafana/ui'; import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; export type InstanceStateFilter = GrafanaAlertState | PromAlertingRuleState.Pending | PromAlertingRuleState.Firing; @@ -11,21 +13,40 @@ interface Props { filterType: 'grafana' | 'prometheus'; stateFilter?: InstanceStateFilter; onStateFilterChange: (value?: InstanceStateFilter) => void; + itemPerStateStats?: Record; } -const grafanaOptions = Object.values(GrafanaAlertState).map((value) => ({ - label: value, - value, -})); +export const AlertInstanceStateFilter = ({ + className, + onStateFilterChange, + stateFilter, + filterType, + itemPerStateStats, +}: Props) => { + const styles = useStyles2(getStyles); -const promOptionValues = [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending] as const; -const promOptions = promOptionValues.map((state) => ({ - label: capitalize(state), - value: state, -})); + const getOptionComponent = (state: InstanceStateFilter) => { + return function InstanceStateCounter() { + return itemPerStateStats && itemPerStateStats[state] ? ( + + ) : null; + }; + }; -export const AlertInstanceStateFilter = ({ className, onStateFilterChange, stateFilter, filterType }: Props) => { - const stateOptions = useMemo(() => (filterType === 'grafana' ? grafanaOptions : promOptions), [filterType]); + const grafanaOptions = Object.values(GrafanaAlertState).map((state) => ({ + label: state, + value: state, + component: getOptionComponent(state), + })); + + const promOptionValues = [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending] as const; + const promOptions = promOptionValues.map((state) => ({ + label: capitalize(state), + value: state, + component: getOptionComponent(state), + })); + + const stateOptions = filterType === 'grafana' ? grafanaOptions : promOptions; return (
@@ -43,3 +64,15 @@ export const AlertInstanceStateFilter = ({ className, onStateFilterChange, state
); }; + +function getStyles(theme: GrafanaTheme2) { + return { + tag: css` + font-size: 11px; + font-weight: normal; + padding: ${theme.spacing(0.25, 0.5)}; + vertical-align: middle; + margin-left: ${theme.spacing(0.5)}; + `, + }; +} diff --git a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx index d3350d95877..b6b97381b28 100644 --- a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx @@ -1,8 +1,6 @@ -import { css } from '@emotion/css'; import React, { FC, useMemo } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; -import { Alert } from 'app/types/unified-alerting'; +import { Alert, PaginationProps } from 'app/types/unified-alerting'; import { alertInstanceKey } from '../../utils/rules'; import { AlertLabels } from '../AlertLabels'; @@ -13,12 +11,14 @@ import { AlertStateTag } from './AlertStateTag'; interface Props { instances: Alert[]; + pagination?: PaginationProps; + footerRow?: JSX.Element; } type AlertTableColumnProps = DynamicTableColumnProps; type AlertTableItemProps = DynamicTableItemProps; -export const AlertInstancesTable: FC = ({ instances }) => { +export const AlertInstancesTable: FC = ({ instances, pagination, footerRow }) => { const items = useMemo( (): AlertTableItemProps[] => instances.map((instance) => ({ @@ -34,33 +34,12 @@ export const AlertInstancesTable: FC = ({ instances }) => { isExpandable={true} items={items} renderExpandedContent={({ data }) => } + pagination={pagination} + footerRow={footerRow} /> ); }; -export const getStyles = (theme: GrafanaTheme2) => ({ - colExpand: css` - width: 36px; - `, - colState: css` - width: 110px; - `, - labelsCell: css` - padding-top: ${theme.spacing(0.5)} !important; - padding-bottom: ${theme.spacing(0.5)} !important; - `, - createdCell: css` - white-space: nowrap; - `, - table: css` - td { - vertical-align: top; - padding-top: ${theme.spacing(1)}; - padding-bottom: ${theme.spacing(1)}; - } - `, -}); - const columns: AlertTableColumnProps[] = [ { id: 'state', diff --git a/public/app/features/alerting/unified/components/rules/CloudRules.tsx b/public/app/features/alerting/unified/components/rules/CloudRules.tsx index 1e797baac95..018bf91ea62 100644 --- a/public/app/features/alerting/unified/components/rules/CloudRules.tsx +++ b/public/app/features/alerting/unified/components/rules/CloudRules.tsx @@ -2,14 +2,18 @@ import { css } from '@emotion/css'; import pluralize from 'pluralize'; import React, { FC, useMemo } from 'react'; -import { GrafanaTheme } from '@grafana/data'; -import { LoadingPlaceholder, useStyles } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { LoadingPlaceholder, Pagination, useStyles2 } from '@grafana/ui'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; +import { usePagination } from '../../hooks/usePagination'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; -import { getRulesDataSources, getRulesSourceName } from '../../utils/datasource'; +import { getPaginationStyles } from '../../styles/pagination'; +import { getRulesDataSources, getRulesSourceUid } from '../../utils/datasource'; import { RulesGroup } from './RulesGroup'; +import { useCombinedGroupNamespace } from './useCombinedGroupNamespace'; interface Props { namespaces: CombinedRuleNamespace[]; @@ -17,15 +21,23 @@ interface Props { } export const CloudRules: FC = ({ namespaces, expandAll }) => { - const styles = useStyles(getStyles); + const styles = useStyles2(getStyles); + const rules = useUnifiedAlertingSelector((state) => state.promRules); const rulesDataSources = useMemo(getRulesDataSources, []); + const groupsWithNamespaces = useCombinedGroupNamespace(namespaces); const dataSourcesLoading = useMemo( () => rulesDataSources.filter((ds) => rules[ds.name]?.loading), [rules, rulesDataSources] ); + const { numberOfPages, onPageChange, page, pageItems } = usePagination( + groupsWithNamespaces, + 1, + DEFAULT_PER_PAGE_PAGINATION + ); + return (
@@ -40,24 +52,30 @@ export const CloudRules: FC = ({ namespaces, expandAll }) => { )}
- {namespaces.map((namespace) => { - const { groups, rulesSource } = namespace; - return groups.map((group) => ( + {pageItems.map(({ group, namespace }) => { + return ( - )); + ); })} {namespaces?.length === 0 && !!rulesDataSources.length &&

No rules found.

} - {!rulesDataSources.length &&

There are no Prometheus or Loki datas sources configured.

} + {!rulesDataSources.length &&

There are no Prometheus or Loki data sources configured.

} +
); }; -const getStyles = (theme: GrafanaTheme) => ({ +const getStyles = (theme: GrafanaTheme2) => ({ loader: css` margin-bottom: 0; `, @@ -66,6 +84,7 @@ const getStyles = (theme: GrafanaTheme) => ({ justify-content: space-between; `, wrapper: css` - margin-bottom: ${theme.spacing.xl}; + margin-bottom: ${theme.spacing(4)}; `, + pagination: getPaginationStyles(theme), }); diff --git a/public/app/features/alerting/unified/components/rules/GrafanaRules.tsx b/public/app/features/alerting/unified/components/rules/GrafanaRules.tsx index 99e26bf74ff..7008d4ea697 100644 --- a/public/app/features/alerting/unified/components/rules/GrafanaRules.tsx +++ b/public/app/features/alerting/unified/components/rules/GrafanaRules.tsx @@ -1,17 +1,21 @@ import { css } from '@emotion/css'; import React, { FC } from 'react'; -import { GrafanaTheme } from '@grafana/data'; -import { LoadingPlaceholder, useStyles } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { LoadingPlaceholder, Pagination, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; import { flattenGrafanaManagedRules } from '../../hooks/useCombinedRuleNamespaces'; +import { usePagination } from '../../hooks/usePagination'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; +import { getPaginationStyles } from '../../styles/pagination'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { initialAsyncRequestState } from '../../utils/redux'; import { RulesGroup } from './RulesGroup'; +import { useCombinedGroupNamespace } from './useCombinedGroupNamespace'; interface Props { namespaces: CombinedRuleNamespace[]; @@ -19,7 +23,7 @@ interface Props { } export const GrafanaRules: FC = ({ namespaces, expandAll }) => { - const styles = useStyles(getStyles); + const styles = useStyles2(getStyles); const [queryParams] = useQueryParams(); const { loading } = useUnifiedAlertingSelector( @@ -29,6 +33,14 @@ export const GrafanaRules: FC = ({ namespaces, expandAll }) => { const wantsGroupedView = queryParams['view'] === 'grouped'; const namespacesFormat = wantsGroupedView ? namespaces : flattenGrafanaManagedRules(namespaces); + const groupsWithNamespaces = useCombinedGroupNamespace(namespacesFormat); + + const { numberOfPages, onPageChange, page, pageItems } = usePagination( + groupsWithNamespaces, + 1, + DEFAULT_PER_PAGE_PAGINATION + ); + return (
@@ -36,22 +48,22 @@ export const GrafanaRules: FC = ({ namespaces, expandAll }) => { {loading ? :
}
- {namespacesFormat?.map((namespace) => - namespace.groups.map((group) => ( - - )) - )} + {pageItems.map(({ group, namespace }) => ( + + ))} {namespacesFormat?.length === 0 &&

No rules found.

} +
); }; -const getStyles = (theme: GrafanaTheme) => ({ +const getStyles = (theme: GrafanaTheme2) => ({ loader: css` margin-bottom: 0; `, @@ -60,6 +72,7 @@ const getStyles = (theme: GrafanaTheme) => ({ justify-content: space-between; `, wrapper: css` - margin-bottom: ${theme.spacing.xl}; + margin-bottom: ${theme.spacing(4)}; `, + pagination: getPaginationStyles(theme), }); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx index 3d1ebf2023d..f3b02d859ad 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx @@ -18,6 +18,11 @@ interface Props { rule: CombinedRule; } +// The limit is set to 15 in order to upkeep the good performance +// and to encourage users to go to the rule details page to see the rest of the instances +// We don't want to paginate the instances list on the alert list page +const INSTANCES_DISPLAY_LIMIT = 15; + export const RuleDetails: FC = ({ rule }) => { const styles = useStyles2(getStyles); const { @@ -43,7 +48,7 @@ export const RuleDetails: FC = ({ rule }) => {
- +
); }; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx index 5d608dd51dd..4c7cfd599b7 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx @@ -14,15 +14,15 @@ const ui = { stateFilter: byTestId('alert-instance-state-filter'), stateButton: byRole('radio'), grafanaStateButton: { - normal: byLabelText('Normal'), - alerting: byLabelText('Alerting'), - pending: byLabelText('Pending'), - noData: byLabelText('NoData'), - error: byLabelText('Error'), + normal: byLabelText(/^Normal/), + alerting: byLabelText(/^Alerting/), + pending: byLabelText(/^Pending/), + noData: byLabelText(/^NoData/), + error: byLabelText(/^Error/), }, cloudStateButton: { - firing: byLabelText('Firing'), - pending: byLabelText('Pending'), + firing: byLabelText(/^Firing/), + pending: byLabelText(/^Pending/), }, instanceRow: byTestId('row'), }; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx index 7cae0e0b74b..3c5f205cdb7 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx @@ -1,17 +1,18 @@ import { css, cx } from '@emotion/css'; +import { countBy } from 'lodash'; import React, { useMemo, useState } from 'react'; import { GrafanaTheme } from '@grafana/data'; -import { useStyles } from '@grafana/ui'; +import { LinkButton, useStyles } from '@grafana/ui'; import { MatcherFilter } from 'app/features/alerting/unified/components/alert-groups/MatcherFilter'; import { AlertInstanceStateFilter, InstanceStateFilter, } from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter'; import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; -import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; +import { createViewLink, sortAlerts } from 'app/features/alerting/unified/utils/misc'; import { SortOrder } from 'app/plugins/panel/alertlist/types'; -import { Alert, CombinedRule } from 'app/types/unified-alerting'; +import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting'; import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto'; import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource'; @@ -20,13 +21,40 @@ import { DetailsField } from '../DetailsField'; import { AlertInstancesTable } from './AlertInstancesTable'; -type Props = { +interface Props { rule: CombinedRule; -}; + pagination?: PaginationProps; + itemsDisplayLimit?: number; +} + +interface ShowMoreStats { + totalItemsCount: number; + visibleItemsCount: number; +} + +function ShowMoreInstances(props: { ruleViewPageLink: string; stats: ShowMoreStats }) { + const styles = useStyles(getStyles); + const { ruleViewPageLink, stats } = props; + + return ( +
+
+ Showing {stats.visibleItemsCount} out of {stats.totalItemsCount} instances +
+ {ruleViewPageLink && ( + + Show all {stats.totalItemsCount} alert instances + + )} +
+ ); +} export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null { const { rule: { promRule, namespace }, + itemsDisplayLimit = Number.POSITIVE_INFINITY, + pagination, } = props; const [queryString, setQueryString] = useState(); @@ -52,6 +80,22 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null { return null; } + const visibleInstances = alerts.slice(0, itemsDisplayLimit); + + const countAllByState = countBy(promRule.alerts, (alert) => mapStateWithReasonToBaseState(alert.state)); + const hiddenItemsCount = alerts.length - visibleInstances.length; + + const stats: ShowMoreStats = { + totalItemsCount: alerts.length, + visibleItemsCount: visibleInstances.length, + }; + + const ruleViewPageLink = createViewLink(namespace.rulesSource, props.rule, location.pathname + location.search); + + const footerRow = hiddenItemsCount ? ( + + ) : undefined; + return (
@@ -67,11 +111,12 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null { filterType={stateFilterType} stateFilter={alertState} onStateFilterChange={setAlertState} + itemPerStateStats={countAllByState} />
- +
); } @@ -111,5 +156,13 @@ const getStyles = (theme: GrafanaTheme) => { rowChild: css` margin-right: ${theme.spacing.sm}; `, + footerRow: css` + display: flex; + flex-direction: column; + gap: ${theme.spacing.sm}; + justify-content: space-between; + align-items: center; + width: 100%; + `, }; }; diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index 5e2ca58c4fc..a6676beccb8 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { CombinedRule } from 'app/types/unified-alerting'; +import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; import { useHasRuler } from '../../hooks/useHasRuler'; import { Annotation } from '../../utils/constants'; import { isGrafanaRulerRule } from '../../utils/rules'; @@ -71,6 +72,8 @@ export const RulesTable: FC = ({ isExpandable={true} items={items} renderExpandedContent={({ data: rule }) => } + pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }} + paginationStyles={styles.pagination} /> ); @@ -87,9 +90,18 @@ export const getStyles = (theme: GrafanaTheme2) => ({ `, wrapper: css` width: auto; - background-color: ${theme.colors.background.secondary}; border-radius: ${theme.shape.borderRadius()}; `, + pagination: css` + display: flex; + margin: 0; + padding-top: ${theme.spacing(1)}; + padding-bottom: ${theme.spacing(0.25)}; + justify-content: center; + border-left: 1px solid ${theme.colors.border.strong}; + border-right: 1px solid ${theme.colors.border.strong}; + border-bottom: 1px solid ${theme.colors.border.strong}; + `, }); function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) { diff --git a/public/app/features/alerting/unified/components/rules/useCombinedGroupNamespace.tsx b/public/app/features/alerting/unified/components/rules/useCombinedGroupNamespace.tsx new file mode 100644 index 00000000000..febc2fe2ea8 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/useCombinedGroupNamespace.tsx @@ -0,0 +1,16 @@ +import { useMemo } from 'react'; + +import { CombinedRuleNamespace } from '../../../../../types/unified-alerting'; + +export function useCombinedGroupNamespace(namespaces: CombinedRuleNamespace[]) { + return useMemo( + () => + namespaces.flatMap((ns) => + ns.groups.map((g) => ({ + namespace: ns, + group: g, + })) + ), + [namespaces] + ); +} diff --git a/public/app/features/alerting/unified/hooks/usePagination.ts b/public/app/features/alerting/unified/hooks/usePagination.ts index f5fc76fe15e..1c7b1bfae6b 100644 --- a/public/app/features/alerting/unified/hooks/usePagination.ts +++ b/public/app/features/alerting/unified/hooks/usePagination.ts @@ -1,18 +1,24 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; export function usePagination(items: T[], initialPage: number, itemsPerPage: number) { const [page, setPage] = useState(initialPage); const numberOfPages = Math.ceil(items.length / itemsPerPage); - const firstItemOnPageIndex = itemsPerPage * (page - 1); - const pageItems = items.slice(firstItemOnPageIndex, firstItemOnPageIndex + itemsPerPage); - const onPageChange = (newPage: number) => { - setPage(newPage); - }; + const pageItems = useMemo( + () => items.slice(firstItemOnPageIndex, firstItemOnPageIndex + itemsPerPage), + [items, firstItemOnPageIndex, itemsPerPage] + ); - // Reset the current page when number of changes has been changed + const onPageChange = useCallback( + (newPage: number) => { + setPage(newPage); + }, + [setPage] + ); + + // Reset the current page when number of pages has been changed useEffect(() => setPage(1), [numberOfPages]); return { page, onPageChange, numberOfPages, pageItems }; diff --git a/public/app/features/alerting/unified/styles/pagination.ts b/public/app/features/alerting/unified/styles/pagination.ts new file mode 100644 index 00000000000..17d7e932347 --- /dev/null +++ b/public/app/features/alerting/unified/styles/pagination.ts @@ -0,0 +1,12 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data/src'; + +export const getPaginationStyles = (theme: GrafanaTheme2) => { + return css` + float: none; + display: flex; + justify-content: flex-start; + margin: ${theme.spacing(2, 0)}; + `; +}; diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index 216105023c5..674c59aee0f 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -123,6 +123,10 @@ export function getRulesSourceName(rulesSource: RulesSource): string { return isCloudRulesSource(rulesSource) ? rulesSource.name : rulesSource; } +export function getRulesSourceUid(rulesSource: RulesSource): string { + return isCloudRulesSource(rulesSource) ? rulesSource.uid : GRAFANA_RULES_SOURCE_NAME; +} + export function isCloudRulesSource(rulesSource: RulesSource | string): rulesSource is DataSourceInstanceSettings { return rulesSource !== GRAFANA_RULES_SOURCE_NAME; } diff --git a/public/app/plugins/panel/alertlist/AlertInstances.tsx b/public/app/plugins/panel/alertlist/AlertInstances.tsx index 7a4e7abbbbb..b3638af561a 100644 --- a/public/app/plugins/panel/alertlist/AlertInstances.tsx +++ b/public/app/plugins/panel/alertlist/AlertInstances.tsx @@ -9,6 +9,8 @@ import { AlertInstancesTable } from 'app/features/alerting/unified/components/ru import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; import { Alert } from 'app/types/unified-alerting'; +import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants'; + import { GroupMode, UnifiedAlertListOptions } from './types'; import { filterAlerts } from './util'; @@ -52,7 +54,12 @@ export const AlertInstances: FC = ({ alerts, options }) => { {hiddenInstances > 0 && , {`${hiddenInstances} hidden by filters`}} )} - {displayInstances && } + {displayInstances && ( + + )} ); }; diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts index 0be514df2bb..c12cc41de7a 100644 --- a/public/app/types/unified-alerting.ts +++ b/public/app/types/unified-alerting.ts @@ -193,3 +193,7 @@ export interface PromBasedDataSource { id: string | number; rulerConfig?: RulerDataSourceConfig; } + +export interface PaginationProps { + itemsPerPage: number; +}