import React, { useState, useEffect } from 'react';
import api from '../services/api';
import { format } from 'date-fns';
import { useTranslation } from '../utils/tempTranslations';
import {
PlusIcon,
BellIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ChevronDownIcon,
ChevronRightIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
import { EditAlertModal, DetectionDetailsModal } from '../components/AlertModals';
import { useDroneTypes } from '../hooks/useDroneTypes';
const Alerts = () => {
const { t } = useTranslation();
// Drone types hook for dynamic drone type data
const { getDroneTypeInfo: getDroneTypeInfoFromAPI, loading: droneTypesLoading } = useDroneTypes();
const [alertRules, setAlertRules] = useState([]);
const [alertLogs, setAlertLogs] = useState([]);
const [alertStats, setAlertStats] = useState(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('rules');
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [editingRule, setEditingRule] = useState(null);
const [showDetectionModal, setShowDetectionModal] = useState(false);
const [selectedDetection, setSelectedDetection] = useState(null);
const [showAlertDetailsModal, setShowAlertDetailsModal] = useState(false);
const [selectedAlertForDetails, setSelectedAlertForDetails] = useState(null);
const [expandedGroups, setExpandedGroups] = useState(new Set());
useEffect(() => {
fetchAlertData();
}, []);
const fetchAlertData = async () => {
try {
const [rulesRes, logsRes, statsRes] = await Promise.all([
api.get('/alerts/rules'),
api.get('/alerts/logs?limit=50'),
api.get('/alerts/stats?hours=24')
]);
setAlertRules(rulesRes.data?.data || []);
setAlertLogs(logsRes.data?.data || []);
setAlertStats(statsRes.data?.data || null);
} catch (error) {
console.error('Error fetching alert data:', error);
// Set default values on error
setAlertRules([]);
setAlertLogs([]);
setAlertStats(null);
} finally {
setLoading(false);
}
};
// Show loading if either alerts or drone types are loading
const isLoading = loading || droneTypesLoading;
// DEBUG: Validate state integrity
useEffect(() => {
console.log('DEBUG: Alerts state check', {
alertRules: alertRules?.length,
alertLogs: alertLogs?.length,
alertStats: alertStats ? 'present' : 'null',
invalidRules: alertRules?.filter(rule => typeof rule.alert_channels === 'object' && !Array.isArray(rule.alert_channels))
});
}, [alertRules, alertLogs, alertStats]);
// Group alerts by alert_event_id to show related alerts together
const groupAlertsByEvent = (logs) => {
const grouped = {};
const ungrouped = [];
logs.forEach(log => {
if (log.alert_event_id) {
if (!grouped[log.alert_event_id]) {
grouped[log.alert_event_id] = [];
}
grouped[log.alert_event_id].push(log);
} else {
ungrouped.push(log);
}
});
// Convert grouped object to array of arrays, sorted by most recent alert in each group
const groupedArrays = Object.values(grouped).map(group =>
group.sort((a, b) => new Date(b.sent_at) - new Date(a.sent_at))
).sort((a, b) => new Date(b[0].sent_at) - new Date(a[0].sent_at));
// Add ungrouped alerts as individual groups
ungrouped.forEach(log => groupedArrays.push([log]));
return groupedArrays;
};
const toggleGroupExpansion = (groupIndex) => {
const newExpanded = new Set(expandedGroups);
if (newExpanded.has(groupIndex)) {
newExpanded.delete(groupIndex);
} else {
newExpanded.add(groupIndex);
}
setExpandedGroups(newExpanded);
};
// Get drone type information with visual styling (now using dynamic API data)
const getDroneTypeInfo = (detection) => {
if (!detection || detection.drone_type === undefined) {
return { name: 'Unknown', color: 'gray', bgColor: 'bg-gray-100', textColor: 'text-gray-600', icon: '🔍' };
}
// Use the dynamic API data from the hook
return getDroneTypeInfoFromAPI(detection.drone_type);
};
const handleDeleteRule = async (ruleId) => {
if (window.confirm(t('alerts.deleteRule') + '?')) {
try {
await api.delete(`/alerts/rules/${ruleId}`);
fetchAlertData();
} catch (error) {
console.error('Error deleting alert rule:', error);
}
}
};
const handleEditRule = (rule) => {
setEditingRule(rule);
setShowEditModal(true);
};
const handleViewDetection = async (detectionId) => {
try {
const response = await api.get(`/detections/${detectionId}`);
setSelectedDetection(response.data.data);
setShowDetectionModal(true);
} catch (error) {
console.error('Error fetching detection details:', error);
}
};
// Parse error message JSON to extract meaningful information
const parseErrorMessage = (errorMessage) => {
if (!errorMessage) return null;
try {
// Check if it's a success message
if (errorMessage.startsWith('Success:')) {
const jsonPart = errorMessage.substring(8).trim();
const successData = JSON.parse(jsonPart);
return {
type: 'success',
data: successData
};
}
// Check if it's a webhook network error
if (errorMessage.startsWith('Webhook network error:')) {
const jsonPart = errorMessage.substring(22).trim();
const errorData = JSON.parse(jsonPart);
return {
type: 'webhook_error',
data: errorData
};
}
// Check if it's an SMS error
if (errorMessage.startsWith('SMS error:')) {
const jsonPart = errorMessage.substring(10).trim();
const errorData = JSON.parse(jsonPart);
return {
type: 'sms_error',
data: errorData
};
}
// Check if it's an email error
if (errorMessage.startsWith('Email error:')) {
const jsonPart = errorMessage.substring(12).trim();
const errorData = JSON.parse(jsonPart);
return {
type: 'email_error',
data: errorData
};
}
// Try to parse as direct JSON
const parsed = JSON.parse(errorMessage);
return {
type: 'parsed_json',
data: parsed
};
} catch (error) {
// If parsing fails, return the raw message
return {
type: 'raw',
data: { message: errorMessage }
};
}
};
const handleViewAlertDetails = (alert) => {
setSelectedAlertForDetails(alert);
setShowAlertDetailsModal(true);
};
const getStatusIcon = (status) => {
switch (status) {
case 'sent':
return
{t('alerts.description')}
{t('alerts.noAlertRulesDescription')}
| {t('alerts.name')} | {t('alerts.priority')} | {t('alerts.channels')} | {t('alerts.conditions')} | {t('alerts.status')} | {t('alerts.created')} | {t('alerts.actions')} |
|---|---|---|---|---|---|---|
|
{rule.name}
{rule.description && (
{rule.description}
)}
|
{(() => { // DEBUG: Check if priority is an object if (typeof rule.priority === 'object' && rule.priority !== null) { console.error('DEBUG: Priority is an object:', rule.priority, 'for rule:', rule.id); return 'Invalid Priority'; } return t(`alerts.${rule.priority}`); })()} |
{(() => {
// Normalize alert_channels - ensure it's always an array
let alertChannels = rule.alert_channels || [];
// DEBUG: Add debugging to catch object rendering
if (typeof alertChannels === 'object' && alertChannels !== null) {
const hasKeys = Object.keys(alertChannels).some(key => ['sms', 'webhook', 'email'].includes(key));
if (hasKeys) {
console.log('DEBUG: Found problematic alert_channels object:', alertChannels, 'for rule:', rule.id);
}
}
if (typeof alertChannels === 'object' && !Array.isArray(alertChannels)) {
// Convert object like {sms: true, webhook: false, email: true} to array
alertChannels = Object.keys(alertChannels).filter(key => alertChannels[key]);
}
if (!Array.isArray(alertChannels)) {
alertChannels = []; // fallback to empty array
}
// DEBUG: Ensure we're only rendering strings
return alertChannels.map((channel, index) => {
if (typeof channel !== 'string') {
console.error('DEBUG: Non-string channel detected:', channel, typeof channel);
return null;
}
return (
{channel}
);
}).filter(Boolean);
})()}
|
{rule.min_detections > 1 && (
{t('alerts.minDetections')}: {rule.min_detections}
)}
{rule.time_window && (
{t('alerts.timeWindow')}: {rule.time_window}s
)}
{rule.cooldown_period && (
{t('alerts.cooldown')}: {rule.cooldown_period}s
)}
{rule.min_threat_level && (
{t('alerts.minThreat')}: {rule.min_threat_level}
)}
{rule.drone_types && rule.drone_types.length > 0 && (
{t('alerts.droneTypes')}:
{rule.drone_types.map((typeId, index) => {
// Use the dynamic API data instead of hardcoded mapping
const droneInfo = getDroneTypeInfoFromAPI(typeId);
return (
{droneInfo.name}
{droneInfo.warning && '⚠️'}
);
})}
|
{rule.is_active ? t('alerts.active') : t('alerts.inactive')} |
{format(new Date(rule.created_at), 'MMM dd, yyyy')}
|
|
{t('alerts.noAlertLogsDescription')}
💡 Alert Grouping: Related alerts (SMS, email, webhook) for the same detection event are grouped together. Click the expand button (▶) or "+X more" badge to see all alerts in a group.
| {t('alerts.status')} | {t('alerts.type')} | {t('alerts.recipient')} | {t('alerts.rule')} | Drone Type | {t('alerts.droneId')} | {t('alerts.detection')} | Alert Details | {t('alerts.message')} | {t('alerts.sentAt')} | Event ID |
|---|---|---|---|---|---|---|---|---|---|---|
|
{relatedAlerts.length > 0 && (
)}
{getStatusIcon(primaryAlert.status)}
{primaryAlert.status}
{relatedAlerts.length > 0 && (
)}
|
{primaryAlert.alert_type}
{!expandedGroups.has(groupIndex) && relatedAlerts.map((alert, idx) => (
{alert.alert_type}
))}
|
{primaryAlert.recipient}
{!expandedGroups.has(groupIndex) && relatedAlerts.length > 0 && relatedAlerts.some(a => a.recipient !== primaryAlert.recipient) && (
+{relatedAlerts.filter(a => a.recipient !== primaryAlert.recipient).length} others
)}
|
{primaryAlert.rule?.name || t('alerts.unknownRule')}
|
{(() => {
const droneTypeInfo = getDroneTypeInfo(primaryAlert.detection);
return (
{droneTypeInfo.icon} {droneTypeInfo.name}
{droneTypeInfo.warning && (
{droneTypeInfo.warning}
)}
);
})()}
|
{primaryAlert.detection?.drone_id ? (
{primaryAlert.detection.drone_id}
) : (
{t('alerts.na')}
)}
|
{primaryAlert.detection_id ? ( ) : ( {t('alerts.na')} )} |
{primaryAlert.message}
|
{format(new Date(primaryAlert.sent_at), 'MMM dd, HH:mm')}
|
{primaryAlert.alert_event_id ? (
{primaryAlert.alert_event_id.substring(0, 8)}...
) : (
-
)}
|
|
|
{getStatusIcon(alert.status)}
{alert.status}
|
{alert.alert_type} |
{alert.recipient}
|
{alert.rule?.name || t('alerts.unknownRule')}
|
{(() => {
const droneTypeInfo = getDroneTypeInfo(alert.detection);
return (
{droneTypeInfo.icon} {droneTypeInfo.name}
{droneTypeInfo.warning && (
{droneTypeInfo.warning}
)}
);
})()}
|
{alert.detection?.drone_id ? (
{alert.detection.drone_id}
) : (
{t('alerts.na')}
)}
|
{alert.detection_id ? ( ) : ( {t('alerts.na')} )} |
{alert.message}
|
{format(new Date(alert.sent_at), 'MMM dd, HH:mm')}
|
{alert.alert_event_id ? (
{alert.alert_event_id.substring(0, 8)}...
) : (
-
)}
|
{JSON.stringify(data, null, 2)}