Skip to content

Commit 569219d

Browse files
NETOBSERV-2437: Active since field added to alert details (#1129)
* active since field added
1 parent a593dd9 commit 569219d

File tree

6 files changed

+120
-1
lines changed

6 files changed

+120
-1
lines changed

web/locales/en/plugin__netobserv-plugin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@
246246
"Navigate to alert details": "Navigate to alert details",
247247
"State": "State",
248248
"Severity": "Severity",
249+
"Active since": "Active since",
249250
"Labels": "Labels",
250251
"Description": "Description",
251252
"Navigate to network traffic": "Navigate to network traffic",
@@ -515,6 +516,7 @@
515516
"Last 2 days": "Last 2 days",
516517
"Last 1 week": "Last 1 week",
517518
"Last 2 weeks": "Last 2 weeks",
519+
"Yesterday": "Yesterday",
518520
"Value is empty": "Value is empty",
519521
"Value is malformed": "Value is malformed",
520522
"Not a valid Kubernetes name": "Not a valid Kubernetes name",

web/src/components/health/alert-details.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as React from 'react';
33
import { Label, Text, TextContent, TextVariants } from '@patternfly/react-core';
44
import { useTranslation } from 'react-i18next';
55
import { Link } from 'react-router-dom';
6+
import { formatActiveSince } from '../../utils/datetime';
67
import { valueFormat } from '../../utils/format';
78
import { AlertWithRuleName, getAlertFilteredLabels, getAlertLink } from './health-helper';
89

@@ -25,6 +26,9 @@ export const AlertDetails: React.FC<AlertDetailsProps> = ({ resourceName, alert
2526
</AlertDetailsValue>
2627
<AlertDetailsValue title={t('State')}>{alert.state}</AlertDetailsValue>
2728
<AlertDetailsValue title={t('Severity')}>{alert.labels.severity}</AlertDetailsValue>
29+
{alert.activeAt && (
30+
<AlertDetailsValue title={t('Active since')}>{formatActiveSince(t, alert.activeAt)}</AlertDetailsValue>
31+
)}
2832
<AlertDetailsValue title={t('Labels')}>
2933
{labels.length === 0
3034
? t('None')

web/src/components/health/alert-row.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as React from 'react';
33

44
import { Label, Tooltip } from '@patternfly/react-core';
55
import { useTranslation } from 'react-i18next';
6+
import { formatActiveSince } from '../../utils/datetime';
67
import { valueFormat } from '../../utils/format';
78
import { HealthColorSquare } from './health-color-square';
89
import { AlertWithRuleName, getAlertFilteredLabels, getAlertLink, getTrafficLink } from './health-helper';
@@ -39,6 +40,7 @@ export const AlertRow: React.FC<AlertRowProps> = ({ kind, resourceName, alert, w
3940
)}
4041
<Td noPadding={!wide}>{alert.state}</Td>
4142
<Td>{alert.labels.severity}</Td>
43+
{alert.activeAt && <Td>{formatActiveSince(t, alert.activeAt)}</Td>}
4244
<Td>
4345
{labels.length === 0
4446
? t('None')

web/src/components/health/rule-details.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const RuleDetails: React.FC<RuleDetailsProps> = ({ kind, info, wide }) =>
2323
<Th>{t('Summary')}</Th>
2424
<Th>{t('State')}</Th>
2525
<Th>{t('Severity')}</Th>
26+
<Th>{t('Active since')}</Th>
2627
<Th>{t('Labels')}</Th>
2728
<Th>{t('Value')}</Th>
2829
<Th>{t('Description')}</Th>

web/src/utils/__tests__/datetime.spec.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { toISODateString, twentyFourHourTime } from '../datetime';
1+
import { TFunction } from 'react-i18next';
2+
import { formatActiveSince, toISODateString, twentyFourHourTime } from '../datetime';
3+
import { getLanguage } from '../language';
24

35
describe('datetime', () => {
46
it('should toISODateString', () => {
@@ -13,3 +15,66 @@ describe('datetime', () => {
1315
expect(twentyFourHourTime(new Date('1955-11-05T06:15:00'))).toBe('06:15');
1416
});
1517
});
18+
19+
describe('formatActiveSince', () => {
20+
const FIXED_NOW = new Date('2025-11-27T15:00:00');
21+
const tMock: TFunction = ((key: string) => key) as unknown as TFunction;
22+
23+
beforeAll(() => {
24+
jest.useFakeTimers();
25+
jest.setSystemTime(FIXED_NOW);
26+
});
27+
28+
afterAll(() => {
29+
jest.useRealTimers();
30+
});
31+
32+
it('should format "today" as only time', () => {
33+
const ts = '2025-11-27T10:15:00';
34+
const date = new Date(ts);
35+
36+
const expectedTime = twentyFourHourTime(date, false);
37+
38+
const result = formatActiveSince(tMock, ts);
39+
40+
expect(result).toBe(expectedTime);
41+
});
42+
43+
it('should format "yesterday" as "Yesterday, HH:MM"', () => {
44+
const ts = '2025-11-26T22:00:00';
45+
const date = new Date(ts);
46+
47+
const expectedTime = twentyFourHourTime(date, false);
48+
49+
const result = formatActiveSince(tMock, ts);
50+
51+
expect(result).toBe(`Yesterday, ${expectedTime}`);
52+
});
53+
54+
it('should format dates within the last 7 days as "Weekday, HH:MM"', () => {
55+
const ts = '2025-11-25T09:30:00';
56+
const date = new Date(ts);
57+
58+
const weekdayFormatter = new Intl.DateTimeFormat(getLanguage(), {
59+
weekday: 'short'
60+
});
61+
const weekday = weekdayFormatter.format(date);
62+
const time = twentyFourHourTime(date, false);
63+
64+
const result = formatActiveSince(tMock, ts);
65+
66+
expect(result).toBe(`${weekday}, ${time}`);
67+
});
68+
69+
it('should format older dates with "YYYY-MM-DD HH:MM"', () => {
70+
const ts = '2025-11-07T14:00:00';
71+
const date = new Date(ts);
72+
73+
const expectedDate = toISODateString(date);
74+
const expectedTime = twentyFourHourTime(date, false);
75+
76+
const result = formatActiveSince(tMock, ts);
77+
78+
expect(result).toBe(`${expectedDate} ${expectedTime}`);
79+
});
80+
});

web/src/utils/datetime.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,48 @@ export const computeStepInterval = (range: TimeRange | number) => {
123123
stepSeconds: step
124124
};
125125
};
126+
127+
/**
128+
* Formats a timestamp for "Active since" display with relative or absolute time.
129+
* - Today: Shows only time (14:23)
130+
* - Yesterday: Shows "Yesterday, HH:MM"
131+
* - Last 7 days: Shows day of week and time (Tue, 14:23)
132+
* - Older: Shows full date and time (2025-11-24 14:23)
133+
*/
134+
export const formatActiveSince = (t: TFunction, timestamp: string): string => {
135+
const activeDate = new Date(timestamp);
136+
const now = new Date();
137+
138+
// Calculate time difference in milliseconds
139+
const diffMs = now.getTime() - activeDate.getTime();
140+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
141+
142+
// Format time as HH:MM
143+
const timeStr = twentyFourHourTime(activeDate, false);
144+
145+
// Today: show only time
146+
if (diffDays === 0 && now.getDate() === activeDate.getDate()) {
147+
return timeStr;
148+
}
149+
150+
// Yesterday: show "Yesterday, HH:MM"
151+
const yesterday = new Date(now);
152+
yesterday.setDate(yesterday.getDate() - 1);
153+
if (
154+
activeDate.getDate() === yesterday.getDate() &&
155+
activeDate.getMonth() === yesterday.getMonth() &&
156+
activeDate.getFullYear() === yesterday.getFullYear()
157+
) {
158+
return `${t('Yesterday')}, ${timeStr}`;
159+
}
160+
161+
// Last 7 days: show day of week and time
162+
if (diffDays < 7) {
163+
const weekdayFormatter = new Intl.DateTimeFormat(getLanguage(), { weekday: 'short' });
164+
const weekday = weekdayFormatter.format(activeDate);
165+
return `${weekday}, ${timeStr}`;
166+
}
167+
168+
// Older: show full date and time (YYYY-MM-DD HH:MM)
169+
return `${toISODateString(activeDate)} ${timeStr}`;
170+
};

0 commit comments

Comments
 (0)