diff --git a/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx b/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx
index 6612d5fa5..ace321f56 100644
--- a/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx
+++ b/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx
@@ -270,6 +270,16 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) {
) : (
"No environments involved"
)}
+ {incident.rule_fingerprint !== "none" && (
+ <>
+ Grouped by
+
diff --git a/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx b/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx
index 525387b19..d0d1304ec 100644
--- a/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx
+++ b/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx
@@ -8,7 +8,13 @@ import { useIncidentAlerts } from "@/utils/hooks/useIncidents";
import { Card } from "@tremor/react";
import AlertSeverity from "@/app/(keep)/alerts/alert-severity";
import { AlertDto } from "@/app/(keep)/alerts/models";
-import { format, parseISO } from "date-fns";
+import {
+ format,
+ parseISO,
+ differenceInMinutes,
+ differenceInHours,
+ differenceInDays,
+} from "date-fns";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React, { useEffect, useMemo, useState } from "react";
@@ -52,7 +58,7 @@ const AlertEventInfo: React.FC<{ event: AuditEvent; alert: AlertDto }> = ({
alert,
}) => {
return (
-
+
{alert.name} ({alert.fingerprint})
@@ -255,6 +261,32 @@ const IncidentTimelineNoAlerts: React.FC = () => {
);
};
+const SeverityLegend: React.FC<{ alerts: AlertDto[] }> = ({ alerts }) => {
+ const severityCounts = alerts.reduce(
+ (acc, alert) => {
+ acc[alert.severity!] = (acc[alert.severity!] || 0) + 1;
+ return acc;
+ },
+ {} as Record
+ );
+
+ return (
+
+ {Object.entries(severityCounts).map(([severity, count]) => (
+
+
+
{severity}
+
({count})
+
+ ))}
+
+ );
+};
+
export default function IncidentTimeline({
incident,
}: {
@@ -303,32 +335,38 @@ export default function IncidentTimeline({
const pixelsPerMillisecond = 5000 / totalDuration; // Assuming 5000px minimum width
let timeScale: "seconds" | "minutes" | "hours" | "days";
- let intervalDuration: number;
+ let intervalCount = 12; // Target number of intervals
let formatString: string;
- if (totalDuration > 3 * 24 * 60 * 60 * 1000) {
+ // Determine scale and format based on total duration
+ const durationInDays = differenceInDays(paddedEndTime, startTime);
+ const durationInHours = differenceInHours(paddedEndTime, startTime);
+ const durationInMinutes = differenceInMinutes(paddedEndTime, startTime);
+
+ if (durationInDays > 3) {
timeScale = "days";
- intervalDuration = 24 * 60 * 60 * 1000;
formatString = "MMM dd";
- } else if (totalDuration > 24 * 60 * 60 * 1000) {
+ intervalCount = Math.min(durationInDays + 1, 12);
+ } else if (durationInHours > 24) {
timeScale = "hours";
- intervalDuration = 60 * 60 * 1000;
- formatString = "HH:mm";
- } else if (totalDuration > 60 * 60 * 1000) {
+ formatString = "MMM dd HH:mm";
+ intervalCount = Math.min(Math.ceil(durationInHours / 2), 12);
+ } else if (durationInMinutes > 60) {
timeScale = "minutes";
- intervalDuration = 5 * 60 * 1000; // 5-minute intervals
formatString = "HH:mm";
+ intervalCount = Math.min(Math.ceil(durationInMinutes / 5), 12);
} else {
timeScale = "seconds";
- intervalDuration = 10 * 1000; // 10-second intervals
formatString = "HH:mm:ss";
+ intervalCount = 12;
}
+ // Calculate interval duration based on total time and desired interval count
+ const intervalDuration = totalDuration / (intervalCount - 1);
+
const intervals: Date[] = [];
- let currentTime = startTime;
- while (currentTime <= paddedEndTime) {
- intervals.push(new Date(currentTime));
- currentTime = new Date(currentTime.getTime() + intervalDuration);
+ for (let i = 0; i < intervalCount; i++) {
+ intervals.push(new Date(startTime.getTime() + i * intervalDuration));
}
return {
@@ -378,79 +416,104 @@ export default function IncidentTimeline({
(endTime.getTime() - startTime.getTime()) * pixelsPerMillisecond
);
+ // Filter out alerts with no audit events
+ const alertsWithEvents = alerts.items.filter((alert) =>
+ auditEvents.some((event) => event.fingerprint === alert.fingerprint)
+ );
+
+ if (alertsWithEvents.length === 0) {
+ return ;
+ }
+
return (
-
-
-
- {/* Time labels */}
-
- {intervals.map((time, index) => (
-
- {format(time, formatString)}
+
+
+
+
+
+ {/* Alert bars */}
+
+ {alertsWithEvents
+ .sort((a, b) => {
+ const aStart = Math.min(
+ ...auditEvents
+ .filter((e) => e.fingerprint === a.fingerprint)
+ .map((e) => parseISO(e.timestamp).getTime())
+ );
+ const bStart = Math.min(
+ ...auditEvents
+ .filter((e) => e.fingerprint === b.fingerprint)
+ .map((e) => parseISO(e.timestamp).getTime())
+ );
+ return aStart - bStart;
+ })
+ .map((alert, index, array) => (
+
+ ))}
- ))}
+
- {/* Alert bars */}
-
- {alerts?.items
- .sort((a, b) => {
- const aStart = Math.min(
- ...auditEvents
- .filter((e) => e.fingerprint === a.fingerprint)
- .map((e) => parseISO(e.timestamp).getTime())
- );
- const bStart = Math.min(
- ...auditEvents
- .filter((e) => e.fingerprint === b.fingerprint)
- .map((e) => parseISO(e.timestamp).getTime())
- );
- return aStart - bStart;
- })
- .map((alert, index, array) => (
-
+ {/* Time labels - Now sticky at bottom */}
+
+
+ {intervals.map((time, index) => (
+
+
+
{format(time, "MMM dd")}
+
{format(time, "HH:mm")}
+
))}
+
+
+ {/* Event details box */}
+ {selectedEvent && (
+
+
a.fingerprint === selectedEvent.fingerprint
+ )!
+ }
+ />
+
+ )}
-
- {/* Event details box */}
- {selectedEvent && (
-
-
a.fingerprint === selectedEvent.fingerprint
- )!
- }
- />
-
- )}
);
}
diff --git a/keep-ui/app/(keep)/settings/settings.client.tsx b/keep-ui/app/(keep)/settings/settings.client.tsx
index 7357e3b75..9ee2e3bec 100644
--- a/keep-ui/app/(keep)/settings/settings.client.tsx
+++ b/keep-ui/app/(keep)/settings/settings.client.tsx
@@ -361,7 +361,7 @@ export default function SettingsPage() {
Users and Access
handleTabChange("webhook")}>
- Webhook
+ Incoming Webhook
handleTabChange("smtp")}>
SMTP
diff --git a/keep-ui/app/(keep)/workflows/builder/builder.tsx b/keep-ui/app/(keep)/workflows/builder/builder.tsx
index d217e5386..232dc7b1f 100644
--- a/keep-ui/app/(keep)/workflows/builder/builder.tsx
+++ b/keep-ui/app/(keep)/workflows/builder/builder.tsx
@@ -110,7 +110,6 @@ function Builder({
Authorization: `Bearer ${accessToken}`,
};
const body = stringify(buildAlert(definition.value));
- debugger;
fetch(url, { method, headers, body })
.then((response) => {
if (response.ok) {
diff --git a/keep-ui/app/read-only-banner.tsx b/keep-ui/app/read-only-banner.tsx
index 80af6cd19..883f50d1a 100644
--- a/keep-ui/app/read-only-banner.tsx
+++ b/keep-ui/app/read-only-banner.tsx
@@ -23,11 +23,11 @@ const ReadOnlyBanner = () => {