diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
index 5d078a0f6a6..e13a118f42f 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
@@ -1,11 +1,13 @@
import { css } from '@emotion/css';
import React, { FC } from 'react';
-import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2 } from '@grafana/ui';
+import { GrafanaTheme2, dateTime, dateTimeFormat } from '@grafana/data';
+import { useStyles2, Tooltip } from '@grafana/ui';
+import { Time } from 'app/features/explore/Time';
import { CombinedRule } from 'app/types/unified-alerting';
import { isRecordingRulerRule } from '../../utils/rules';
+import { isNullDate } from '../../utils/time';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
@@ -62,6 +64,8 @@ interface EvaluationBehaviorSummaryProps {
const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => {
let forDuration: string | undefined;
let every = rule.group.interval;
+ let lastEvaluation = rule.promRule?.lastEvaluation;
+ let lastEvaluationDuration = rule.promRule?.evaluationTime;
// recording rules don't have a for duration
if (!isRecordingRulerRule(rule.rulerRule)) {
@@ -80,6 +84,26 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
{forDuration}
)}
+
+ {lastEvaluation && !isNullDate(lastEvaluation) && (
+
+
+ {`${dateTime(lastEvaluation).locale('en').fromNow(true)} ago`}
+
+
+ )}
+
+ {lastEvaluation && !isNullDate(lastEvaluation) && lastEvaluationDuration !== undefined && (
+
+
+ {Time({ timeInMs: lastEvaluationDuration * 1000, humanize: true })}
+
+
+ )}
>
);
};
diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
index 2a6021e80c2..a7e913e4a96 100644
--- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
@@ -223,7 +223,13 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll,
)}
{!isCollapsed && (
-
+
)}
{isEditingGroup && closeEditModal()} />}
{isReorderingGroup && (
diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx
index affa11b32f2..b04b12b8960 100644
--- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx
@@ -1,14 +1,23 @@
import { css, cx } from '@emotion/css';
-import React, { FC, useMemo } from 'react';
+import React, { FC, useCallback, useMemo } from 'react';
-import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2 } from '@grafana/ui';
+import {
+ GrafanaTheme2,
+ addDurationToDate,
+ isValidDate,
+ isValidDuration,
+ parseDuration,
+ dateTimeFormat,
+ dateTime,
+} from '@grafana/data';
+import { useStyles2, Tooltip } 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';
+import { isNullDate } from '../../utils/time';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';
import { ProvisioningBadge } from '../Provisioning';
@@ -29,6 +38,7 @@ interface Props {
showGuidelines?: boolean;
showGroupColumn?: boolean;
showSummaryColumn?: boolean;
+ showNextEvaluationColumn?: boolean;
emptyMessage?: string;
className?: string;
}
@@ -40,6 +50,7 @@ export const RulesTable: FC = ({
emptyMessage = 'No rules found.',
showGroupColumn = false,
showSummaryColumn = false,
+ showNextEvaluationColumn = false,
}) => {
const styles = useStyles2(getStyles);
@@ -54,7 +65,7 @@ export const RulesTable: FC = ({
});
}, [rules]);
- const columns = useColumns(showSummaryColumn, showGroupColumn);
+ const columns = useColumns(showSummaryColumn, showGroupColumn, showNextEvaluationColumn);
if (!rules.length) {
return {emptyMessage}
;
@@ -101,9 +112,29 @@ export const getStyles = (theme: GrafanaTheme2) => ({
`,
});
-function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
+function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNextEvaluationColumn: boolean) {
const { hasRuler, rulerRulesLoaded } = useHasRuler();
+ const calculateNextEvaluationDate = useCallback((rule: CombinedRule) => {
+ const isValidLastEvaluation =
+ rule.promRule?.lastEvaluation &&
+ !isNullDate(rule.promRule.lastEvaluation) &&
+ isValidDate(rule.promRule.lastEvaluation);
+ const isValidIntervalDuration = rule.group.interval && isValidDuration(rule.group.interval);
+
+ if (!isValidLastEvaluation || !isValidIntervalDuration) {
+ return;
+ }
+
+ const lastEvaluationDate = Date.parse(rule.promRule?.lastEvaluation || '');
+ const intervalDuration = parseDuration(rule.group.interval!);
+ const nextEvaluationDate = addDurationToDate(lastEvaluationDate, intervalDuration);
+ return {
+ humanized: dateTime(nextEvaluationDate).locale('en').fromNow(true),
+ fullDate: dateTimeFormat(nextEvaluationDate, { format: 'YYYY-MM-DD HH:mm:ss' }),
+ };
+ }, []);
+
return useMemo((): RuleTableColumnProps[] => {
const columns: RuleTableColumnProps[] = [
{
@@ -125,7 +156,7 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
label: 'Name',
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => rule.name,
- size: 5,
+ size: showNextEvaluationColumn ? 4 : 5,
},
{
id: 'provisioned',
@@ -166,9 +197,28 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
renderCell: ({ data: rule }) => {
return ;
},
- size: 5,
+ size: showNextEvaluationColumn ? 4 : 5,
});
}
+
+ if (showNextEvaluationColumn) {
+ columns.push({
+ id: 'nextEvaluation',
+ label: 'Next evaluation',
+ renderCell: ({ data: rule }) => {
+ const nextEvalInfo = calculateNextEvaluationDate(rule);
+ return (
+ nextEvalInfo?.fullDate && (
+
+ in {nextEvalInfo?.humanized}
+
+ )
+ );
+ },
+ size: 2,
+ });
+ }
+
if (showGroupColumn) {
columns.push({
id: 'group',
@@ -200,5 +250,12 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
});
return columns;
- }, [showSummaryColumn, showGroupColumn, hasRuler, rulerRulesLoaded]);
+ }, [
+ showSummaryColumn,
+ showGroupColumn,
+ showNextEvaluationColumn,
+ hasRuler,
+ rulerRulesLoaded,
+ calculateNextEvaluationDate,
+ ]);
}
diff --git a/public/app/features/alerting/unified/utils/time.ts b/public/app/features/alerting/unified/utils/time.ts
index a6958b9d6ab..491c5b790ee 100644
--- a/public/app/features/alerting/unified/utils/time.ts
+++ b/public/app/features/alerting/unified/utils/time.ts
@@ -99,3 +99,7 @@ export function parsePrometheusDuration(duration: string): number {
return totalDuration;
}
+
+export const isNullDate = (date: string) => {
+ return date.includes('0001-01-01T00');
+};