Alerting: Alert rules pagination (#50612)

pull/51237/head
Konrad Lalik 3 years ago committed by GitHub
parent 1ca2e2b6c2
commit 765b995b1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      packages/grafana-ui/src/components/Pagination/Pagination.tsx
  2. 3
      public/app/features/alerting/unified/RuleViewer.tsx
  3. 128
      public/app/features/alerting/unified/components/DynamicTable.tsx
  4. 59
      public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx
  5. 33
      public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx
  6. 43
      public/app/features/alerting/unified/components/rules/CloudRules.tsx
  7. 43
      public/app/features/alerting/unified/components/rules/GrafanaRules.tsx
  8. 7
      public/app/features/alerting/unified/components/rules/RuleDetails.tsx
  9. 14
      public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx
  10. 65
      public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
  11. 14
      public/app/features/alerting/unified/components/rules/RulesTable.tsx
  12. 16
      public/app/features/alerting/unified/components/rules/useCombinedGroupNamespace.tsx
  13. 20
      public/app/features/alerting/unified/hooks/usePagination.ts
  14. 12
      public/app/features/alerting/unified/styles/pagination.ts
  15. 4
      public/app/features/alerting/unified/utils/datasource.ts
  16. 9
      public/app/plugins/panel/alertlist/AlertInstances.tsx
  17. 4
      public/app/types/unified-alerting.ts

@ -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<Props> = ({
@ -24,6 +25,7 @@ export const Pagination: React.FC<Props> = ({
onNavigate,
hideWhenSinglePage,
showSmallVersion,
className,
}) => {
const styles = useStyles2(getStyles);
const pageLengthToCondense = showSmallVersion ? 1 : 8;
@ -96,7 +98,7 @@ export const Pagination: React.FC<Props> = ({
}
return (
<div className={styles.container}>
<div className={cx(styles.container, className)}>
<ol>
<li className={styles.item}>
<Button

@ -15,6 +15,7 @@ import {
} from '@grafana/ui';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants';
import { AlertQuery } from '../../../types/unified-alerting-dto';
import { AlertLabels } from './components/AlertLabels';
@ -179,7 +180,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
</div>
</div>
<div>
<RuleDetailsMatchingInstances rule={rule} />
<RuleDetailsMatchingInstances rule={rule} pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }} />
</div>
</RuleViewerLayoutContent>
{!isFederatedRule && data && Object.keys(data).length > 0 && (

@ -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<T = unknown> {
id: string | number;
@ -23,6 +30,8 @@ export interface DynamicTableProps<T = unknown> {
items: Array<DynamicTableItemProps<T>>;
isExpandable?: boolean;
pagination?: DynamicTablePagination;
paginationStyles?: string;
// provide these to manually control expanded status
onCollapse?: (item: DynamicTableItemProps<T>) => void;
@ -41,6 +50,8 @@ export interface DynamicTableProps<T = unknown> {
index: number,
items: Array<DynamicTableItemProps<T>>
) => ReactNode;
footerRow?: JSX.Element;
}
export const DynamicTable = <T extends object>({
@ -52,12 +63,16 @@ export const DynamicTable = <T extends object>({
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<T>) => {
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 = <T extends object>({
);
}
};
const itemsPerPage = pagination?.itemsPerPage ?? items.length;
const { page, numberOfPages, onPageChange, pageItems } = usePagination(items, 1, itemsPerPage);
return (
<div className={styles.container} data-testid="dynamic-table">
<div className={styles.row} data-testid="header">
{renderPrefixHeader && renderPrefixHeader()}
{isExpandable && <div className={styles.cell} />}
{cols.map((col) => (
<div className={styles.cell} key={col.id}>
{col.label}
</div>
))}
</div>
<>
<div className={styles.container} data-testid="dynamic-table">
<div className={styles.row} data-testid="header">
{renderPrefixHeader && renderPrefixHeader()}
{isExpandable && <div className={styles.cell} />}
{cols.map((col) => (
<div className={styles.cell} key={col.id}>
{col.label}
</div>
))}
</div>
{items.map((item, index) => {
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
return (
<div className={styles.row} key={`${item.id}-${index}`} data-testid={testIdGenerator?.(item, index) ?? 'row'}>
{renderPrefixCell && renderPrefixCell(item, index, items)}
{isExpandable && (
<div className={cx(styles.cell, styles.expandCell)}>
<IconButton
aria-label={`${isItemExpanded ? 'Collapse' : 'Expand'} row`}
size="xl"
data-testid="collapse-toggle"
className={styles.expandButton}
name={isItemExpanded ? 'angle-down' : 'angle-right'}
onClick={() => toggleExpanded(item)}
type="button"
/>
</div>
)}
{cols.map((col) => (
<div className={cx(styles.cell, styles.bodyCell)} data-column={col.label} key={`${item.id}-${col.id}`}>
{col.renderCell(item, index)}
</div>
))}
{isItemExpanded && renderExpandedContent && (
<div className={styles.expandedContentRow} data-testid="expanded-content">
{renderExpandedContent(item, index, items)}
</div>
)}
</div>
);
})}
</div>
{pageItems.map((item, index) => {
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
return (
<div
className={styles.row}
key={`${item.id}-${index}`}
data-testid={testIdGenerator?.(item, index) ?? 'row'}
>
{renderPrefixCell && renderPrefixCell(item, index, items)}
{isExpandable && (
<div className={cx(styles.cell, styles.expandCell)}>
<IconButton
aria-label={`${isItemExpanded ? 'Collapse' : 'Expand'} row`}
size="xl"
data-testid="collapse-toggle"
className={styles.expandButton}
name={isItemExpanded ? 'angle-down' : 'angle-right'}
onClick={() => toggleExpanded(item)}
type="button"
/>
</div>
)}
{cols.map((col) => (
<div className={cx(styles.cell, styles.bodyCell)} data-column={col.label} key={`${item.id}-${col.id}`}>
{col.renderCell(item, index)}
</div>
))}
{isItemExpanded && renderExpandedContent && (
<div className={styles.expandedContentRow} data-testid="expanded-content">
{renderExpandedContent(item, index, items)}
</div>
)}
</div>
);
})}
{footerRow && <div className={cx(styles.row, styles.footerRow)}>{footerRow}</div>}
</div>
{pagination && (
<Pagination
className={cx(defaultPaginationStyles, paginationStyles)}
currentPage={page}
numberOfPages={numberOfPages}
onNavigate={onPageChange}
hideWhenSinglePage
/>
)}
</>
);
};
@ -186,6 +221,10 @@ const getStyles = <T extends unknown>(
: ''}
}
`,
footerRow: css`
display: flex;
padding: ${theme.spacing(1)};
`,
cell: css`
align-items: center;
padding: ${theme.spacing(1)};
@ -197,6 +236,7 @@ const getStyles = <T extends unknown>(
`,
bodyCell: css`
overflow: hidden;
${theme.breakpoints.down('sm')} {
grid-column-end: right;
grid-column-start: right;

@ -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<string, number>;
}
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] ? (
<Tag name={itemPerStateStats[state].toFixed(0)} colorIndex={9} className={styles.tag} />
) : 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 (
<div className={className} data-testid="alert-instance-state-filter">
@ -43,3 +64,15 @@ export const AlertInstanceStateFilter = ({ className, onStateFilterChange, state
</div>
);
};
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)};
`,
};
}

@ -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<Alert>;
type AlertTableItemProps = DynamicTableItemProps<Alert>;
export const AlertInstancesTable: FC<Props> = ({ instances }) => {
export const AlertInstancesTable: FC<Props> = ({ instances, pagination, footerRow }) => {
const items = useMemo(
(): AlertTableItemProps[] =>
instances.map((instance) => ({
@ -34,33 +34,12 @@ export const AlertInstancesTable: FC<Props> = ({ instances }) => {
isExpandable={true}
items={items}
renderExpandedContent={({ data }) => <AlertInstanceDetails instance={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',

@ -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<Props> = ({ 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 (
<section className={styles.wrapper}>
<div className={styles.sectionHeader}>
@ -40,24 +52,30 @@ export const CloudRules: FC<Props> = ({ namespaces, expandAll }) => {
)}
</div>
{namespaces.map((namespace) => {
const { groups, rulesSource } = namespace;
return groups.map((group) => (
{pageItems.map(({ group, namespace }) => {
return (
<RulesGroup
group={group}
key={`${getRulesSourceName(rulesSource)}-${name}-${group.name}`}
key={`${getRulesSourceUid(namespace.rulesSource)}-${namespace.name}-${group.name}`}
namespace={namespace}
expandAll={expandAll}
/>
));
);
})}
{namespaces?.length === 0 && !!rulesDataSources.length && <p>No rules found.</p>}
{!rulesDataSources.length && <p>There are no Prometheus or Loki datas sources configured.</p>}
{!rulesDataSources.length && <p>There are no Prometheus or Loki data sources configured.</p>}
<Pagination
className={styles.pagination}
currentPage={page}
numberOfPages={numberOfPages}
onNavigate={onPageChange}
hideWhenSinglePage
/>
</section>
);
};
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),
});

@ -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<Props> = ({ namespaces, expandAll }) => {
const styles = useStyles(getStyles);
const styles = useStyles2(getStyles);
const [queryParams] = useQueryParams();
const { loading } = useUnifiedAlertingSelector(
@ -29,6 +33,14 @@ export const GrafanaRules: FC<Props> = ({ 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 (
<section className={styles.wrapper}>
<div className={styles.sectionHeader}>
@ -36,22 +48,22 @@ export const GrafanaRules: FC<Props> = ({ namespaces, expandAll }) => {
{loading ? <LoadingPlaceholder className={styles.loader} text="Loading..." /> : <div />}
</div>
{namespacesFormat?.map((namespace) =>
namespace.groups.map((group) => (
<RulesGroup
group={group}
key={`${namespace.name}-${group.name}`}
namespace={namespace}
expandAll={expandAll}
/>
))
)}
{pageItems.map(({ group, namespace }) => (
<RulesGroup group={group} key={`${namespace.name}-${group.name}`} namespace={namespace} expandAll={expandAll} />
))}
{namespacesFormat?.length === 0 && <p>No rules found.</p>}
<Pagination
className={styles.pagination}
currentPage={page}
numberOfPages={numberOfPages}
onNavigate={onPageChange}
hideWhenSinglePage
/>
</section>
);
};
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),
});

@ -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<Props> = ({ rule }) => {
const styles = useStyles2(getStyles);
const {
@ -43,7 +48,7 @@ export const RuleDetails: FC<Props> = ({ rule }) => {
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
</div>
</div>
<RuleDetailsMatchingInstances rule={rule} />
<RuleDetailsMatchingInstances rule={rule} itemsDisplayLimit={INSTANCES_DISPLAY_LIMIT} />
</div>
);
};

@ -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'),
};

@ -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 (
<div className={styles.footerRow}>
<div>
Showing {stats.visibleItemsCount} out of {stats.totalItemsCount} instances
</div>
{ruleViewPageLink && (
<LinkButton href={ruleViewPageLink} size="sm" variant="secondary">
Show all {stats.totalItemsCount} alert instances
</LinkButton>
)}
</div>
);
}
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
const {
rule: { promRule, namespace },
itemsDisplayLimit = Number.POSITIVE_INFINITY,
pagination,
} = props;
const [queryString, setQueryString] = useState<string>();
@ -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 ? (
<ShowMoreInstances stats={stats} ruleViewPageLink={ruleViewPageLink} />
) : undefined;
return (
<DetailsField label="Matching instances" horizontal={true}>
<div className={cx(styles.flexRow, styles.spaceBetween)}>
@ -67,11 +111,12 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
filterType={stateFilterType}
stateFilter={alertState}
onStateFilterChange={setAlertState}
itemPerStateStats={countAllByState}
/>
</div>
</div>
<AlertInstancesTable instances={alerts} />
<AlertInstancesTable instances={visibleInstances} pagination={pagination} footerRow={footerRow} />
</DetailsField>
);
}
@ -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%;
`,
};
};

@ -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<Props> = ({
isExpandable={true}
items={items}
renderExpandedContent={({ data: rule }) => <RuleDetails rule={rule} />}
pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }}
paginationStyles={styles.pagination}
/>
</div>
);
@ -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) {

@ -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]
);
}

@ -1,18 +1,24 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
export function usePagination<T>(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]
);
const onPageChange = useCallback(
(newPage: number) => {
setPage(newPage);
},
[setPage]
);
// Reset the current page when number of changes has been changed
// Reset the current page when number of pages has been changed
useEffect(() => setPage(1), [numberOfPages]);
return { page, onPageChange, numberOfPages, pageItems };

@ -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)};
`;
};

@ -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;
}

@ -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<Props> = ({ alerts, options }) => {
{hiddenInstances > 0 && <span>, {`${hiddenInstances} hidden by filters`}</span>}
</div>
)}
{displayInstances && <AlertInstancesTable instances={filteredAlerts} />}
{displayInstances && (
<AlertInstancesTable
instances={filteredAlerts}
pagination={{ itemsPerPage: 2 * DEFAULT_PER_PAGE_PAGINATION }}
/>
)}
</div>
);
};

@ -193,3 +193,7 @@ export interface PromBasedDataSource {
id: string | number;
rulerConfig?: RulerDataSourceConfig;
}
export interface PaginationProps {
itemsPerPage: number;
}

Loading…
Cancel
Save