mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:11:12 -05:00
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:
38
src/hooks/admin/useCorrelatedAlerts.ts
Normal file
38
src/hooks/admin/useCorrelatedAlerts.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
197
src/hooks/admin/useIncidents.ts
Normal file
197
src/hooks/admin/useIncidents.ts
Normal 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');
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user