mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Alert rules pagination (#50612)
This commit is contained in:
parent
1ca2e2b6c2
commit
765b995b1b
@ -1,4 +1,4 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { useStyles2 } from '../../themes';
|
import { useStyles2 } from '../../themes';
|
||||||
@ -16,6 +16,7 @@ export interface Props {
|
|||||||
hideWhenSinglePage?: boolean;
|
hideWhenSinglePage?: boolean;
|
||||||
/** Small version only shows the current page and the navigation buttons. */
|
/** Small version only shows the current page and the navigation buttons. */
|
||||||
showSmallVersion?: boolean;
|
showSmallVersion?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Pagination: React.FC<Props> = ({
|
export const Pagination: React.FC<Props> = ({
|
||||||
@ -24,6 +25,7 @@ export const Pagination: React.FC<Props> = ({
|
|||||||
onNavigate,
|
onNavigate,
|
||||||
hideWhenSinglePage,
|
hideWhenSinglePage,
|
||||||
showSmallVersion,
|
showSmallVersion,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const pageLengthToCondense = showSmallVersion ? 1 : 8;
|
const pageLengthToCondense = showSmallVersion ? 1 : 8;
|
||||||
@ -96,7 +98,7 @@ export const Pagination: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={cx(styles.container, className)}>
|
||||||
<ol>
|
<ol>
|
||||||
<li className={styles.item}>
|
<li className={styles.item}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants';
|
||||||
import { AlertQuery } from '../../../types/unified-alerting-dto';
|
import { AlertQuery } from '../../../types/unified-alerting-dto';
|
||||||
|
|
||||||
import { AlertLabels } from './components/AlertLabels';
|
import { AlertLabels } from './components/AlertLabels';
|
||||||
@ -179,7 +180,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<RuleDetailsMatchingInstances rule={rule} />
|
<RuleDetailsMatchingInstances rule={rule} pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }} />
|
||||||
</div>
|
</div>
|
||||||
</RuleViewerLayoutContent>
|
</RuleViewerLayoutContent>
|
||||||
{!isFederatedRule && data && Object.keys(data).length > 0 && (
|
{!isFederatedRule && data && Object.keys(data).length > 0 && (
|
||||||
|
@ -2,7 +2,14 @@ import { css, cx } from '@emotion/css';
|
|||||||
import React, { ReactNode, useState } from 'react';
|
import React, { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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> {
|
export interface DynamicTableColumnProps<T = unknown> {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
@ -23,6 +30,8 @@ export interface DynamicTableProps<T = unknown> {
|
|||||||
items: Array<DynamicTableItemProps<T>>;
|
items: Array<DynamicTableItemProps<T>>;
|
||||||
|
|
||||||
isExpandable?: boolean;
|
isExpandable?: boolean;
|
||||||
|
pagination?: DynamicTablePagination;
|
||||||
|
paginationStyles?: string;
|
||||||
|
|
||||||
// provide these to manually control expanded status
|
// provide these to manually control expanded status
|
||||||
onCollapse?: (item: DynamicTableItemProps<T>) => void;
|
onCollapse?: (item: DynamicTableItemProps<T>) => void;
|
||||||
@ -41,6 +50,8 @@ export interface DynamicTableProps<T = unknown> {
|
|||||||
index: number,
|
index: number,
|
||||||
items: Array<DynamicTableItemProps<T>>
|
items: Array<DynamicTableItemProps<T>>
|
||||||
) => ReactNode;
|
) => ReactNode;
|
||||||
|
|
||||||
|
footerRow?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicTable = <T extends object>({
|
export const DynamicTable = <T extends object>({
|
||||||
@ -52,12 +63,16 @@ export const DynamicTable = <T extends object>({
|
|||||||
isExpanded,
|
isExpanded,
|
||||||
renderExpandedContent,
|
renderExpandedContent,
|
||||||
testIdGenerator,
|
testIdGenerator,
|
||||||
|
pagination,
|
||||||
|
paginationStyles,
|
||||||
// render a cell BEFORE expand icon for header/ each row.
|
// render a cell BEFORE expand icon for header/ each row.
|
||||||
// currently use by RuleList to render guidelines
|
// currently use by RuleList to render guidelines
|
||||||
renderPrefixCell,
|
renderPrefixCell,
|
||||||
renderPrefixHeader,
|
renderPrefixHeader,
|
||||||
|
footerRow,
|
||||||
}: DynamicTableProps<T>) => {
|
}: DynamicTableProps<T>) => {
|
||||||
|
const defaultPaginationStyles = useStyles2(getPaginationStyles);
|
||||||
|
|
||||||
if ((onCollapse || onExpand || isExpanded) && !(onCollapse && onExpand && isExpanded)) {
|
if ((onCollapse || onExpand || isExpanded) && !(onCollapse && onExpand && isExpanded)) {
|
||||||
throw new Error('either all of onCollapse, onExpand, isExpanded must be provided, or none');
|
throw new Error('either all of onCollapse, onExpand, isExpanded must be provided, or none');
|
||||||
}
|
}
|
||||||
@ -77,50 +92,70 @@ export const DynamicTable = <T extends object>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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>
|
|
||||||
|
|
||||||
{items.map((item, index) => {
|
const itemsPerPage = pagination?.itemsPerPage ?? items.length;
|
||||||
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
|
const { page, numberOfPages, onPageChange, pageItems } = usePagination(items, 1, itemsPerPage);
|
||||||
return (
|
|
||||||
<div className={styles.row} key={`${item.id}-${index}`} data-testid={testIdGenerator?.(item, index) ?? 'row'}>
|
return (
|
||||||
{renderPrefixCell && renderPrefixCell(item, index, items)}
|
<>
|
||||||
{isExpandable && (
|
<div className={styles.container} data-testid="dynamic-table">
|
||||||
<div className={cx(styles.cell, styles.expandCell)}>
|
<div className={styles.row} data-testid="header">
|
||||||
<IconButton
|
{renderPrefixHeader && renderPrefixHeader()}
|
||||||
aria-label={`${isItemExpanded ? 'Collapse' : 'Expand'} row`}
|
{isExpandable && <div className={styles.cell} />}
|
||||||
size="xl"
|
{cols.map((col) => (
|
||||||
data-testid="collapse-toggle"
|
<div className={styles.cell} key={col.id}>
|
||||||
className={styles.expandButton}
|
{col.label}
|
||||||
name={isItemExpanded ? 'angle-down' : 'angle-right'}
|
</div>
|
||||||
onClick={() => toggleExpanded(item)}
|
))}
|
||||||
type="button"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
{pageItems.map((item, index) => {
|
||||||
)}
|
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
|
||||||
{cols.map((col) => (
|
return (
|
||||||
<div className={cx(styles.cell, styles.bodyCell)} data-column={col.label} key={`${item.id}-${col.id}`}>
|
<div
|
||||||
{col.renderCell(item, index)}
|
className={styles.row}
|
||||||
</div>
|
key={`${item.id}-${index}`}
|
||||||
))}
|
data-testid={testIdGenerator?.(item, index) ?? 'row'}
|
||||||
{isItemExpanded && renderExpandedContent && (
|
>
|
||||||
<div className={styles.expandedContentRow} data-testid="expanded-content">
|
{renderPrefixCell && renderPrefixCell(item, index, items)}
|
||||||
{renderExpandedContent(item, index, items)}
|
{isExpandable && (
|
||||||
</div>
|
<div className={cx(styles.cell, styles.expandCell)}>
|
||||||
)}
|
<IconButton
|
||||||
</div>
|
aria-label={`${isItemExpanded ? 'Collapse' : 'Expand'} row`}
|
||||||
);
|
size="xl"
|
||||||
})}
|
data-testid="collapse-toggle"
|
||||||
</div>
|
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`
|
cell: css`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: ${theme.spacing(1)};
|
padding: ${theme.spacing(1)};
|
||||||
@ -197,6 +236,7 @@ const getStyles = <T extends unknown>(
|
|||||||
`,
|
`,
|
||||||
bodyCell: css`
|
bodyCell: css`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
${theme.breakpoints.down('sm')} {
|
${theme.breakpoints.down('sm')} {
|
||||||
grid-column-end: right;
|
grid-column-end: right;
|
||||||
grid-column-start: right;
|
grid-column-start: right;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
import { capitalize } from 'lodash';
|
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';
|
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
export type InstanceStateFilter = GrafanaAlertState | PromAlertingRuleState.Pending | PromAlertingRuleState.Firing;
|
export type InstanceStateFilter = GrafanaAlertState | PromAlertingRuleState.Pending | PromAlertingRuleState.Firing;
|
||||||
@ -11,21 +13,40 @@ interface Props {
|
|||||||
filterType: 'grafana' | 'prometheus';
|
filterType: 'grafana' | 'prometheus';
|
||||||
stateFilter?: InstanceStateFilter;
|
stateFilter?: InstanceStateFilter;
|
||||||
onStateFilterChange: (value?: InstanceStateFilter) => void;
|
onStateFilterChange: (value?: InstanceStateFilter) => void;
|
||||||
|
itemPerStateStats?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const grafanaOptions = Object.values(GrafanaAlertState).map((value) => ({
|
export const AlertInstanceStateFilter = ({
|
||||||
label: value,
|
className,
|
||||||
value,
|
onStateFilterChange,
|
||||||
}));
|
stateFilter,
|
||||||
|
filterType,
|
||||||
|
itemPerStateStats,
|
||||||
|
}: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const promOptionValues = [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending] as const;
|
const getOptionComponent = (state: InstanceStateFilter) => {
|
||||||
const promOptions = promOptionValues.map((state) => ({
|
return function InstanceStateCounter() {
|
||||||
label: capitalize(state),
|
return itemPerStateStats && itemPerStateStats[state] ? (
|
||||||
value: state,
|
<Tag name={itemPerStateStats[state].toFixed(0)} colorIndex={9} className={styles.tag} />
|
||||||
}));
|
) : null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const AlertInstanceStateFilter = ({ className, onStateFilterChange, stateFilter, filterType }: Props) => {
|
const grafanaOptions = Object.values(GrafanaAlertState).map((state) => ({
|
||||||
const stateOptions = useMemo(() => (filterType === 'grafana' ? grafanaOptions : promOptions), [filterType]);
|
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 (
|
return (
|
||||||
<div className={className} data-testid="alert-instance-state-filter">
|
<div className={className} data-testid="alert-instance-state-filter">
|
||||||
@ -43,3 +64,15 @@ export const AlertInstanceStateFilter = ({ className, onStateFilterChange, state
|
|||||||
</div>
|
</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 React, { FC, useMemo } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { Alert, PaginationProps } from 'app/types/unified-alerting';
|
||||||
import { Alert } from 'app/types/unified-alerting';
|
|
||||||
|
|
||||||
import { alertInstanceKey } from '../../utils/rules';
|
import { alertInstanceKey } from '../../utils/rules';
|
||||||
import { AlertLabels } from '../AlertLabels';
|
import { AlertLabels } from '../AlertLabels';
|
||||||
@ -13,12 +11,14 @@ import { AlertStateTag } from './AlertStateTag';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instances: Alert[];
|
instances: Alert[];
|
||||||
|
pagination?: PaginationProps;
|
||||||
|
footerRow?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertTableColumnProps = DynamicTableColumnProps<Alert>;
|
type AlertTableColumnProps = DynamicTableColumnProps<Alert>;
|
||||||
type AlertTableItemProps = DynamicTableItemProps<Alert>;
|
type AlertTableItemProps = DynamicTableItemProps<Alert>;
|
||||||
|
|
||||||
export const AlertInstancesTable: FC<Props> = ({ instances }) => {
|
export const AlertInstancesTable: FC<Props> = ({ instances, pagination, footerRow }) => {
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
(): AlertTableItemProps[] =>
|
(): AlertTableItemProps[] =>
|
||||||
instances.map((instance) => ({
|
instances.map((instance) => ({
|
||||||
@ -34,33 +34,12 @@ export const AlertInstancesTable: FC<Props> = ({ instances }) => {
|
|||||||
isExpandable={true}
|
isExpandable={true}
|
||||||
items={items}
|
items={items}
|
||||||
renderExpandedContent={({ data }) => <AlertInstanceDetails instance={data} />}
|
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[] = [
|
const columns: AlertTableColumnProps[] = [
|
||||||
{
|
{
|
||||||
id: 'state',
|
id: 'state',
|
||||||
|
@ -2,14 +2,18 @@ import { css } from '@emotion/css';
|
|||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { LoadingPlaceholder, useStyles } from '@grafana/ui';
|
import { LoadingPlaceholder, Pagination, useStyles2 } from '@grafana/ui';
|
||||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
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 { 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 { RulesGroup } from './RulesGroup';
|
||||||
|
import { useCombinedGroupNamespace } from './useCombinedGroupNamespace';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
namespaces: CombinedRuleNamespace[];
|
namespaces: CombinedRuleNamespace[];
|
||||||
@ -17,15 +21,23 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CloudRules: FC<Props> = ({ namespaces, expandAll }) => {
|
export const CloudRules: FC<Props> = ({ namespaces, expandAll }) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const rules = useUnifiedAlertingSelector((state) => state.promRules);
|
const rules = useUnifiedAlertingSelector((state) => state.promRules);
|
||||||
const rulesDataSources = useMemo(getRulesDataSources, []);
|
const rulesDataSources = useMemo(getRulesDataSources, []);
|
||||||
|
const groupsWithNamespaces = useCombinedGroupNamespace(namespaces);
|
||||||
|
|
||||||
const dataSourcesLoading = useMemo(
|
const dataSourcesLoading = useMemo(
|
||||||
() => rulesDataSources.filter((ds) => rules[ds.name]?.loading),
|
() => rulesDataSources.filter((ds) => rules[ds.name]?.loading),
|
||||||
[rules, rulesDataSources]
|
[rules, rulesDataSources]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { numberOfPages, onPageChange, page, pageItems } = usePagination(
|
||||||
|
groupsWithNamespaces,
|
||||||
|
1,
|
||||||
|
DEFAULT_PER_PAGE_PAGINATION
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.wrapper}>
|
<section className={styles.wrapper}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
@ -40,24 +52,30 @@ export const CloudRules: FC<Props> = ({ namespaces, expandAll }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{namespaces.map((namespace) => {
|
{pageItems.map(({ group, namespace }) => {
|
||||||
const { groups, rulesSource } = namespace;
|
return (
|
||||||
return groups.map((group) => (
|
|
||||||
<RulesGroup
|
<RulesGroup
|
||||||
group={group}
|
group={group}
|
||||||
key={`${getRulesSourceName(rulesSource)}-${name}-${group.name}`}
|
key={`${getRulesSourceUid(namespace.rulesSource)}-${namespace.name}-${group.name}`}
|
||||||
namespace={namespace}
|
namespace={namespace}
|
||||||
expandAll={expandAll}
|
expandAll={expandAll}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
})}
|
})}
|
||||||
{namespaces?.length === 0 && !!rulesDataSources.length && <p>No rules found.</p>}
|
{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>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
loader: css`
|
loader: css`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`,
|
`,
|
||||||
@ -66,6 +84,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`,
|
`,
|
||||||
wrapper: css`
|
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 { css } from '@emotion/css';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { LoadingPlaceholder, useStyles } from '@grafana/ui';
|
import { LoadingPlaceholder, Pagination, useStyles2 } from '@grafana/ui';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||||
import { flattenGrafanaManagedRules } from '../../hooks/useCombinedRuleNamespaces';
|
import { flattenGrafanaManagedRules } from '../../hooks/useCombinedRuleNamespaces';
|
||||||
|
import { usePagination } from '../../hooks/usePagination';
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
|
import { getPaginationStyles } from '../../styles/pagination';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { initialAsyncRequestState } from '../../utils/redux';
|
import { initialAsyncRequestState } from '../../utils/redux';
|
||||||
|
|
||||||
import { RulesGroup } from './RulesGroup';
|
import { RulesGroup } from './RulesGroup';
|
||||||
|
import { useCombinedGroupNamespace } from './useCombinedGroupNamespace';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
namespaces: CombinedRuleNamespace[];
|
namespaces: CombinedRuleNamespace[];
|
||||||
@ -19,7 +23,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GrafanaRules: FC<Props> = ({ namespaces, expandAll }) => {
|
export const GrafanaRules: FC<Props> = ({ namespaces, expandAll }) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
|
|
||||||
const { loading } = useUnifiedAlertingSelector(
|
const { loading } = useUnifiedAlertingSelector(
|
||||||
@ -29,6 +33,14 @@ export const GrafanaRules: FC<Props> = ({ namespaces, expandAll }) => {
|
|||||||
const wantsGroupedView = queryParams['view'] === 'grouped';
|
const wantsGroupedView = queryParams['view'] === 'grouped';
|
||||||
const namespacesFormat = wantsGroupedView ? namespaces : flattenGrafanaManagedRules(namespaces);
|
const namespacesFormat = wantsGroupedView ? namespaces : flattenGrafanaManagedRules(namespaces);
|
||||||
|
|
||||||
|
const groupsWithNamespaces = useCombinedGroupNamespace(namespacesFormat);
|
||||||
|
|
||||||
|
const { numberOfPages, onPageChange, page, pageItems } = usePagination(
|
||||||
|
groupsWithNamespaces,
|
||||||
|
1,
|
||||||
|
DEFAULT_PER_PAGE_PAGINATION
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.wrapper}>
|
<section className={styles.wrapper}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
@ -36,22 +48,22 @@ export const GrafanaRules: FC<Props> = ({ namespaces, expandAll }) => {
|
|||||||
{loading ? <LoadingPlaceholder className={styles.loader} text="Loading..." /> : <div />}
|
{loading ? <LoadingPlaceholder className={styles.loader} text="Loading..." /> : <div />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{namespacesFormat?.map((namespace) =>
|
{pageItems.map(({ group, namespace }) => (
|
||||||
namespace.groups.map((group) => (
|
<RulesGroup group={group} key={`${namespace.name}-${group.name}`} namespace={namespace} expandAll={expandAll} />
|
||||||
<RulesGroup
|
))}
|
||||||
group={group}
|
|
||||||
key={`${namespace.name}-${group.name}`}
|
|
||||||
namespace={namespace}
|
|
||||||
expandAll={expandAll}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{namespacesFormat?.length === 0 && <p>No rules found.</p>}
|
{namespacesFormat?.length === 0 && <p>No rules found.</p>}
|
||||||
|
<Pagination
|
||||||
|
className={styles.pagination}
|
||||||
|
currentPage={page}
|
||||||
|
numberOfPages={numberOfPages}
|
||||||
|
onNavigate={onPageChange}
|
||||||
|
hideWhenSinglePage
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
loader: css`
|
loader: css`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`,
|
`,
|
||||||
@ -60,6 +72,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`,
|
`,
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
margin-bottom: ${theme.spacing.xl};
|
margin-bottom: ${theme.spacing(4)};
|
||||||
`,
|
`,
|
||||||
|
pagination: getPaginationStyles(theme),
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,11 @@ interface Props {
|
|||||||
rule: CombinedRule;
|
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 }) => {
|
export const RuleDetails: FC<Props> = ({ rule }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const {
|
const {
|
||||||
@ -43,7 +48,7 @@ export const RuleDetails: FC<Props> = ({ rule }) => {
|
|||||||
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
|
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RuleDetailsMatchingInstances rule={rule} />
|
<RuleDetailsMatchingInstances rule={rule} itemsDisplayLimit={INSTANCES_DISPLAY_LIMIT} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -14,15 +14,15 @@ const ui = {
|
|||||||
stateFilter: byTestId('alert-instance-state-filter'),
|
stateFilter: byTestId('alert-instance-state-filter'),
|
||||||
stateButton: byRole('radio'),
|
stateButton: byRole('radio'),
|
||||||
grafanaStateButton: {
|
grafanaStateButton: {
|
||||||
normal: byLabelText('Normal'),
|
normal: byLabelText(/^Normal/),
|
||||||
alerting: byLabelText('Alerting'),
|
alerting: byLabelText(/^Alerting/),
|
||||||
pending: byLabelText('Pending'),
|
pending: byLabelText(/^Pending/),
|
||||||
noData: byLabelText('NoData'),
|
noData: byLabelText(/^NoData/),
|
||||||
error: byLabelText('Error'),
|
error: byLabelText(/^Error/),
|
||||||
},
|
},
|
||||||
cloudStateButton: {
|
cloudStateButton: {
|
||||||
firing: byLabelText('Firing'),
|
firing: byLabelText(/^Firing/),
|
||||||
pending: byLabelText('Pending'),
|
pending: byLabelText(/^Pending/),
|
||||||
},
|
},
|
||||||
instanceRow: byTestId('row'),
|
instanceRow: byTestId('row'),
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { countBy } from 'lodash';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
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 { MatcherFilter } from 'app/features/alerting/unified/components/alert-groups/MatcherFilter';
|
||||||
import {
|
import {
|
||||||
AlertInstanceStateFilter,
|
AlertInstanceStateFilter,
|
||||||
InstanceStateFilter,
|
InstanceStateFilter,
|
||||||
} from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter';
|
} from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter';
|
||||||
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
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 { 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 { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
@ -20,13 +21,40 @@ import { DetailsField } from '../DetailsField';
|
|||||||
|
|
||||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||||
|
|
||||||
type Props = {
|
interface Props {
|
||||||
rule: CombinedRule;
|
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 {
|
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
rule: { promRule, namespace },
|
rule: { promRule, namespace },
|
||||||
|
itemsDisplayLimit = Number.POSITIVE_INFINITY,
|
||||||
|
pagination,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [queryString, setQueryString] = useState<string>();
|
const [queryString, setQueryString] = useState<string>();
|
||||||
@ -52,6 +80,22 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
return 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 (
|
return (
|
||||||
<DetailsField label="Matching instances" horizontal={true}>
|
<DetailsField label="Matching instances" horizontal={true}>
|
||||||
<div className={cx(styles.flexRow, styles.spaceBetween)}>
|
<div className={cx(styles.flexRow, styles.spaceBetween)}>
|
||||||
@ -67,11 +111,12 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
filterType={stateFilterType}
|
filterType={stateFilterType}
|
||||||
stateFilter={alertState}
|
stateFilter={alertState}
|
||||||
onStateFilterChange={setAlertState}
|
onStateFilterChange={setAlertState}
|
||||||
|
itemPerStateStats={countAllByState}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AlertInstancesTable instances={alerts} />
|
<AlertInstancesTable instances={visibleInstances} pagination={pagination} footerRow={footerRow} />
|
||||||
</DetailsField>
|
</DetailsField>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -111,5 +156,13 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
rowChild: css`
|
rowChild: css`
|
||||||
margin-right: ${theme.spacing.sm};
|
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 { useStyles2 } from '@grafana/ui';
|
||||||
import { CombinedRule } from 'app/types/unified-alerting';
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||||
import { Annotation } from '../../utils/constants';
|
import { Annotation } from '../../utils/constants';
|
||||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||||
@ -71,6 +72,8 @@ export const RulesTable: FC<Props> = ({
|
|||||||
isExpandable={true}
|
isExpandable={true}
|
||||||
items={items}
|
items={items}
|
||||||
renderExpandedContent={({ data: rule }) => <RuleDetails rule={rule} />}
|
renderExpandedContent={({ data: rule }) => <RuleDetails rule={rule} />}
|
||||||
|
pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }}
|
||||||
|
paginationStyles={styles.pagination}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -87,9 +90,18 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
`,
|
`,
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
width: auto;
|
width: auto;
|
||||||
background-color: ${theme.colors.background.secondary};
|
|
||||||
border-radius: ${theme.shape.borderRadius()};
|
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) {
|
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) {
|
export function usePagination<T>(items: T[], initialPage: number, itemsPerPage: number) {
|
||||||
const [page, setPage] = useState(initialPage);
|
const [page, setPage] = useState(initialPage);
|
||||||
|
|
||||||
const numberOfPages = Math.ceil(items.length / itemsPerPage);
|
const numberOfPages = Math.ceil(items.length / itemsPerPage);
|
||||||
|
|
||||||
const firstItemOnPageIndex = itemsPerPage * (page - 1);
|
const firstItemOnPageIndex = itemsPerPage * (page - 1);
|
||||||
const pageItems = items.slice(firstItemOnPageIndex, firstItemOnPageIndex + itemsPerPage);
|
|
||||||
|
|
||||||
const onPageChange = (newPage: number) => {
|
const pageItems = useMemo(
|
||||||
setPage(newPage);
|
() => 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]);
|
useEffect(() => setPage(1), [numberOfPages]);
|
||||||
|
|
||||||
return { page, onPageChange, numberOfPages, pageItems };
|
return { page, onPageChange, numberOfPages, pageItems };
|
||||||
|
12
public/app/features/alerting/unified/styles/pagination.ts
Normal file
12
public/app/features/alerting/unified/styles/pagination.ts
Normal file
@ -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;
|
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 {
|
export function isCloudRulesSource(rulesSource: RulesSource | string): rulesSource is DataSourceInstanceSettings {
|
||||||
return rulesSource !== GRAFANA_RULES_SOURCE_NAME;
|
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 { sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
||||||
import { Alert } from 'app/types/unified-alerting';
|
import { Alert } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants';
|
||||||
|
|
||||||
import { GroupMode, UnifiedAlertListOptions } from './types';
|
import { GroupMode, UnifiedAlertListOptions } from './types';
|
||||||
import { filterAlerts } from './util';
|
import { filterAlerts } from './util';
|
||||||
|
|
||||||
@ -52,7 +54,12 @@ export const AlertInstances: FC<Props> = ({ alerts, options }) => {
|
|||||||
{hiddenInstances > 0 && <span>, {`${hiddenInstances} hidden by filters`}</span>}
|
{hiddenInstances > 0 && <span>, {`${hiddenInstances} hidden by filters`}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{displayInstances && <AlertInstancesTable instances={filteredAlerts} />}
|
{displayInstances && (
|
||||||
|
<AlertInstancesTable
|
||||||
|
instances={filteredAlerts}
|
||||||
|
pagination={{ itemsPerPage: 2 * DEFAULT_PER_PAGE_PAGINATION }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -193,3 +193,7 @@ export interface PromBasedDataSource {
|
|||||||
id: string | number;
|
id: string | number;
|
||||||
rulerConfig?: RulerDataSourceConfig;
|
rulerConfig?: RulerDataSourceConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginationProps {
|
||||||
|
itemsPerPage: number;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user