Files
thrilltrack-explorer/src/components/admin/IncidentsPanel.tsx
gpt-engineer-app[bot] 7fba819fc7 Implement alert correlation UI
- Add hooks and components for correlated alerts and incidents
- Integrate panels into MonitoringOverview
- Extend query keys for correlation and incidents
- Implement incident actions (create, acknowledge, resolve) and wiring
2025-11-11 02:03:20 +00:00

219 lines
8.7 KiB
TypeScript

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<string | null>(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 (
<Card>
<CardHeader>
<CardTitle>Active Incidents</CardTitle>
<CardDescription>Loading incidents...</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
</CardContent>
</Card>
);
}
if (!incidents || incidents.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Active Incidents</CardTitle>
<CardDescription>No active incidents</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
<CheckCircle2 className="h-12 w-12 mb-2 opacity-50" />
<p>All clear - no incidents detected</p>
</div>
</CardContent>
</Card>
);
}
const openIncidents = incidents.filter(i => i.status === 'open' || i.status === 'investigating');
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Active Incidents</span>
<span className="text-sm font-normal text-muted-foreground">
{openIncidents.length} active {incidents.length} total
</span>
</CardTitle>
<CardDescription>
Automatically detected incidents from correlated alerts
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{incidents.map((incident) => {
const severityConfig = SEVERITY_CONFIG[incident.severity];
const statusConfig = STATUS_CONFIG[incident.status];
const Icon = severityConfig.icon;
return (
<div
key={incident.id}
className="border rounded-lg p-4 space-y-3 bg-card"
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
<Icon className={`h-5 w-5 mt-0.5 ${severityConfig.color}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap mb-1">
<span className="text-xs font-mono font-medium px-2 py-0.5 rounded bg-muted">
{incident.incident_number}
</span>
<Badge variant={severityConfig.badge as any} className="text-xs">
{incident.severity.toUpperCase()}
</Badge>
<span className={`text-xs font-medium px-2 py-0.5 rounded ${statusConfig.color}`}>
{statusConfig.label}
</span>
<span className="text-xs px-2 py-0.5 rounded bg-primary/10 text-primary">
{incident.alert_count} alerts
</span>
</div>
<p className="text-sm font-medium mb-1">{incident.title}</p>
{incident.description && (
<p className="text-sm text-muted-foreground">{incident.description}</p>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
Detected: {formatDistanceToNow(new Date(incident.detected_at), { addSuffix: true })}
</span>
{incident.acknowledged_at && (
<span className="flex items-center gap-1">
<Eye className="h-3 w-3" />
Acknowledged: {formatDistanceToNow(new Date(incident.acknowledged_at), { addSuffix: true })}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{incident.status === 'open' && (
<Button
variant="outline"
size="sm"
onClick={() => handleAcknowledge(incident.id)}
disabled={acknowledgeIncident.isPending}
>
Acknowledge
</Button>
)}
{(incident.status === 'open' || incident.status === 'investigating') && (
<Dialog>
<DialogTrigger asChild>
<Button
variant="default"
size="sm"
onClick={() => setSelectedIncident(incident.id)}
>
Resolve
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Resolve Incident {incident.incident_number}</DialogTitle>
<DialogDescription>
Add resolution notes and close this incident. All linked alerts will be automatically resolved.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="resolution-notes">Resolution Notes</Label>
<Textarea
id="resolution-notes"
placeholder="Describe how this incident was resolved..."
value={resolutionNotes}
onChange={(e) => setResolutionNotes(e.target.value)}
rows={4}
/>
</div>
</div>
<DialogFooter>
<Button
variant="default"
onClick={handleResolve}
disabled={resolveIncident.isPending}
>
Resolve Incident
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
);
}