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'); +};