diff --git a/web/locales/en/plugin__netobserv-plugin.json b/web/locales/en/plugin__netobserv-plugin.json index afd3fe408..268e86918 100644 --- a/web/locales/en/plugin__netobserv-plugin.json +++ b/web/locales/en/plugin__netobserv-plugin.json @@ -246,6 +246,7 @@ "Navigate to alert details": "Navigate to alert details", "State": "State", "Severity": "Severity", + "Active since": "Active since", "Labels": "Labels", "Description": "Description", "Navigate to network traffic": "Navigate to network traffic", @@ -515,6 +516,7 @@ "Last 2 days": "Last 2 days", "Last 1 week": "Last 1 week", "Last 2 weeks": "Last 2 weeks", + "Yesterday": "Yesterday", "Value is empty": "Value is empty", "Value is malformed": "Value is malformed", "Not a valid Kubernetes name": "Not a valid Kubernetes name", diff --git a/web/src/components/health/alert-details.tsx b/web/src/components/health/alert-details.tsx index bb083145f..5d94cc242 100644 --- a/web/src/components/health/alert-details.tsx +++ b/web/src/components/health/alert-details.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { Label, Text, TextContent, TextVariants } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import { formatActiveSince } from '../../utils/datetime'; import { valueFormat } from '../../utils/format'; import { AlertWithRuleName, getAlertFilteredLabels, getAlertLink } from './health-helper'; @@ -25,6 +26,9 @@ export const AlertDetails: React.FC = ({ resourceName, alert {alert.state} {alert.labels.severity} + {alert.activeAt && ( + {formatActiveSince(t, alert.activeAt)} + )} {labels.length === 0 ? t('None') diff --git a/web/src/components/health/alert-row.tsx b/web/src/components/health/alert-row.tsx index 51c6ef9cf..bfbbccee2 100644 --- a/web/src/components/health/alert-row.tsx +++ b/web/src/components/health/alert-row.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { Label, Tooltip } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; +import { formatActiveSince } from '../../utils/datetime'; import { valueFormat } from '../../utils/format'; import { HealthColorSquare } from './health-color-square'; import { AlertWithRuleName, getAlertFilteredLabels, getAlertLink, getTrafficLink } from './health-helper'; @@ -39,6 +40,7 @@ export const AlertRow: React.FC = ({ kind, resourceName, alert, w )} {alert.state} {alert.labels.severity} + {alert.activeAt && {formatActiveSince(t, alert.activeAt)}} {labels.length === 0 ? t('None') diff --git a/web/src/components/health/rule-details.tsx b/web/src/components/health/rule-details.tsx index 9209e0706..c30232ffb 100644 --- a/web/src/components/health/rule-details.tsx +++ b/web/src/components/health/rule-details.tsx @@ -23,6 +23,7 @@ export const RuleDetails: React.FC = ({ kind, info, wide }) => {t('Summary')} {t('State')} {t('Severity')} + {t('Active since')} {t('Labels')} {t('Value')} {t('Description')} diff --git a/web/src/utils/__tests__/datetime.spec.ts b/web/src/utils/__tests__/datetime.spec.ts index 469b2c429..8fd480dc1 100644 --- a/web/src/utils/__tests__/datetime.spec.ts +++ b/web/src/utils/__tests__/datetime.spec.ts @@ -1,4 +1,6 @@ -import { toISODateString, twentyFourHourTime } from '../datetime'; +import { TFunction } from 'react-i18next'; +import { formatActiveSince, toISODateString, twentyFourHourTime } from '../datetime'; +import { getLanguage } from '../language'; describe('datetime', () => { it('should toISODateString', () => { @@ -13,3 +15,66 @@ describe('datetime', () => { expect(twentyFourHourTime(new Date('1955-11-05T06:15:00'))).toBe('06:15'); }); }); + +describe('formatActiveSince', () => { + const FIXED_NOW = new Date('2025-11-27T15:00:00'); + const tMock: TFunction = ((key: string) => key) as unknown as TFunction; + + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(FIXED_NOW); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should format "today" as only time', () => { + const ts = '2025-11-27T10:15:00'; + const date = new Date(ts); + + const expectedTime = twentyFourHourTime(date, false); + + const result = formatActiveSince(tMock, ts); + + expect(result).toBe(expectedTime); + }); + + it('should format "yesterday" as "Yesterday, HH:MM"', () => { + const ts = '2025-11-26T22:00:00'; + const date = new Date(ts); + + const expectedTime = twentyFourHourTime(date, false); + + const result = formatActiveSince(tMock, ts); + + expect(result).toBe(`Yesterday, ${expectedTime}`); + }); + + it('should format dates within the last 7 days as "Weekday, HH:MM"', () => { + const ts = '2025-11-25T09:30:00'; + const date = new Date(ts); + + const weekdayFormatter = new Intl.DateTimeFormat(getLanguage(), { + weekday: 'short' + }); + const weekday = weekdayFormatter.format(date); + const time = twentyFourHourTime(date, false); + + const result = formatActiveSince(tMock, ts); + + expect(result).toBe(`${weekday}, ${time}`); + }); + + it('should format older dates with "YYYY-MM-DD HH:MM"', () => { + const ts = '2025-11-07T14:00:00'; + const date = new Date(ts); + + const expectedDate = toISODateString(date); + const expectedTime = twentyFourHourTime(date, false); + + const result = formatActiveSince(tMock, ts); + + expect(result).toBe(`${expectedDate} ${expectedTime}`); + }); +}); diff --git a/web/src/utils/datetime.ts b/web/src/utils/datetime.ts index 48599e35d..c66c92157 100644 --- a/web/src/utils/datetime.ts +++ b/web/src/utils/datetime.ts @@ -123,3 +123,48 @@ export const computeStepInterval = (range: TimeRange | number) => { stepSeconds: step }; }; + +/** + * Formats a timestamp for "Active since" display with relative or absolute time. + * - Today: Shows only time (14:23) + * - Yesterday: Shows "Yesterday, HH:MM" + * - Last 7 days: Shows day of week and time (Tue, 14:23) + * - Older: Shows full date and time (2025-11-24 14:23) + */ +export const formatActiveSince = (t: TFunction, timestamp: string): string => { + const activeDate = new Date(timestamp); + const now = new Date(); + + // Calculate time difference in milliseconds + const diffMs = now.getTime() - activeDate.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + // Format time as HH:MM + const timeStr = twentyFourHourTime(activeDate, false); + + // Today: show only time + if (diffDays === 0 && now.getDate() === activeDate.getDate()) { + return timeStr; + } + + // Yesterday: show "Yesterday, HH:MM" + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + if ( + activeDate.getDate() === yesterday.getDate() && + activeDate.getMonth() === yesterday.getMonth() && + activeDate.getFullYear() === yesterday.getFullYear() + ) { + return `${t('Yesterday')}, ${timeStr}`; + } + + // Last 7 days: show day of week and time + if (diffDays < 7) { + const weekdayFormatter = new Intl.DateTimeFormat(getLanguage(), { weekday: 'short' }); + const weekday = weekdayFormatter.format(activeDate); + return `${weekday}, ${timeStr}`; + } + + // Older: show full date and time (YYYY-MM-DD HH:MM) + return `${toISODateString(activeDate)} ${timeStr}`; +};