Alerting: Adds evaluation interval to group view (#59974)

This commit is contained in:
Gilles De Mey
2022-12-12 13:53:18 +01:00
committed by GitHub
parent 97fa63d14d
commit b71ae13b63
4 changed files with 79 additions and 65 deletions
@@ -322,8 +322,11 @@ describe('RuleList', () => {
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(2);
expect(groups[0]).toHaveTextContent('1 rule');
expect(groups[1]).toHaveTextContent('4 rules: 1 firing, 1 pending');
expect(groups[0]).toHaveTextContent('1 firing');
expect(groups[1]).toHaveTextContent('1 firing');
expect(groups[1]).toHaveTextContent('1 pending');
expect(groups[1]).toHaveTextContent('1 recording');
expect(groups[1]).toHaveTextContent('1 normal');
// expand second group to see rules table
expect(ui.rulesTable.query()).not.toBeInTheDocument();
@@ -105,7 +105,7 @@ const RuleList = withErrorBoundary(
{expandAll ? 'Collapse all' : 'Expand all'}
</Button>
)}
<RuleStats showInactive={true} showRecording={true} namespaces={filteredNamespaces} />
<RuleStats namespaces={filteredNamespaces} includeTotal />
</div>
{(canCreateGrafanaRules || canCreateCloudRules) && (
<LinkButton
@@ -1,15 +1,15 @@
import pluralize from 'pluralize';
import React, { FC, Fragment, useMemo } from 'react';
import { Stack } from '@grafana/experimental';
import { Badge } from '@grafana/ui';
import { CombinedRule, CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { isAlertingRule, isRecordingRule, isRecordingRulerRule } from '../../utils/rules';
import { StateColoredText } from '../StateColoredText';
interface Props {
showInactive?: boolean;
showRecording?: boolean;
includeTotal?: boolean;
group?: CombinedRuleGroup;
namespaces?: CombinedRuleNamespace[];
}
@@ -23,14 +23,17 @@ const emptyStats = {
error: 0,
} as const;
export const RuleStats: FC<Props> = ({ showInactive, showRecording, group, namespaces }) => {
export const RuleStats: FC<Props> = ({ group, namespaces, includeTotal }) => {
const evaluationInterval = group?.interval;
const calculated = useMemo(() => {
const stats = { ...emptyStats };
const calcRule = (rule: CombinedRule) => {
if (rule.promRule && isAlertingRule(rule.promRule)) {
stats[rule.promRule.state] += 1;
}
if (rule.promRule?.health === 'err' || rule.promRule?.health === 'error') {
if (ruleHasError(rule)) {
stats.error += 1;
}
if (
@@ -41,75 +44,73 @@ export const RuleStats: FC<Props> = ({ showInactive, showRecording, group, names
}
stats.total += 1;
};
if (group) {
group.rules.forEach(calcRule);
}
if (namespaces) {
namespaces.forEach((namespace) => namespace.groups.forEach((group) => group.rules.forEach(calcRule)));
}
return stats;
}, [group, namespaces]);
const statsComponents: React.ReactNode[] = [];
if (calculated[PromAlertingRuleState.Firing]) {
if (includeTotal) {
statsComponents.push(
<StateColoredText key="firing" status={PromAlertingRuleState.Firing}>
{calculated[PromAlertingRuleState.Firing]} firing
</StateColoredText>
);
}
if (calculated.error) {
statsComponents.push(
<StateColoredText key="errors" status={PromAlertingRuleState.Firing}>
{calculated.error} errors
</StateColoredText>
);
}
if (calculated[PromAlertingRuleState.Pending]) {
statsComponents.push(
<StateColoredText key="pending" status={PromAlertingRuleState.Pending}>
{calculated[PromAlertingRuleState.Pending]} pending
</StateColoredText>
);
}
if (showInactive && calculated[PromAlertingRuleState.Inactive]) {
statsComponents.push(
<StateColoredText key="inactive" status="neutral">
{calculated[PromAlertingRuleState.Inactive]} normal
</StateColoredText>
);
}
if (showRecording && calculated.recording) {
statsComponents.push(
<StateColoredText key="recording" status="neutral">
{calculated.recording} recording
</StateColoredText>
<Fragment key="total">
{calculated.total} {pluralize('rule', calculated.total)}
</Fragment>
);
}
if (calculated[PromAlertingRuleState.Firing]) {
statsComponents.push(
<Badge color="red" key="firing" text={`${calculated[PromAlertingRuleState.Firing]} firing`} />
);
}
if (calculated.error) {
statsComponents.push(<Badge color="red" key="errors" text={`${calculated.error} errors`} />);
}
if (calculated[PromAlertingRuleState.Pending]) {
statsComponents.push(
<Badge color={'orange'} key="pending" text={`${calculated[PromAlertingRuleState.Pending]} pending`} />
);
}
if (calculated[PromAlertingRuleState.Inactive]) {
statsComponents.push(
<Badge color="green" key="inactive" text={`${calculated[PromAlertingRuleState.Inactive]} normal`} />
);
}
if (calculated.recording) {
statsComponents.push(<Badge color="purple" key="recording" text={`${calculated.recording} recording`} />);
}
const hasStats = Boolean(statsComponents.length);
return (
<div>
<span>
{calculated.total} {pluralize('rule', calculated.total)}
</span>
{!!statsComponents.length && (
<Stack direction="row">
{hasStats && (
<div>
<Stack gap={0.5}>{statsComponents}</Stack>
</div>
)}
{evaluationInterval && (
<>
<span>: </span>
{statsComponents.reduce<React.ReactNode[]>(
(prev, curr, idx) =>
prev.length
? [
prev,
<Fragment key={idx}>
<span>, </span>
</Fragment>,
curr,
]
: [curr],
[]
)}
<div>|</div>
<Badge text={evaluationInterval} icon="clock-nine" color={'blue'} />
</>
)}
</div>
</Stack>
);
};
function ruleHasError(rule: CombinedRule) {
return rule.promRule?.health === 'err' || rule.promRule?.health === 'error';
}
@@ -3,6 +3,7 @@ import pluralize from 'pluralize';
import React, { FC, useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { logInfo } from '@grafana/runtime';
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
import { useDispatch } from 'app/types';
@@ -213,12 +214,22 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll,
</h6>
<div className={styles.spacer} />
<div className={styles.headerStats}>
<RuleStats showInactive={false} group={group} />
<RuleStats group={group} />
</div>
{isProvisioned && (
<>
<div className={styles.actionsSeparator}>|</div>
<div className={styles.actionIcons}>
<Badge color="purple" text="Provisioned" />
</div>
</>
)}
{!!actionIcons.length && (
<>
<div className={styles.actionsSeparator}>|</div>
<div className={styles.actionIcons}>{actionIcons}</div>
<div className={styles.actionIcons}>
<Stack gap={0.5}>{actionIcons}</Stack>
</div>
</>
)}
</div>
@@ -313,9 +324,8 @@ export const getStyles = (theme: GrafanaTheme2) => ({
margin: 0 ${theme.spacing(2)};
`,
actionIcons: css`
& > * + * {
margin-left: ${theme.spacing(0.5)};
}
width: 80px;
align-items: center;
`,
rulesTable: css`
margin-top: ${theme.spacing(3)};