Skip to content

Commit dd4c9a8

Browse files
committed
feat: fit the alerts timeline to the actual data timespan
Modified alerts timeline to automatically fit to actual alert data timespan instead of using fixed incidents timeline range, improving readability by focusing on relevant alert activity periods. Assisted-by: Claude Code (claude-sonnet, claude-3-5-haiku)
1 parent 2f449d8 commit dd4c9a8

File tree

3 files changed

+81
-21
lines changed

3 files changed

+81
-21
lines changed

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ import { useDispatch, useSelector } from 'react-redux';
2626
import { setAlertsAreLoading } from '../../../actions/observe';
2727
import { MonitoringState } from '../../../reducers/observe';
2828
import { IncidentsTooltip } from '../IncidentsTooltip';
29-
import { createAlertsChartBars, formatDate, generateDateArray } from '../utils';
29+
import {
30+
createAlertsChartBars,
31+
formatDate,
32+
generateDateArray,
33+
generateAlertsDateArray,
34+
} from '../utils';
3035

31-
const AlertsChart = ({ chartDays, theme }: { chartDays: number; theme: 'light' | 'dark' }) => {
36+
const AlertsChart = ({ theme }: { theme: 'light' | 'dark' }) => {
3237
const dispatch = useDispatch();
3338
const [chartContainerHeight, setChartContainerHeight] = useState<number>();
3439
const [chartHeight, setChartHeight] = useState<number>();
@@ -45,12 +50,19 @@ const AlertsChart = ({ chartDays, theme }: { chartDays: number; theme: 'light' |
4550
state.plugins.mcp.getIn(['incidentsData', 'groupId']),
4651
);
4752

48-
const dateValues = useMemo(() => generateDateArray(chartDays), [chartDays]);
53+
// Use dynamic date range based on actual alerts data instead of fixed chartDays
54+
const dateValues = useMemo(() => {
55+
if (!Array.isArray(alertsData) || alertsData.length === 0) {
56+
// Fallback to single day if no alerts data
57+
return generateDateArray(1);
58+
}
59+
return generateAlertsDateArray(alertsData);
60+
}, [alertsData]);
4961

5062
const chartData = useMemo(() => {
5163
if (!Array.isArray(alertsData) || alertsData.length === 0) return [];
52-
return alertsData.map((alert) => createAlertsChartBars(alert, dateValues));
53-
}, [alertsData, dateValues]);
64+
return alertsData.map((alert) => createAlertsChartBars(alert));
65+
}, [alertsData]);
5466

5567
useEffect(() => {
5668
setChartContainerHeight(chartData?.length < 5 ? 300 : chartData?.length * 60);

web/src/components/Incidents/IncidentsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ const IncidentsPage = () => {
515515
/>
516516
</StackItem>
517517
<StackItem>
518-
<AlertsChart chartDays={timeRanges.length} theme={theme} />
518+
<AlertsChart theme={theme} />
519519
</StackItem>
520520
</>
521521
)}

web/src/components/Incidents/utils.ts

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const createIncidentsChartBars = (incident: Incident, dateArray: SpanDate
171171
return data;
172172
};
173173

174-
function consolidateAndMergeAlertIntervals(data: Alert, dateArray: SpanDates) {
174+
function consolidateAndMergeAlertIntervals(data: Alert) {
175175
if (!data.values || data.values.length === 0) {
176176
return [];
177177
}
@@ -195,24 +195,15 @@ function consolidateAndMergeAlertIntervals(data: Alert, dateArray: SpanDates) {
195195

196196
intervals.push([currentStart, sortedValues[sortedValues.length - 1][0], 'data']);
197197

198-
// Handle gaps before and after the detected intervals
199-
const startBoundary = dateArray[0],
200-
endBoundary = dateArray[dateArray.length - 1];
201-
const firstIntervalStart = intervals[0][0],
202-
lastIntervalEnd = intervals[intervals.length - 1][1];
203-
204-
if (firstIntervalStart > startBoundary) {
205-
intervals.unshift([startBoundary, firstIntervalStart - 1, 'nodata']);
206-
}
207-
if (lastIntervalEnd < endBoundary) {
208-
intervals.push([lastIntervalEnd + 1, endBoundary, 'nodata']);
209-
}
198+
// For dynamic alerts timeline, we don't add padding gaps since the dateArray
199+
// is already calculated to fit the alert data with appropriate padding
200+
// This allows the timeline to focus on the actual alert activity period
210201

211202
return intervals;
212203
}
213204

214-
export const createAlertsChartBars = (alert: Alert, dateValues: SpanDates) => {
215-
const groupedData = consolidateAndMergeAlertIntervals(alert, dateValues);
205+
export const createAlertsChartBars = (alert: Alert) => {
206+
const groupedData = consolidateAndMergeAlertIntervals(alert);
216207
const barChartColorScheme = {
217208
critical: t_global_color_status_danger_default.var,
218209
info: t_global_color_status_info_default.var,
@@ -298,6 +289,63 @@ export function generateDateArray(days: number): Array<number> {
298289
return dateArray;
299290
}
300291

292+
/**
293+
* Generates a dynamic date array based on the actual min/max timestamps from alerts data.
294+
* This creates a focused timeline that spans only the relevant alert activity period.
295+
*
296+
* @param {Array<Alert>} alertsData - Array of alert objects containing timestamp values
297+
* @returns {Array<number>} - Array of timestamp values representing the date range with some padding
298+
*
299+
* @example
300+
* const alertsDateRange = generateAlertsDateArray(alertsData);
301+
* // Returns timestamps spanning from earliest alert minus padding to latest alert plus padding
302+
*/
303+
export function generateAlertsDateArray(alertsData: Array<Alert>): Array<number> {
304+
if (!Array.isArray(alertsData) || alertsData.length === 0) {
305+
// Fallback to current day if no alerts data
306+
const now = new Date();
307+
now.setHours(0, 0, 0, 0);
308+
return [now.getTime() / 1000];
309+
}
310+
311+
let minTimestamp = Infinity;
312+
let maxTimestamp = -Infinity;
313+
314+
// Find min and max timestamps across all alerts
315+
alertsData.forEach((alert) => {
316+
if (alert.values && Array.isArray(alert.values)) {
317+
alert.values.forEach(([timestamp]) => {
318+
minTimestamp = Math.min(minTimestamp, timestamp);
319+
maxTimestamp = Math.max(maxTimestamp, timestamp);
320+
});
321+
}
322+
});
323+
324+
// Handle edge case where no valid timestamps found
325+
if (minTimestamp === Infinity || maxTimestamp === -Infinity) {
326+
const now = new Date();
327+
now.setHours(0, 0, 0, 0);
328+
return [now.getTime() / 1000];
329+
}
330+
331+
// Add some padding to make the timeline more readable
332+
// Padding: 10% of the time span, minimum 1 hour, maximum 24 hours
333+
const timeSpan = maxTimestamp - minTimestamp;
334+
const paddingSeconds = Math.max(3600, Math.min(86400, timeSpan * 0.1));
335+
336+
const paddedMin = minTimestamp - paddingSeconds;
337+
const paddedMax = maxTimestamp + paddingSeconds;
338+
339+
// Generate array with exactly 2 timestamps: min (start) and max (end)
340+
const dateArray: Array<number> = [];
341+
342+
// Always show exactly 2 ticks for clean X-axis: start and end only
343+
dateArray.push(paddedMin); // Min date (start)
344+
dateArray.push(paddedMax); // Max date (end)
345+
346+
return dateArray;
347+
}
348+
301349
/**
302350
* Filters incidents based on the specified filters.
303351
*

0 commit comments

Comments
 (0)