Skip to content

Commit ceb6fcd

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

File tree

11 files changed

+672
-330
lines changed

11 files changed

+672
-330
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: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@ 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,
3736
roundDateToInterval,
3837
} from '../utils';
3938
import { dateTimeFormatter, timeFormatter } from '../../console/utils/datetime';
@@ -58,7 +57,6 @@ const formatComponentList = (componentList: string[] | undefined): string => {
5857

5958
const IncidentsChart = ({
6059
incidentsData,
61-
incidentsTimestamps,
6260
chartDays,
6361
theme,
6462
selectedGroupId,
@@ -67,7 +65,6 @@ const IncidentsChart = ({
6765
lastRefreshTime,
6866
}: {
6967
incidentsData: Array<Incident>;
70-
incidentsTimestamps: IncidentsTimestamps;
7168
chartDays: number;
7269
theme: 'light' | 'dark';
7370
selectedGroupId: string;
@@ -83,43 +80,39 @@ const IncidentsChart = ({
8380
[chartDays, currentTime],
8481
);
8582

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-
10183
const { t, i18n } = useTranslation(process.env.I18N_NAMESPACE);
10284

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

10688
const filteredIncidents = selectedGroupId
107-
? enrichedIncidentsData.filter((incident) => incident.group_id === selectedGroupId)
108-
: enrichedIncidentsData;
89+
? incidentsData.filter((incident) => incident.group_id === selectedGroupId)
90+
: incidentsData;
91+
92+
// Group incidents by group_id so split severity segments share the same row
93+
const incidentsByGroupId = new Map<string, typeof filteredIncidents>();
94+
for (const incident of filteredIncidents) {
95+
const existing = incidentsByGroupId.get(incident.group_id);
96+
if (existing) {
97+
existing.push(incident);
98+
} else {
99+
incidentsByGroupId.set(incident.group_id, [incident]);
100+
}
101+
}
109102

110-
// Create chart bars and sort by original x values to maintain proper order
111-
const chartBars = filteredIncidents.map((incident) =>
112-
createIncidentsChartBars(incident, dateValues),
103+
// Create chart bars per group and sort by original x values
104+
const chartBars = Array.from(incidentsByGroupId.values()).map((group) =>
105+
createIncidentsChartBars(group, dateValues),
113106
);
114107
chartBars.sort((a, b) => a[0].x - b[0].x);
115108

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

120113
useEffect(() => {
121114
setIsLoading(false);
122-
}, [enrichedIncidentsData]);
115+
}, [incidentsData]);
123116
useEffect(() => {
124117
setChartContainerHeight(chartData?.length < 5 ? 300 : chartData?.length * 60);
125118
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 & 67 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,9 @@ import {
3737
onDeleteIncidentFilterChip,
3838
onIncidentFiltersSelect,
3939
parseUrlParams,
40-
PROMETHEUS_QUERY_INTERVAL_SECONDS,
4140
roundTimestampToFiveMinutes,
4241
updateBrowserUrl,
42+
DAY_MS,
4343
} from './utils';
4444
import { groupAlertsForTable, convertToAlerts } from './processAlerts';
4545
import { CompressArrowsAltIcon, CompressIcon, FilterIcon } from '@patternfly/react-icons';
@@ -243,12 +243,11 @@ const IncidentsPage = () => {
243243
}
244244

245245
const currentTime = incidentsLastRefreshTime;
246-
const ONE_DAY = 24 * 60 * 60 * 1000;
247246

248247
// Fetch timestamps and alerts in parallel, but wait for both before processing
249248
const timestampPromise = fetchDataForIncidentsAndAlerts(
250249
safeFetch,
251-
{ endTime: currentTime, duration: 15 * ONE_DAY },
250+
{ endTime: currentTime, duration: 15 * DAY_MS },
252251
'timestamp(ALERTS{alertstate="firing"})',
253252
).then((res) => res.data.result);
254253

@@ -265,35 +264,18 @@ const IncidentsPage = () => {
265264

266265
Promise.all([timestampPromise, alertsPromise])
267266
.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-
280267
// Round timestamp values before storing
281268
const roundedTimestamps =
282-
timestampsValues?.map((result: any) => ({
269+
timestampsResults?.map((result: any) => ({
283270
...result,
284-
value: result.value.map((value: any) => [
285-
value[0],
286-
roundTimestampToFiveMinutes(parseInt(value[1])).toString(),
271+
values: result.values.map((value: any) => [
272+
roundTimestampToFiveMinutes(parseInt(value[1])),
287273
]),
288274
})) || [];
289275

290-
const fetchedAlertsTimestamps = {
291-
minOverTime: roundedTimestamps,
292-
lastOverTime: [],
293-
};
294276
dispatch(
295277
setAlertsTimestamps({
296-
alertsTimestamps: fetchedAlertsTimestamps,
278+
alertsTimestamps: roundedTimestamps,
297279
}),
298280
);
299281

@@ -302,7 +284,7 @@ const IncidentsPage = () => {
302284
prometheusResults,
303285
incidentForAlertProcessing,
304286
currentTime,
305-
fetchedAlertsTimestamps,
287+
roundedTimestamps,
306288
);
307289
dispatch(
308290
setAlertsData({
@@ -350,9 +332,10 @@ const IncidentsPage = () => {
350332
: 'cluster_health_components_map';
351333

352334
// Fetch timestamps and incidents in parallel, but wait for both before processing
353-
const timestampPromise = fetchInstantData(
335+
const timestampPromise = fetchDataForIncidentsAndAlerts(
354336
safeFetch,
355-
'min_over_time(timestamp(cluster_health_components_map)[15d:5m])',
337+
{ endTime: currentTime, duration: 15 * DAY_MS },
338+
'timestamp(cluster_health_components_map)',
356339
).then((res) => res.data.result);
357340

358341
const incidentsPromise = Promise.all(
@@ -368,32 +351,29 @@ const IncidentsPage = () => {
368351
const roundedTimestamps =
369352
timestampsResults?.map((result: any) => ({
370353
...result,
371-
value: [
372-
result.value[0],
373-
roundTimestampToFiveMinutes(parseInt(result.value[1])).toString(),
374-
],
354+
values: result.values.map((value: any) => [
355+
roundTimestampToFiveMinutes(parseInt(value[1])),
356+
]),
375357
})) || [];
376358

377-
const fetchedTimestamps = {
378-
minOverTime: roundedTimestamps,
379-
lastOverTime: [],
380-
};
381359
dispatch(
382360
setIncidentsTimestamps({
383-
incidentsTimestamps: fetchedTimestamps,
361+
incidentsTimestamps: roundedTimestamps,
384362
}),
385363
);
386364

387365
const prometheusResults = incidentsResults.flat();
388-
const incidents = convertToIncidents(prometheusResults, currentTime);
366+
const incidents = convertToIncidents(prometheusResults, currentTime, roundedTimestamps);
389367

390368
// Update the raw, unfiltered incidents state
391369
dispatch(setIncidents({ incidents }));
392370

371+
const filteredData = filterIncident(incidentsActiveFilters, incidents);
372+
393373
// Filter the incidents and dispatch
394374
dispatch(
395375
setFilteredIncidentsData({
396-
filteredIncidentsData: filterIncident(incidentsActiveFilters, incidents),
376+
filteredIncidentsData: filteredData,
397377
}),
398378
);
399379

@@ -402,7 +382,7 @@ const IncidentsPage = () => {
402382
if (isGroupSelected) {
403383
// Use fetchedTimestamps directly instead of stale closure value
404384
setIncidentForAlertProcessing(
405-
processIncidentsForAlerts(prometheusResults, fetchedTimestamps),
385+
processIncidentsForAlerts(prometheusResults, roundedTimestamps),
406386
);
407387
dispatch(setAlertsAreLoading({ alertsAreLoading: true }));
408388
} else {
@@ -759,30 +739,3 @@ export const McpCmoAlertingPage = () => {
759739
</MonitoringProvider>
760740
);
761741
};
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)) {

web/src/components/Incidents/model.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export type Timestamps = [number, string];
44

55
export type SpanDates = Array<number>;
66

7-
export type AlertsIntervalsArray = [number, number, 'data' | 'nodata'];
7+
export type AlertsIntervalsArray = [number, number, 'data' | 'nodata', number?];
88

99
export type Incident = {
1010
component: string;
@@ -15,23 +15,12 @@ export type Incident = {
1515
src_severity: string;
1616
src_alertname: string;
1717
src_namespace: string;
18+
severity: any;
1819
silenced: boolean;
1920
x: number;
2021
values: Array<Timestamps>;
2122
metric: Metric;
2223
firstTimestamp: number;
23-
firstTimestamps: Array<Array<any>>;
24-
lastTimestamp: number;
25-
};
26-
27-
export type IncidentsTimestamps = {
28-
minOverTime: Array<any>;
29-
lastOverTime: Array<any>;
30-
};
31-
32-
export type AlertsTimestamps = {
33-
minOverTime: Array<any>;
34-
lastOverTime: Array<any>;
3524
};
3625

3726
// Define the interface for Metric
@@ -60,7 +49,7 @@ export type Alert = {
6049
severity: Severity;
6150
silenced: boolean;
6251
x: number;
63-
firstTimestamps: Array<Array<any>>;
52+
firstTimestamp: number;
6453
values: Array<Timestamps>;
6554
alertsExpandedRowData?: Array<Alert>;
6655
};
@@ -115,8 +104,7 @@ export type IncidentsDetailsAlert = {
115104
resolved: boolean;
116105
severity: Severity;
117106
x: number;
118-
firstTimestamps: Array<Array<any>>;
119-
lastTimestamp: number;
107+
firstTimestamp: number;
120108
values: Array<Timestamps>;
121109
silenced: boolean;
122110
rule: {

0 commit comments

Comments
 (0)