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
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 02:03:20 +00:00
parent 5a8caa51b6
commit 7fba819fc7
7 changed files with 731 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabaseClient';
import { queryKeys } from '@/lib/queryKeys';
export interface CorrelatedAlert {
rule_id: string;
rule_name: string;
rule_description: string;
incident_severity: 'critical' | 'high' | 'medium' | 'low';
incident_title_template: string;
time_window_minutes: number;
min_alerts_required: number;
matching_alerts_count: number;
alert_ids: string[];
alert_sources: string[];
alert_messages: string[];
first_alert_at: string;
last_alert_at: string;
can_create_incident: boolean;
}
export function useCorrelatedAlerts() {
return useQuery({
queryKey: queryKeys.monitoring.correlatedAlerts(),
queryFn: async () => {
const { data, error } = await supabase
.from('alert_correlations_view')
.select('*')
.order('incident_severity', { ascending: true })
.order('matching_alerts_count', { ascending: false });
if (error) throw error;
return (data || []) as CorrelatedAlert[];
},
staleTime: 15000,
refetchInterval: 30000,
});
}

View File

@@ -0,0 +1,197 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabaseClient';
import { queryKeys } from '@/lib/queryKeys';
import { toast } from 'sonner';
export interface Incident {
id: string;
incident_number: string;
title: string;
description: string;
severity: 'critical' | 'high' | 'medium' | 'low';
status: 'open' | 'investigating' | 'resolved' | 'closed';
correlation_rule_id?: string;
detected_at: string;
acknowledged_at?: string;
acknowledged_by?: string;
resolved_at?: string;
resolved_by?: string;
resolution_notes?: string;
alert_count: number;
created_at: string;
updated_at: string;
}
export function useIncidents(status?: 'open' | 'investigating' | 'resolved' | 'closed') {
return useQuery({
queryKey: queryKeys.monitoring.incidents(status),
queryFn: async () => {
let query = supabase
.from('incidents')
.select('*')
.order('detected_at', { ascending: false });
if (status) {
query = query.eq('status', status);
}
const { data, error } = await query;
if (error) throw error;
return (data || []) as Incident[];
},
staleTime: 15000,
refetchInterval: 30000,
});
}
export function useCreateIncident() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
ruleId,
title,
description,
severity,
alertIds,
alertSources,
}: {
ruleId?: string;
title: string;
description?: string;
severity: 'critical' | 'high' | 'medium' | 'low';
alertIds: string[];
alertSources: ('system' | 'rate_limit')[];
}) => {
// Create the incident (incident_number is auto-generated by trigger)
const { data: incident, error: incidentError } = await supabase
.from('incidents')
.insert([{
title,
description,
severity,
correlation_rule_id: ruleId,
status: 'open' as const,
} as any])
.select()
.single();
if (incidentError) throw incidentError;
// Link alerts to the incident
const incidentAlerts = alertIds.map((alertId, index) => ({
incident_id: incident.id,
alert_source: alertSources[index] || 'system',
alert_id: alertId,
}));
const { error: linkError } = await supabase
.from('incident_alerts')
.insert(incidentAlerts);
if (linkError) throw linkError;
return incident as Incident;
},
onSuccess: (incident) => {
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.incidents() });
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.correlatedAlerts() });
toast.success(`Incident ${incident.incident_number} created`);
},
onError: (error) => {
console.error('Failed to create incident:', error);
toast.error('Failed to create incident');
},
});
}
export function useAcknowledgeIncident() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (incidentId: string) => {
const { data, error } = await supabase
.from('incidents')
.update({
status: 'investigating',
acknowledged_at: new Date().toISOString(),
acknowledged_by: (await supabase.auth.getUser()).data.user?.id,
})
.eq('id', incidentId)
.select()
.single();
if (error) throw error;
return data as Incident;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.incidents() });
toast.success('Incident acknowledged');
},
onError: (error) => {
console.error('Failed to acknowledge incident:', error);
toast.error('Failed to acknowledge incident');
},
});
}
export function useResolveIncident() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
incidentId,
resolutionNotes,
resolveAlerts = true,
}: {
incidentId: string;
resolutionNotes?: string;
resolveAlerts?: boolean;
}) => {
const userId = (await supabase.auth.getUser()).data.user?.id;
// Update incident
const { error: incidentError } = await supabase
.from('incidents')
.update({
status: 'resolved',
resolved_at: new Date().toISOString(),
resolved_by: userId,
resolution_notes: resolutionNotes,
})
.eq('id', incidentId);
if (incidentError) throw incidentError;
// Optionally resolve all linked alerts
if (resolveAlerts) {
const { data: linkedAlerts } = await supabase
.from('incident_alerts')
.select('alert_source, alert_id')
.eq('incident_id', incidentId);
if (linkedAlerts) {
for (const alert of linkedAlerts) {
const table = alert.alert_source === 'system' ? 'system_alerts' : 'rate_limit_alerts';
await supabase
.from(table)
.update({ resolved_at: new Date().toISOString() })
.eq('id', alert.alert_id);
}
}
}
return { incidentId };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.incidents() });
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.groupedAlerts() });
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.combinedAlerts() });
toast.success('Incident resolved');
},
onError: (error) => {
console.error('Failed to resolve incident:', error);
toast.error('Failed to resolve incident');
},
});
}