Skip to content

Commit bb1ffe8

Browse files
committed
feat: refactor intervals logic
1 parent a5864d2 commit bb1ffe8

File tree

15 files changed

+902
-843
lines changed

15 files changed

+902
-843
lines changed

web/src/components/Incidents/AlertsChart/AlertsChart.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,18 @@ const AlertsChart = ({ theme }: { theme: 'light' | 'dark' }) => {
7373

7474
const chartData: AlertsChartBar[][] = useMemo(() => {
7575
if (!Array.isArray(alertsData) || alertsData.length === 0) return [];
76-
return alertsData.map((alert) => createAlertsChartBars(alert));
76+
77+
// Group alerts by identity so intervals of the same alert share the same row
78+
const groupedByIdentity = new Map<string, typeof alertsData>();
79+
for (const alert of alertsData) {
80+
const key = [alert.alertname, alert.namespace, alert.severity].join('|');
81+
if (!groupedByIdentity.has(key)) {
82+
groupedByIdentity.set(key, []);
83+
}
84+
groupedByIdentity.get(key)!.push(alert);
85+
}
86+
87+
return Array.from(groupedByIdentity.values()).map((alerts) => createAlertsChartBars(alerts));
7788
}, [alertsData]);
7889

7990
useEffect(() => {

web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ import {
2828
} from '@patternfly/react-tokens';
2929
import '../incidents-styles.css';
3030
import { IncidentsTooltip } from '../IncidentsTooltip';
31-
import { Incident, IncidentsTimestamps } from '../model';
31+
import { Incident } from '../model';
3232
import {
3333
calculateIncidentsChartDomain,
3434
createIncidentsChartBars,
3535
generateDateArray,
36-
matchTimestampMetricForIncident,
36+
removeTrailingPaddingFromSeveritySegments,
3737
roundDateToInterval,
3838
} from '../utils';
3939
import { dateTimeFormatter, timeFormatter } from '../../console/utils/datetime';
@@ -58,7 +58,6 @@ const formatComponentList = (componentList: string[] | undefined): string => {
5858

5959
const IncidentsChart = ({
6060
incidentsData,
61-
incidentsTimestamps,
6261
chartDays,
6362
theme,
6463
selectedGroupId,
@@ -67,7 +66,6 @@ const IncidentsChart = ({
6766
lastRefreshTime,
6867
}: {
6968
incidentsData: Array<Incident>;
70-
incidentsTimestamps: IncidentsTimestamps;
7169
chartDays: number;
7270
theme: 'light' | 'dark';
7371
selectedGroupId: string;
@@ -83,43 +81,45 @@ const IncidentsChart = ({
8381
[chartDays, currentTime],
8482
);
8583

86-
const enrichedIncidentsData = useMemo(() => {
87-
return incidentsData.map((incident) => {
88-
// find the matched timestamp for the incident
89-
const matchedMinTimestamp = matchTimestampMetricForIncident(
90-
incident,
91-
incidentsTimestamps.minOverTime,
92-
);
93-
94-
return {
95-
...incident,
96-
firstTimestamp: parseInt(matchedMinTimestamp?.value?.[1] ?? '0'),
97-
};
98-
});
99-
}, [incidentsData, incidentsTimestamps]);
100-
10184
const { t, i18n } = useTranslation(process.env.I18N_NAMESPACE);
10285

10386
const chartData = useMemo(() => {
104-
if (!Array.isArray(enrichedIncidentsData) || enrichedIncidentsData.length === 0) return [];
87+
if (!Array.isArray(incidentsData) || incidentsData.length === 0) return [];
10588

10689
const filteredIncidents = selectedGroupId
107-
? enrichedIncidentsData.filter((incident) => incident.group_id === selectedGroupId)
108-
: enrichedIncidentsData;
90+
? incidentsData.filter((incident) => incident.group_id === selectedGroupId)
91+
: incidentsData;
10992

110-
// Create chart bars and sort by original x values to maintain proper order
111-
const chartBars = filteredIncidents.map((incident) =>
112-
createIncidentsChartBars(incident, dateValues),
93+
// Group incidents by group_id so split severity segments share the same row
94+
const incidentsByGroupId = new Map<string, typeof filteredIncidents>();
95+
for (const incident of filteredIncidents) {
96+
const existing = incidentsByGroupId.get(incident.group_id);
97+
if (existing) {
98+
existing.push(incident);
99+
} else {
100+
incidentsByGroupId.set(incident.group_id, [incident]);
101+
}
102+
}
103+
104+
// When an incident changes severity, its segments share the same row.
105+
// Non-last segments have trailing padding (+300s) that overlaps with the
106+
// next segment's leading padding (-300s). Remove the trailing padding
107+
// value from non-last segments to prevent visual overlap.
108+
const adjustedGroups = Array.from(incidentsByGroupId.values()).map((group) =>
109+
removeTrailingPaddingFromSeveritySegments(group),
113110
);
111+
112+
// Create chart bars per group and sort by original x values
113+
const chartBars = adjustedGroups.map((group) => createIncidentsChartBars(group, dateValues));
114114
chartBars.sort((a, b) => a[0].x - b[0].x);
115115

116116
// Reassign consecutive x values to eliminate gaps between bars
117117
return chartBars.map((bars, index) => bars.map((bar) => ({ ...bar, x: index + 1 })));
118-
}, [enrichedIncidentsData, dateValues, selectedGroupId]);
118+
}, [incidentsData, dateValues, selectedGroupId]);
119119

120120
useEffect(() => {
121121
setIsLoading(false);
122-
}, [enrichedIncidentsData]);
122+
}, [incidentsData]);
123123
useEffect(() => {
124124
setChartContainerHeight(chartData?.length < 5 ? 300 : chartData?.length * 60);
125125
setChartHeight(chartData?.length < 5 ? 250 : chartData?.length * 55);

web/src/components/Incidents/IncidentsDetailsRowTable.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ const IncidentsDetailsRowTable = ({ alerts }: IncidentsDetailsRowTableProps) =>
2525
if (alerts && alerts.length > 0) {
2626
return [...alerts]
2727
.sort((a: IncidentsDetailsAlert, b: IncidentsDetailsAlert) => {
28-
const aFirstTimestamp = a.firstTimestamps[0][1];
29-
const bFirstTimestamp = b.firstTimestamps[0][1];
30-
const aStart = aFirstTimestamp > 0 ? aFirstTimestamp : a.alertsStartFiring;
31-
const bStart = bFirstTimestamp > 0 ? bFirstTimestamp : b.alertsStartFiring;
28+
const aStart = a.firstTimestamp > 0 ? a.firstTimestamp : a.alertsStartFiring;
29+
const bStart = b.firstTimestamp > 0 ? b.firstTimestamp : b.alertsStartFiring;
3230
return aStart - bStart;
3331
})
3432
.map((alertDetails: IncidentsDetailsAlert, rowIndex) => {
@@ -50,8 +48,8 @@ const IncidentsDetailsRowTable = ({ alerts }: IncidentsDetailsRowTableProps) =>
5048
<Td dataLabel="expanded-details-firingstart">
5149
<Timestamp
5250
timestamp={
53-
(alertDetails.firstTimestamps[0][1] > 0
54-
? alertDetails.firstTimestamps[0][1]
51+
(alertDetails.firstTimestamp > 0
52+
? alertDetails.firstTimestamp
5553
: alertDetails.alertsStartFiring) * 1000
5654
}
5755
/>

web/src/components/Incidents/IncidentsPage.tsx

Lines changed: 20 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable react-hooks/exhaustive-deps */
22
import { useMemo, useState, useEffect, useCallback } from 'react';
33
import { useSafeFetch } from '../console/utils/safe-fetch-hook';
4-
import { createAlertsQuery, fetchDataForIncidentsAndAlerts, fetchInstantData } from './api';
4+
import { createAlertsQuery, fetchDataForIncidentsAndAlerts } from './api';
55
import { useTranslation } from 'react-i18next';
66
import {
77
Bullseye,
@@ -37,9 +37,8 @@ import {
3737
onDeleteIncidentFilterChip,
3838
onIncidentFiltersSelect,
3939
parseUrlParams,
40-
PROMETHEUS_QUERY_INTERVAL_SECONDS,
41-
roundTimestampToFiveMinutes,
4240
updateBrowserUrl,
41+
DAY_MS,
4342
} from './utils';
4443
import { groupAlertsForTable, convertToAlerts } from './processAlerts';
4544
import { CompressArrowsAltIcon, CompressIcon, FilterIcon } from '@patternfly/react-icons';
@@ -48,9 +47,7 @@ import {
4847
setAlertsAreLoading,
4948
setAlertsData,
5049
setAlertsTableData,
51-
setAlertsTimestamps,
5250
setFilteredIncidentsData,
53-
setIncidentsTimestamps,
5451
setIncidentPageFilterType,
5552
setIncidents,
5653
setIncidentsActiveFilters,
@@ -150,10 +147,6 @@ const IncidentsPage = () => {
150147
(state: MonitoringState) => state.plugins.mcp.incidentsData.filteredIncidentsData,
151148
);
152149

153-
const incidentsTimestamps = useSelector(
154-
(state: MonitoringState) => state.plugins.mcp.incidentsData.incidentsTimestamps,
155-
);
156-
157150
const selectedGroupId = incidentsActiveFilters.groupId?.[0] ?? undefined;
158151

159152
const incidentPageFilterTypeSelected = useSelector(
@@ -243,66 +236,27 @@ const IncidentsPage = () => {
243236
}
244237

245238
const currentTime = incidentsLastRefreshTime;
246-
const ONE_DAY = 24 * 60 * 60 * 1000;
247239

248-
// Fetch timestamps and alerts in parallel, but wait for both before processing
249-
const timestampPromise = fetchDataForIncidentsAndAlerts(
250-
safeFetch,
251-
{ endTime: currentTime, duration: 15 * ONE_DAY },
252-
'timestamp(ALERTS{alertstate="firing"})',
253-
).then((res) => res.data.result);
240+
// Always fetch 15 days of alert data so firstTimestamp is computed from full history
241+
const fetchTimeRanges = getIncidentsTimeRanges(15 * DAY_MS, currentTime);
254242

255-
const alertsPromise = Promise.all(
256-
timeRanges.map(async (range) => {
243+
Promise.all(
244+
fetchTimeRanges.map(async (range) => {
257245
const response = await fetchDataForIncidentsAndAlerts(
258246
safeFetch,
259247
range,
260248
createAlertsQuery(incidentForAlertProcessing),
261249
);
262250
return response.data.result;
263251
}),
264-
);
265-
266-
Promise.all([timestampPromise, alertsPromise])
267-
.then(([timestampsResults, alertsResults]) => {
268-
// Gaps detection here such that if the same timestamp has
269-
// gaps greater than 5 minutes, this will be added more than one time.
270-
// For example, if there is a metric for AlertH_Gapped
271-
// with values:[ "1770699000", "1770699300", "1770708300", "1770708600", "1770708900"]
272-
// there will be two gaps detected. With the following min values: 1770699300 and 1770699300
273-
// the interval will be [1770699300 - 1770699300] and [1770708300 - 1770699300]
274-
275-
const timestampsValues = timestampsResults?.map((result: any) => ({
276-
...result,
277-
value: detectMinForEachGap(result.values, PROMETHEUS_QUERY_INTERVAL_SECONDS),
278-
}));
279-
280-
// Round timestamp values before storing
281-
const roundedTimestamps =
282-
timestampsValues?.map((result: any) => ({
283-
...result,
284-
value: result.value.map((value: any) => [
285-
value[0],
286-
roundTimestampToFiveMinutes(parseInt(value[1])).toString(),
287-
]),
288-
})) || [];
289-
290-
const fetchedAlertsTimestamps = {
291-
minOverTime: roundedTimestamps,
292-
lastOverTime: [],
293-
};
294-
dispatch(
295-
setAlertsTimestamps({
296-
alertsTimestamps: fetchedAlertsTimestamps,
297-
}),
298-
);
299-
252+
)
253+
.then((alertsResults) => {
300254
const prometheusResults = alertsResults.flat();
301255
const alerts = convertToAlerts(
302256
prometheusResults,
303257
incidentForAlertProcessing,
304258
currentTime,
305-
fetchedAlertsTimestamps,
259+
daysSpan,
306260
);
307261
dispatch(
308262
setAlertsData({
@@ -342,68 +296,41 @@ const IncidentsPage = () => {
342296
? incidentsActiveFilters.days[0].split(' ')[0] + 'd'
343297
: '',
344298
);
345-
const calculatedTimeRanges = getIncidentsTimeRanges(daysDuration, currentTime);
346299

347300
const isGroupSelected = !!selectedGroupId;
348301
const incidentsQuery = isGroupSelected
349302
? `cluster_health_components_map{group_id='${selectedGroupId}'}`
350303
: 'cluster_health_components_map';
351304

352-
// Fetch timestamps and incidents in parallel, but wait for both before processing
353-
const timestampPromise = fetchInstantData(
354-
safeFetch,
355-
'min_over_time(timestamp(cluster_health_components_map)[15d:5m])',
356-
).then((res) => res.data.result);
305+
// Always fetch 15 days of data so firstTimestamp is computed from full history
306+
const fetchTimeRanges = getIncidentsTimeRanges(15 * DAY_MS, currentTime);
357307

358-
const incidentsPromise = Promise.all(
359-
calculatedTimeRanges.map(async (range) => {
308+
Promise.all(
309+
fetchTimeRanges.map(async (range) => {
360310
const response = await fetchDataForIncidentsAndAlerts(safeFetch, range, incidentsQuery);
361311
return response.data.result;
362312
}),
363-
);
364-
365-
Promise.all([timestampPromise, incidentsPromise])
366-
.then(([timestampsResults, incidentsResults]) => {
367-
// Round timestamp values before storing
368-
const roundedTimestamps =
369-
timestampsResults?.map((result: any) => ({
370-
...result,
371-
value: [
372-
result.value[0],
373-
roundTimestampToFiveMinutes(parseInt(result.value[1])).toString(),
374-
],
375-
})) || [];
376-
377-
const fetchedTimestamps = {
378-
minOverTime: roundedTimestamps,
379-
lastOverTime: [],
380-
};
381-
dispatch(
382-
setIncidentsTimestamps({
383-
incidentsTimestamps: fetchedTimestamps,
384-
}),
385-
);
386-
313+
)
314+
.then((incidentsResults) => {
387315
const prometheusResults = incidentsResults.flat();
388-
const incidents = convertToIncidents(prometheusResults, currentTime);
316+
const incidents = convertToIncidents(prometheusResults, currentTime, daysDuration);
389317

390318
// Update the raw, unfiltered incidents state
391319
dispatch(setIncidents({ incidents }));
392320

321+
const filteredData = filterIncident(incidentsActiveFilters, incidents);
322+
393323
// Filter the incidents and dispatch
394324
dispatch(
395325
setFilteredIncidentsData({
396-
filteredIncidentsData: filterIncident(incidentsActiveFilters, incidents),
326+
filteredIncidentsData: filteredData,
397327
}),
398328
);
399329

400330
setIncidentsAreLoading(false);
401331

402332
if (isGroupSelected) {
403-
// Use fetchedTimestamps directly instead of stale closure value
404-
setIncidentForAlertProcessing(
405-
processIncidentsForAlerts(prometheusResults, fetchedTimestamps),
406-
);
333+
setIncidentForAlertProcessing(processIncidentsForAlerts(prometheusResults));
407334
dispatch(setAlertsAreLoading({ alertsAreLoading: true }));
408335
} else {
409336
closeDropDownFilters();
@@ -724,7 +651,6 @@ const IncidentsPage = () => {
724651
<StackItem>
725652
<IncidentsChart
726653
incidentsData={filteredData}
727-
incidentsTimestamps={incidentsTimestamps}
728654
chartDays={timeRanges.length}
729655
theme={theme}
730656
selectedGroupId={selectedGroupId}
@@ -759,30 +685,3 @@ export const McpCmoAlertingPage = () => {
759685
</MonitoringProvider>
760686
);
761687
};
762-
763-
/**
764-
* @param {Array<Array>} dataValues - The matrix from out.json (data.result[0].values)
765-
* @param {number} gapThreshold - e.g., 300
766-
*/
767-
const detectMinForEachGap = (dataValues, gapThreshold) => {
768-
if (!dataValues || dataValues.length === 0) return [];
769-
770-
const mins = [];
771-
let currentMin = dataValues[0];
772-
773-
// Start from the second element to compare with the previous one
774-
for (let i = 1; i < dataValues.length; i++) {
775-
const delta = dataValues[i][1] - dataValues[i - 1][1];
776-
777-
if (delta > gapThreshold) {
778-
// Gap detected: save the min of the interval that just ended
779-
mins.push(currentMin);
780-
// The current timestamp is the min of the NEW interval
781-
currentMin = dataValues[i];
782-
}
783-
}
784-
785-
// Always push the min of the last interval
786-
mins.push(currentMin);
787-
return mins;
788-
};

web/src/components/Incidents/IncidentsTable.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,7 @@ export const IncidentsTable = () => {
9595
if (!alert.alertsExpandedRowData || alert.alertsExpandedRowData.length === 0) {
9696
return 0;
9797
}
98-
return Math.min(
99-
...alert.alertsExpandedRowData.map((alertData) => alertData.firstTimestamps[0][1]),
100-
);
98+
return Math.min(...alert.alertsExpandedRowData.map((alertData) => alertData.firstTimestamp));
10199
};
102100

103101
if (isEmpty(alertsTableData) || alertsAreLoading || isEmpty(incidentsActiveFilters.groupId)) {

0 commit comments

Comments
 (0)