From 3703bbec5d18ff0d007ca951d28fdb96ef66797c Mon Sep 17 00:00:00 2001 From: Tal Borenstein Date: Wed, 18 Sep 2024 11:58:12 +0300 Subject: [PATCH] feat: working --- .../app/incidents/[id]/incident-alerts.tsx | 54 +++--- .../app/incidents/[id]/incident-timeline.tsx | 174 ++++++++++++------ keep-ui/app/incidents/[id]/incident.tsx | 7 +- keep-ui/utils/hooks/useTopology.ts | 14 +- keep/api/routes/alerts.py | 14 +- 5 files changed, 168 insertions(+), 95 deletions(-) diff --git a/keep-ui/app/incidents/[id]/incident-alerts.tsx b/keep-ui/app/incidents/[id]/incident-alerts.tsx index cf32d87d4..60a50d4da 100644 --- a/keep-ui/app/incidents/[id]/incident-alerts.tsx +++ b/keep-ui/app/incidents/[id]/incident-alerts.tsx @@ -27,8 +27,8 @@ import AlertName from "app/alerts/alert-name"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import IncidentAlertMenu from "./incident-alert-menu"; import IncidentPagination from "../incident-pagination"; -import React, {Dispatch, SetStateAction, useEffect, useState} from "react"; -import {IncidentDto} from "../model"; +import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { IncidentDto } from "../model"; interface Props { incident: IncidentDto; @@ -39,7 +39,6 @@ interface Pagination { offset: number; } - const columnHelper = createColumnHelper(); export default function IncidentAlerts({ incident }: Props) { @@ -48,11 +47,15 @@ export default function IncidentAlerts({ incident }: Props) { offset: 0, }); - const { data: alerts, isLoading } = useIncidentAlerts(incident.id, alertsPagination.limit, alertsPagination.offset); + const { data: alerts, isLoading } = useIncidentAlerts( + incident.id, + alertsPagination.limit, + alertsPagination.offset + ); const [pagination, setTablePagination] = useState({ - pageIndex: alerts? Math.ceil(alerts.offset / alerts.limit) : 0, - pageSize: alerts? alerts.limit : 20, + pageIndex: alerts ? Math.ceil(alerts.offset / alerts.limit) : 0, + pageSize: alerts ? alerts.limit : 20, }); useEffect(() => { @@ -60,16 +63,16 @@ export default function IncidentAlerts({ incident }: Props) { setAlertsPagination({ limit: pagination.pageSize, offset: 0, - }) + }); } const currentOffset = pagination.pageSize * pagination.pageIndex; if (alerts && alerts.offset != currentOffset) { setAlertsPagination({ limit: pagination.pageSize, offset: currentOffset, - }) + }); } - }, [pagination]) + }, [pagination]); usePollIncidentAlerts(incident.id); const columns = [ @@ -116,7 +119,7 @@ export default function IncidentAlerts({ incident }: Props) { (context.getValue() ?? []).map((source, index) => ( {source} ( - incident.is_confirmed && + cell: (context) => + incident.is_confirmed && ( - ), + ), }), ]; @@ -164,9 +167,9 @@ export default function IncidentAlerts({ incident }: Props) { {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { + {headerGroup.headers.map((header, index) => { return ( - + {flexRender( header.column.columnDef.header, header.getContext() @@ -179,10 +182,13 @@ export default function IncidentAlerts({ incident }: Props) { {alerts && alerts?.items?.length > 0 && ( - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - + {table.getRowModel().rows.map((row, index) => ( + + {row.getVisibleCells().map((cell, index) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} @@ -196,10 +202,10 @@ export default function IncidentAlerts({ incident }: Props) { {Array(pagination.pageSize) .fill("") - .map((index) => ( - - {columns.map((c) => ( - + .map((index, rowIndex) => ( + + {columns.map((c, cellIndex) => ( + ))} @@ -211,7 +217,7 @@ export default function IncidentAlerts({ incident }: Props) {
- +
); diff --git a/keep-ui/app/incidents/[id]/incident-timeline.tsx b/keep-ui/app/incidents/[id]/incident-timeline.tsx index 3a9b9da63..6e977e8dd 100644 --- a/keep-ui/app/incidents/[id]/incident-timeline.tsx +++ b/keep-ui/app/incidents/[id]/incident-timeline.tsx @@ -1,16 +1,7 @@ "use client"; -import React, { useMemo, useState } from "react"; -import { - format, - parseISO, - differenceInMinutes, - differenceInHours, - differenceInDays, - addMinutes, - addHours, - addDays, -} from "date-fns"; +import React, { useEffect, useMemo, useState } from "react"; +import { format, parseISO } from "date-fns"; import { AuditEvent, useAlerts } from "utils/hooks/useAlerts"; import { AlertDto } from "app/alerts/models"; import { useIncidentAlerts } from "utils/hooks/useIncidents"; @@ -58,14 +49,22 @@ const AlertEventInfo: React.FC<{ event: AuditEvent; alert: AlertDto }> = ({ }) => { return (
-

{alert.name}

+

+ {alert.name} ({alert.fingerprint}) +

{alert.description}

-
+

Date:

{format(parseISO(event.timestamp), "dd, MMM yyyy - HH:mm.ss 'UTC'")}

+

Action:

+

{event.action}

+ +

Description:

+

{event.description}

+

Severity:

@@ -208,7 +207,13 @@ const AlertBar: React.FC = ({ >
- + {alert.name}
@@ -241,13 +246,22 @@ export default function IncidentTimeline({ incident.id ); const { useMultipleFingerprintsAlertAudit } = useAlerts(); - const { data: auditEvents, isLoading: auditEventsLoading } = - useMultipleFingerprintsAlertAudit(alerts?.items.map((m) => m.fingerprint)); + const { + data: auditEvents, + isLoading: auditEventsLoading, + mutate, + } = useMultipleFingerprintsAlertAudit( + alerts?.items.map((m) => m.fingerprint) + ); const [selectedEvent, setSelectedEvent] = useState(null); + useEffect(() => { + mutate(); + }, [alerts, mutate]); + const timelineData = useMemo(() => { - if (auditEvents) { + if (auditEvents && alerts) { const allTimestamps = auditEvents.map((event) => parseISO(event.timestamp).getTime() ); @@ -255,70 +269,110 @@ export default function IncidentTimeline({ const startTime = new Date(Math.min(...allTimestamps)); const endTime = new Date(Math.max(...allTimestamps)); - const paddedStartTime = new Date(startTime.getTime()); - const paddedEndTime = new Date(endTime.getTime()); + // Add padding to start and end times + const paddedStartTime = new Date(startTime.getTime() - 1000 * 60 * 10); // 10 minutes before + const paddedEndTime = new Date(endTime.getTime() + 1000 * 60 * 10); // 10 minutes after - const daysDifference = differenceInDays(paddedEndTime, paddedStartTime); + const totalDuration = paddedEndTime.getTime() - paddedStartTime.getTime(); + const pixelsPerMillisecond = 5000 / totalDuration; // Assuming 5000px minimum width let timeScale: "minutes" | "hours" | "days"; - let intervals; - let formatString; + let intervalDuration: number; + let formatString: string; - if (daysDifference > 3) { + if (totalDuration > 3 * 24 * 60 * 60 * 1000) { timeScale = "days"; - intervals = Array.from({ length: 9 }, (_, i) => - addDays(paddedStartTime, (i * daysDifference) / 8) - ); + intervalDuration = 24 * 60 * 60 * 1000; formatString = "MMM dd"; - } else if (daysDifference > 1) { + } else if (totalDuration > 24 * 60 * 60 * 1000) { timeScale = "hours"; - intervals = Array.from({ length: 9 }, (_, i) => - addHours( - paddedStartTime, - (i * differenceInHours(paddedEndTime, paddedStartTime)) / 8 - ) - ); + intervalDuration = 60 * 60 * 1000; formatString = "HH:mm"; } else { timeScale = "minutes"; - intervals = Array.from({ length: 9 }, (_, i) => - addMinutes( - paddedStartTime, - (i * differenceInMinutes(paddedEndTime, paddedStartTime)) / 8 - ) - ); + intervalDuration = 5 * 60 * 1000; // 5-minute intervals formatString = "HH:mm:ss"; } + const intervals: Date[] = []; + let currentTime = paddedStartTime; + while (currentTime <= paddedEndTime) { + intervals.push(new Date(currentTime)); + currentTime = new Date(currentTime.getTime() + intervalDuration); + } + return { startTime: paddedStartTime, endTime: paddedEndTime, intervals, formatString, timeScale, + pixelsPerMillisecond, }; } return {}; - }, [auditEvents]); + }, [auditEvents, alerts]); if (auditEventsLoading || !auditEvents || alertsLoading) return <>No Data; - const { startTime, endTime, intervals, formatString, timeScale } = - timelineData; + const { + startTime, + endTime, + intervals, + formatString, + timeScale, + pixelsPerMillisecond, + } = timelineData; + + if ( + !intervals || + !startTime || + !endTime || + !timeScale || + !pixelsPerMillisecond + ) + return <>No Data; - if (!intervals || !startTime || !endTime || !timeScale) return <>No Data; + const totalWidth = Math.max( + 5000, + (endTime.getTime() - startTime.getTime()) * pixelsPerMillisecond + ); return (
-
+
{/* Time labels */} -
+
{intervals.map((time, index) => ( -
+
{format(time, formatString)}
))} + {/* Add an extra label for the first time, positioned at the start */} +
+ {format(intervals[0], formatString)} +
{/* Alert bars */} @@ -345,27 +399,31 @@ export default function IncidentTimeline({ startTime={startTime} endTime={endTime} timeScale={timeScale} + pixelsPerMillisecond={pixelsPerMillisecond} onEventClick={setSelectedEvent} selectedEventId={selectedEvent?.id || null} isFirstRow={index === 0} isLastRow={index === array.length - 1} + minWidth={200} /> ))}
-
- {/* Event details box */} - {selectedEvent && ( - a.fingerprint === selectedEvent.fingerprint - )! - } - /> - )}
+
+ {/* Event details box */} + {selectedEvent && ( +
+ a.fingerprint === selectedEvent.fingerprint + )! + } + /> +
+ )}
); } diff --git a/keep-ui/app/incidents/[id]/incident.tsx b/keep-ui/app/incidents/[id]/incident.tsx index 10bd07e36..1150f58e5 100644 --- a/keep-ui/app/incidents/[id]/incident.tsx +++ b/keep-ui/app/incidents/[id]/incident.tsx @@ -52,11 +52,8 @@ export default function IncidentView({ incidentId }: Props) {
-
-
+
+
value === null || value === undefined; @@ -20,7 +20,7 @@ export const useTopology = ( environment?: string ) => { const { data: session } = useSession(); - useTopologyPolling(); + const { data: pollTopology } = useTopologyPolling(); const apiUrl = getApiURL(); const url = !session @@ -36,6 +36,12 @@ export const useTopology = ( (url: string) => fetcher(url, session!.accessToken) ); + useEffect(() => { + if (pollTopology) { + mutate(); + } + }, [pollTopology, mutate]); + return { topologyData: data, error, @@ -46,12 +52,14 @@ export const useTopology = ( export const useTopologyPolling = () => { const { bind, unbind } = useWebsocket(); + const [pollTopology, setPollTopology] = useState(0); const handleIncoming = useCallback((data: TopologyUpdate) => { toast.success( `Topology pulled from ${data.providerId} (${data.providerType})`, { position: "top-right" } ); + setPollTopology(Math.floor(Math.random() * 10000)); }, []); useEffect(() => { @@ -60,4 +68,6 @@ export const useTopologyPolling = () => { unbind("topology-update", handleIncoming); }; }, [bind, unbind, handleIncoming]); + + return { data: pollTopology }; }; diff --git a/keep/api/routes/alerts.py b/keep/api/routes/alerts.py index 1092543ba..ae9f1d675 100644 --- a/keep/api/routes/alerts.py +++ b/keep/api/routes/alerts.py @@ -4,7 +4,6 @@ import json import logging import os -from itertools import groupby from typing import Optional import celpy @@ -695,11 +694,14 @@ def get_multiple_fingerprint_alert_audit( if not alert_audit: raise HTTPException(status_code=404, detail="Alert not found") grouped_events = [] - # Group the results by fingerprint - grouped_audit = { - fingerprint: list(group) - for fingerprint, group in groupby(alert_audit, key=lambda x: x.fingerprint) - } + + # Group the results by fingerprint for "deduplication" (2x, 3x, etc.) thingy.. + grouped_audit = {} + for audit in alert_audit: + if audit.fingerprint not in grouped_audit: + grouped_audit[audit.fingerprint] = [] + grouped_audit[audit.fingerprint].append(audit) + for values in grouped_audit.values(): grouped_events.extend(AlertAuditDto.from_orm_list(values)) return grouped_events