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 { 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,7 +92,12 @@ 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()}
|
||||
@ -89,10 +109,14 @@ export const DynamicTable = <T extends object>({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{items.map((item, index) => {
|
||||
{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'}>
|
||||
<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)}>
|
||||
@ -120,7 +144,18 @@ export const DynamicTable = <T extends object>({
|
||||
</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) => ({
|
||||
const getOptionComponent = (state: InstanceStateFilter) => {
|
||||
return function InstanceStateCounter() {
|
||||
return itemPerStateStats && itemPerStateStats[state] ? (
|
||||
<Tag name={itemPerStateStats[state].toFixed(0)} colorIndex={9} className={styles.tag} />
|
||||
) : null;
|
||||
};
|
||||
};
|
||||
|
||||
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),
|
||||
}));
|
||||
|
||||
export const AlertInstanceStateFilter = ({ className, onStateFilterChange, stateFilter, filterType }: Props) => {
|
||||
const stateOptions = useMemo(() => (filterType === 'grafana' ? grafanaOptions : promOptions), [filterType]);
|
||||
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) => {
|
||||
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 };
|
||||
|
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;
|
||||
}
|
||||
|
||||
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…
Reference in New Issue
Block a user