diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
index ed59a8e9791..992eba0a1a2 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
@@ -12,18 +12,21 @@ import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
import {
getCloudRule,
getGrafanaRule,
+ getVanillaPromRule,
grantUserPermissions,
mockDataSource,
mockPluginLinkExtension,
+ mockPromAlertingRule,
} from '../../mocks';
import { grafanaRulerRule } from '../../mocks/grafanaRulerApi';
import { setupDataSources } from '../../testSetup/datasources';
import { Annotation } from '../../utils/constants';
import { DataSourceType } from '../../utils/datasource';
import * as ruleId from '../../utils/rule-id';
+import { stringifyIdentifier } from '../../utils/rule-id';
import { AlertRuleProvider } from './RuleContext';
-import RuleViewer from './RuleViewer';
+import RuleViewer, { ActiveTab } from './RuleViewer';
// metadata and interactive elements
const ELEMENTS = {
@@ -35,6 +38,12 @@ const ELEMENTS = {
evaluationInterval: (interval: string) => byText(`Every ${interval}`),
label: ([key, value]: [string, string]) => byRole('listitem', { name: `${key}: ${value}` }),
},
+ details: {
+ pendingPeriod: byText(/Pending period/i),
+ },
+ tabs: {
+ details: byRole('tab', { name: /Details/i }),
+ },
actions: {
edit: byRole('link', { name: 'Edit' }),
more: {
@@ -259,13 +268,39 @@ describe('RuleViewer', () => {
await waitFor(() => expect(ELEMENTS.actions.more.pluginActions.declareIncident.get()).toBeEnabled());
});
});
+
+ describe('Vanilla Prometheus rule', () => {
+ const mockRule = getVanillaPromRule({
+ name: 'prom test alert',
+ annotations: { [Annotation.summary]: 'prom summary', [Annotation.runbookURL]: 'https://runbook.example.com' },
+ promRule: {
+ ...mockPromAlertingRule(),
+ duration: 900, // 15 minutes
+ },
+ });
+
+ const mockRuleIdentifier = ruleId.fromCombinedRule('prometheus', mockRule);
+
+ it('should render pending period for vanilla Prometheus alert rule', async () => {
+ renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Details);
+
+ expect(screen.getByText('prom test alert')).toBeInTheDocument();
+
+ // One summary is rendered by the Title component, and the other by the DetailsTab component
+ expect(ELEMENTS.metadata.summary(mockRule.annotations[Annotation.summary]).getAll()).toHaveLength(2);
+
+ expect(within(ELEMENTS.details.pendingPeriod.get()).getByText(/15m/i)).toBeInTheDocument();
+ });
+ });
});
-const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier) => {
+const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier, tab: ActiveTab = ActiveTab.Query) => {
+ const path = `/alerting/${identifier.ruleSourceName}/${stringifyIdentifier(identifier)}/view?tab=${tab}`;
render(
-
+ ,
+ { historyOptions: { initialEntries: [path] } }
);
await waitFor(() => expect(ELEMENTS.loading.query()).not.toBeInTheDocument());
diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx
index 5c723326abb..53c74196586 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx
@@ -7,6 +7,7 @@ import { ClipboardButton, Stack, Text, TextLink, useStyles2 } from '@grafana/ui'
import { CombinedRule } from 'app/types/unified-alerting';
import { Annotations } from 'app/types/unified-alerting-dto';
+import { usePendingPeriod } from '../../../hooks/rules/usePendingPeriod';
import { isGrafanaRulerRule, isRecordingRulerRule } from '../../../utils/rules';
import { MetaText } from '../../MetaText';
import { Tokenize } from '../../Tokenize';
@@ -26,6 +27,8 @@ const Details = ({ rule }: DetailsProps) => {
let ruleType: RuleType;
+ const pendingPeriod = usePendingPeriod(rule);
+
if (isGrafanaRulerRule(rule.rulerRule)) {
ruleType = RuleType.GrafanaManagedAlertRule;
} else if (isRecordingRulerRule(rule.rulerRule)) {
@@ -89,10 +92,10 @@ const Details = ({ rule }: DetailsProps) => {
)}
- {!isRecordingRulerRule(rule.rulerRule) && (
+ {pendingPeriod && (
<>
Pending period
- {rule.rulerRule?.for ?? '0s'}
+ {pendingPeriod}
>
)}
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
index 6c8ddb552e1..7c7659a5e32 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
@@ -5,8 +5,9 @@ import { Tooltip, useStyles2 } from '@grafana/ui';
import { Time } from 'app/features/explore/Time';
import { CombinedRule } from 'app/types/unified-alerting';
+import { usePendingPeriod } from '../../hooks/rules/usePendingPeriod';
import { useCleanAnnotations } from '../../utils/annotations';
-import { isGrafanaRecordingRule, isRecordingRulerRule } from '../../utils/rules';
+import { isGrafanaRecordingRule } from '../../utils/rules';
import { isNullDate } from '../../utils/time';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
@@ -64,16 +65,12 @@ interface EvaluationBehaviorSummaryProps {
}
const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => {
- let forDuration: string | undefined;
const every = rule.group.interval;
const lastEvaluation = rule.promRule?.lastEvaluation;
const lastEvaluationDuration = rule.promRule?.evaluationTime;
const metric = isGrafanaRecordingRule(rule.rulerRule) ? rule.rulerRule?.grafana_alert.record?.metric : undefined;
- // recording rules don't have a for duration
- if (!isRecordingRulerRule(rule.rulerRule)) {
- forDuration = rule.rulerRule?.for ?? '0s';
- }
+ const pendingPeriod = usePendingPeriod(rule);
return (
<>
@@ -88,9 +85,11 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
)}
-
- {forDuration}
-
+ {pendingPeriod && (
+
+ {pendingPeriod}
+
+ )}
{lastEvaluation && !isNullDate(lastEvaluation) && (
diff --git a/public/app/features/alerting/unified/hooks/rules/usePendingPeriod.tsx b/public/app/features/alerting/unified/hooks/rules/usePendingPeriod.tsx
new file mode 100644
index 00000000000..d7ebd337f7c
--- /dev/null
+++ b/public/app/features/alerting/unified/hooks/rules/usePendingPeriod.tsx
@@ -0,0 +1,9 @@
+import { useMemo } from 'react';
+
+import { CombinedRule } from 'app/types/unified-alerting';
+
+import { getPendingPeriod } from '../../utils/rules';
+
+export function usePendingPeriod(rule: CombinedRule): string | undefined {
+ return useMemo(() => getPendingPeriod(rule), [rule]);
+}
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 699252137db..632868a90e0 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -785,6 +785,19 @@ export function getCloudRule(override?: Partial) {
});
}
+export function getVanillaPromRule(override?: Partial>) {
+ return mockCombinedRule({
+ namespace: {
+ groups: [],
+ name: 'Prometheus',
+ rulesSource: mockDataSource(),
+ },
+ promRule: mockPromAlertingRule(),
+ rulerRule: undefined,
+ ...override,
+ });
+}
+
export function mockPluginLinkExtension(extension: Partial): PluginExtensionLink {
return {
type: PluginExtensionTypes.link,
diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts
index fe4aa16b4b4..cc4a7af1af9 100644
--- a/public/app/features/alerting/unified/utils/rules.ts
+++ b/public/app/features/alerting/unified/utils/rules.ts
@@ -44,7 +44,7 @@ import { RULER_NOT_SUPPORTED_MSG } from './constants';
import { getRulesSourceName, isGrafanaRulesSource } from './datasource';
import { GRAFANA_ORIGIN_LABEL } from './labels';
import { AsyncRequestState } from './redux';
-import { safeParsePrometheusDuration } from './time';
+import { formatPrometheusDuration, safeParsePrometheusDuration } from './time';
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
@@ -137,6 +137,26 @@ export function getRuleHealth(health: string): RuleHealth | undefined {
}
}
+export function getPendingPeriod(rule: CombinedRule): string | undefined {
+ if (isRecordingRulerRule(rule.rulerRule) || isRecordingRule(rule.promRule)) {
+ return undefined;
+ }
+
+ // We prefer the for duration from the ruler rule because it is formatted as a duration string
+ // Prometheus duration is in seconds and we need to format it as a duration string
+ // Additionally, due to eventual consistency of the Prometheus endpoint the ruler data might be newer
+ if (isAlertingRulerRule(rule.rulerRule)) {
+ return rule.rulerRule.for;
+ }
+
+ if (isAlertingRule(rule.promRule)) {
+ const durationInMilliseconds = (rule.promRule.duration ?? 0) * 1000;
+ return formatPrometheusDuration(durationInMilliseconds);
+ }
+
+ return undefined;
+}
+
export interface RulePluginOrigin {
pluginId: string;
}
diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts
index 338fa189fc9..734e6c650ad 100644
--- a/public/app/types/unified-alerting.ts
+++ b/public/app/types/unified-alerting.ts
@@ -50,6 +50,11 @@ export interface AlertingRule extends RuleBase {
};
state: PromAlertingRuleState;
type: PromRuleType.Alerting;
+
+ /**
+ * Pending period in seconds, aka for. 0 or undefined means no pending period
+ */
+ duration?: number;
totals?: Partial, number>>;
totalsFiltered?: Partial, number>>;
activeAt?: string; // ISO timestamp