From fe416540494fcf85b651ead88ff727620abbe5f8 Mon Sep 17 00:00:00 2001 From: rioloc Date: Fri, 30 Jan 2026 15:06:16 +0100 Subject: [PATCH] feat: force rounded dates for consecutive intervals --- .../Incidents/AlertsChart/AlertsChart.tsx | 5 +++- .../IncidentsChart/IncidentsChart.tsx | 5 +++- web/src/components/Incidents/utils.spec.ts | 28 ++++++++++++++++++- web/src/components/Incidents/utils.ts | 20 +++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/web/src/components/Incidents/AlertsChart/AlertsChart.tsx b/web/src/components/Incidents/AlertsChart/AlertsChart.tsx index 43dd87584..1705b2e14 100644 --- a/web/src/components/Incidents/AlertsChart/AlertsChart.tsx +++ b/web/src/components/Incidents/AlertsChart/AlertsChart.tsx @@ -32,6 +32,7 @@ import { generateDateArray, generateAlertsDateArray, getCurrentTime, + roundDateToInterval, } from '../utils'; import { dateTimeFormatter, timeFormatter } from '../../console/utils/datetime'; import { useTranslation } from 'react-i18next'; @@ -164,7 +165,9 @@ const AlertsChart = ({ theme }: { theme: 'light' | 'dark' }) => { const endDate = datum.alertstate === 'firing' ? '---' - : dateTimeFormatter(i18n.language).format(new Date(datum.y)); + : dateTimeFormatter(i18n.language).format( + roundDateToInterval(new Date(datum.y)), + ); const alertName = datum.silenced ? `${datum.name} (silenced)` : datum.name; diff --git a/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx b/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx index b5d27ad1e..51c7a52a8 100644 --- a/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx +++ b/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx @@ -33,6 +33,7 @@ import { calculateIncidentsChartDomain, createIncidentsChartBars, generateDateArray, + roundDateToInterval, } from '../utils'; import { dateTimeFormatter, timeFormatter } from '../../console/utils/datetime'; import { useTranslation } from 'react-i18next'; @@ -178,7 +179,9 @@ const IncidentsChart = ({ const startDate = dateTimeFormatter(i18n.language).format(new Date(datum.y0)); const endDate = datum.firing ? '---' - : dateTimeFormatter(i18n.language).format(new Date(datum.y)); + : dateTimeFormatter(i18n.language).format( + roundDateToInterval(new Date(datum.y)), + ); const components = formatComponentList(datum.componentList); return `${t('Severity')}: ${t(datum.name)} diff --git a/web/src/components/Incidents/utils.spec.ts b/web/src/components/Incidents/utils.spec.ts index 7bef07021..97c55df8f 100644 --- a/web/src/components/Incidents/utils.spec.ts +++ b/web/src/components/Incidents/utils.spec.ts @@ -1,4 +1,4 @@ -import { insertPaddingPointsForChart } from './utils'; +import { insertPaddingPointsForChart, roundDateToInterval } from './utils'; describe('insertPaddingPointsForChart', () => { describe('edge cases', () => { @@ -287,3 +287,29 @@ describe('insertPaddingPointsForChart', () => { }); }); }); + +describe('roundDateToInterval', () => { + describe('exact 5-minute boundaries', () => { + it('should return unchanged date for 23:55:00', () => { + const date = new Date('2026-01-26T23:55:00.000Z'); + const rounded = roundDateToInterval(date); + expect(rounded.getTime()).toBe(date.getTime()); + }); + }); + + describe('rounding to nearest 5-minute boundary', () => { + it('should round 22:57:00 down to 22:55:00', () => { + const date = new Date('2026-01-26T22:57:00.000Z'); + const rounded = roundDateToInterval(date); + const expected = new Date('2026-01-26T22:55:00.000Z'); + expect(rounded.getTime()).toBe(expected.getTime()); + }); + + it('should round 22:59:00 up to 23:00:00', () => { + const date = new Date('2026-01-26T22:59:00.000Z'); + const rounded = roundDateToInterval(date); + const expected = new Date('2026-01-26T23:00:00.000Z'); + expect(rounded.getTime()).toBe(expected.getTime()); + }); + }); +}); diff --git a/web/src/components/Incidents/utils.ts b/web/src/components/Incidents/utils.ts index a778c0ffa..6f60fc9a8 100644 --- a/web/src/components/Incidents/utils.ts +++ b/web/src/components/Incidents/utils.ts @@ -54,6 +54,26 @@ export const getCurrentTime = (): number => { return Math.floor(now / intervalMs) * intervalMs; }; +/** + * Rounds a Date to the nearest 5-minute boundary for display purposes. + * This is used in tooltips to show cleaner, rounded timestamps instead of precise + * interval boundaries that may differ by seconds. + * + * For example: + * - 22:57:00 -> 22:55:00 (rounds down) + * - 22:59:00 -> 23:00:00 (rounds up) + * - 23:30:00 -> 23:30:00 (already at boundary) + * - 23:29:59 -> 23:30:00 (rounds up) + * + * @param date - The Date object to round + * @returns A new Date object rounded to the nearest 5-minute boundary + */ +export const roundDateToInterval = (date: Date): Date => { + const intervalMs = PROMETHEUS_QUERY_INTERVAL_SECONDS * 1000; + const roundedMs = Math.round(date.getTime() / intervalMs) * intervalMs; + return new Date(roundedMs); +}; + /** * Determines if an incident or alert is resolved based on the time elapsed since the last data point. *