diff --git a/src/components/admin/CorrelatedAlertsPanel.tsx b/src/components/admin/CorrelatedAlertsPanel.tsx new file mode 100644 index 00000000..7fc53381 --- /dev/null +++ b/src/components/admin/CorrelatedAlertsPanel.tsx @@ -0,0 +1,175 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle, AlertCircle, Link2, Clock, Sparkles } from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; +import type { CorrelatedAlert } from '@/hooks/admin/useCorrelatedAlerts'; +import { useCreateIncident } from '@/hooks/admin/useIncidents'; + +interface CorrelatedAlertsPanelProps { + correlations?: CorrelatedAlert[]; + isLoading: boolean; +} + +const SEVERITY_CONFIG = { + critical: { color: 'text-destructive', icon: AlertCircle, badge: 'bg-destructive/10 text-destructive' }, + high: { color: 'text-orange-500', icon: AlertTriangle, badge: 'bg-orange-500/10 text-orange-500' }, + medium: { color: 'text-yellow-500', icon: AlertTriangle, badge: 'bg-yellow-500/10 text-yellow-500' }, + low: { color: 'text-blue-500', icon: AlertTriangle, badge: 'bg-blue-500/10 text-blue-500' }, +}; + +export function CorrelatedAlertsPanel({ correlations, isLoading }: CorrelatedAlertsPanelProps) { + const createIncident = useCreateIncident(); + + const handleCreateIncident = (correlation: CorrelatedAlert) => { + createIncident.mutate({ + ruleId: correlation.rule_id, + title: correlation.incident_title_template, + description: correlation.rule_description, + severity: correlation.incident_severity, + alertIds: correlation.alert_ids, + alertSources: correlation.alert_sources as ('system' | 'rate_limit')[], + }); + }; + + if (isLoading) { + return ( + + + + + Correlated Alerts + + Loading correlation patterns... + + +
+
+
+
+
+ ); + } + + if (!correlations || correlations.length === 0) { + return ( + + + + + Correlated Alerts + + No correlated alert patterns detected + + +
+ +

Alert correlation engine is active

+

Incidents will be auto-detected when patterns match

+
+
+
+ ); + } + + return ( + + + + + + Correlated Alerts + + + {correlations.length} {correlations.length === 1 ? 'pattern' : 'patterns'} detected + + + + Multiple related alerts indicating potential incidents + + + + {correlations.map((correlation) => { + const config = SEVERITY_CONFIG[correlation.incident_severity]; + const Icon = config.icon; + + return ( +
+
+
+ +
+
+ + {config.badge.split(' ')[1].split('-')[0].toUpperCase()} + + + + Correlated + + + {correlation.matching_alerts_count} alerts + +
+

+ {correlation.rule_name} +

+

+ {correlation.rule_description} +

+
+ + + Window: {correlation.time_window_minutes}m + + + + First: {formatDistanceToNow(new Date(correlation.first_alert_at), { addSuffix: true })} + + + + Last: {formatDistanceToNow(new Date(correlation.last_alert_at), { addSuffix: true })} + +
+
+
+
+ {correlation.can_create_incident ? ( + + ) : ( + + Incident exists + + )} +
+
+ + {correlation.alert_messages.length > 0 && ( +
+

Sample alerts:

+
+ {correlation.alert_messages.slice(0, 3).map((message, idx) => ( +
+ {message} +
+ ))} +
+
+ )} +
+ ); + })} +
+
+ ); +} diff --git a/src/components/admin/IncidentsPanel.tsx b/src/components/admin/IncidentsPanel.tsx new file mode 100644 index 00000000..5e968f8f --- /dev/null +++ b/src/components/admin/IncidentsPanel.tsx @@ -0,0 +1,218 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { AlertCircle, AlertTriangle, CheckCircle2, Clock, Eye } from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; +import type { Incident } from '@/hooks/admin/useIncidents'; +import { useAcknowledgeIncident, useResolveIncident } from '@/hooks/admin/useIncidents'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { useState } from 'react'; + +interface IncidentsPanelProps { + incidents?: Incident[]; + isLoading: boolean; +} + +const SEVERITY_CONFIG = { + critical: { color: 'text-destructive', icon: AlertCircle, badge: 'destructive' }, + high: { color: 'text-orange-500', icon: AlertTriangle, badge: 'default' }, + medium: { color: 'text-yellow-500', icon: AlertTriangle, badge: 'secondary' }, + low: { color: 'text-blue-500', icon: AlertTriangle, badge: 'outline' }, +}; + +const STATUS_CONFIG = { + open: { label: 'Open', color: 'bg-red-500/10 text-red-600' }, + investigating: { label: 'Investigating', color: 'bg-yellow-500/10 text-yellow-600' }, + resolved: { label: 'Resolved', color: 'bg-green-500/10 text-green-600' }, + closed: { label: 'Closed', color: 'bg-gray-500/10 text-gray-600' }, +}; + +export function IncidentsPanel({ incidents, isLoading }: IncidentsPanelProps) { + const acknowledgeIncident = useAcknowledgeIncident(); + const resolveIncident = useResolveIncident(); + const [resolutionNotes, setResolutionNotes] = useState(''); + const [selectedIncident, setSelectedIncident] = useState(null); + + const handleAcknowledge = (incidentId: string) => { + acknowledgeIncident.mutate(incidentId); + }; + + const handleResolve = () => { + if (selectedIncident) { + resolveIncident.mutate({ + incidentId: selectedIncident, + resolutionNotes, + resolveAlerts: true, + }); + setResolutionNotes(''); + setSelectedIncident(null); + } + }; + + if (isLoading) { + return ( + + + Active Incidents + Loading incidents... + + +
+
+
+
+
+ ); + } + + if (!incidents || incidents.length === 0) { + return ( + + + Active Incidents + No active incidents + + +
+ +

All clear - no incidents detected

+
+
+
+ ); + } + + const openIncidents = incidents.filter(i => i.status === 'open' || i.status === 'investigating'); + + return ( + + + + Active Incidents + + {openIncidents.length} active • {incidents.length} total + + + + Automatically detected incidents from correlated alerts + + + + {incidents.map((incident) => { + const severityConfig = SEVERITY_CONFIG[incident.severity]; + const statusConfig = STATUS_CONFIG[incident.status]; + const Icon = severityConfig.icon; + + return ( +
+
+
+ +
+
+ + {incident.incident_number} + + + {incident.severity.toUpperCase()} + + + {statusConfig.label} + + + {incident.alert_count} alerts + +
+

{incident.title}

+ {incident.description && ( +

{incident.description}

+ )} +
+ + + Detected: {formatDistanceToNow(new Date(incident.detected_at), { addSuffix: true })} + + {incident.acknowledged_at && ( + + + Acknowledged: {formatDistanceToNow(new Date(incident.acknowledged_at), { addSuffix: true })} + + )} +
+
+
+
+ {incident.status === 'open' && ( + + )} + {(incident.status === 'open' || incident.status === 'investigating') && ( + + + + + + + Resolve Incident {incident.incident_number} + + Add resolution notes and close this incident. All linked alerts will be automatically resolved. + + +
+
+ +