Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions web/src/components/health/alert-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -25,6 +26,9 @@ export const AlertDetails: React.FC<AlertDetailsProps> = ({ resourceName, alert
</AlertDetailsValue>
<AlertDetailsValue title={t('State')}>{alert.state}</AlertDetailsValue>
<AlertDetailsValue title={t('Severity')}>{alert.labels.severity}</AlertDetailsValue>
{alert.activeAt && (
<AlertDetailsValue title={t('Active since')}>{formatActiveSince(t, alert.activeAt)}</AlertDetailsValue>
)}
<AlertDetailsValue title={t('Labels')}>
{labels.length === 0
? t('None')
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/health/alert-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -39,6 +40,7 @@ export const AlertRow: React.FC<AlertRowProps> = ({ kind, resourceName, alert, w
)}
<Td noPadding={!wide}>{alert.state}</Td>
<Td>{alert.labels.severity}</Td>
{alert.activeAt && <Td>{formatActiveSince(t, alert.activeAt)}</Td>}
<Td>
{labels.length === 0
? t('None')
Expand Down
1 change: 1 addition & 0 deletions web/src/components/health/rule-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const RuleDetails: React.FC<RuleDetailsProps> = ({ kind, info, wide }) =>
<Th>{t('Summary')}</Th>
<Th>{t('State')}</Th>
<Th>{t('Severity')}</Th>
<Th>{t('Active since')}</Th>
<Th>{t('Labels')}</Th>
<Th>{t('Value')}</Th>
<Th>{t('Description')}</Th>
Expand Down
67 changes: 66 additions & 1 deletion web/src/utils/__tests__/datetime.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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}`);
});
});
45 changes: 45 additions & 0 deletions web/src/utils/datetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
};