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 +
+ + {incident.rule_fingerprint} + +
+ + )}
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 = () => {