import React, { useState, useEffect } from 'react';
import api from '../services/api';
import { format } from 'date-fns';
import { t } from '../utils/tempTranslations';
import {
PlusIcon,
BellIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ChevronDownIcon,
ChevronRightIcon
} from '@heroicons/react/24/outline';
import { EditAlertModal, DetectionDetailsModal } from '../components/AlertModals';
const Alerts = () => {
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 [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);
}
};
// 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
const getDroneTypeInfo = (detection) => {
if (!detection || detection.drone_type === undefined) {
return { name: 'Unknown', color: 'gray', bgColor: 'bg-gray-100', textColor: 'text-gray-600', icon: '🔍' };
}
// Map drone types based on the backend droneTypes utility
const droneTypeMap = {
0: { name: 'Unknown', color: 'gray', bgColor: 'bg-gray-100', textColor: 'text-gray-600', icon: '🔍' },
1: { name: 'Generic', color: 'blue', bgColor: 'bg-blue-100', textColor: 'text-blue-800', icon: '🚁' },
2: { name: 'Orlan', color: 'red', bgColor: 'bg-red-100', textColor: 'text-red-800', icon: '⚠️' },
3: { name: 'Zala', color: 'orange', bgColor: 'bg-orange-100', textColor: 'text-orange-800', icon: '🔶' },
4: { name: 'Forpost', color: 'purple', bgColor: 'bg-purple-100', textColor: 'text-purple-800', icon: '🟣' },
5: { name: 'Inokhodets', color: 'indigo', bgColor: 'bg-indigo-100', textColor: 'text-indigo-800', icon: '🔷' },
6: { name: 'Lancet', color: 'red', bgColor: 'bg-red-200', textColor: 'text-red-900', icon: '💥' },
7: { name: 'Shahed', color: 'yellow', bgColor: 'bg-yellow-100', textColor: 'text-yellow-800', icon: '⚡' },
8: { name: 'Geran', color: 'amber', bgColor: 'bg-amber-100', textColor: 'text-amber-800', icon: '🟨' },
9: { name: 'Kub', color: 'green', bgColor: 'bg-green-100', textColor: 'text-green-800', icon: '🟢' },
10: { name: 'X-UAV', color: 'teal', bgColor: 'bg-teal-100', textColor: 'text-teal-800', icon: '🔷' },
11: { name: 'SuperCam', color: 'cyan', bgColor: 'bg-cyan-100', textColor: 'text-cyan-800', icon: '📷' },
12: { name: 'Eleron', color: 'lime', bgColor: 'bg-lime-100', textColor: 'text-lime-800', icon: '🟩' },
13: { name: 'DJI', color: 'blue', bgColor: 'bg-blue-200', textColor: 'text-blue-900', icon: '📱' },
14: { name: 'Autel', color: 'violet', bgColor: 'bg-violet-100', textColor: 'text-violet-800', icon: '🟪' },
15: { name: 'Parrot', color: 'emerald', bgColor: 'bg-emerald-100', textColor: 'text-emerald-800', icon: '🦜' },
16: { name: 'Skydio', color: 'sky', bgColor: 'bg-sky-100', textColor: 'text-sky-800', icon: '☁️' },
17: { name: 'CryptoOrlan', color: 'red', bgColor: 'bg-red-300', textColor: 'text-red-900', icon: '🔴' }
};
return droneTypeMap[detection.drone_type] || droneTypeMap[0];
};
const handleDeleteRule = async (ruleId) => {
if (window.confirm('Are you sure you want to delete this alert rule?')) {
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);
}
};
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}
)}
|
{t(`alerts.${rule.priority}`)} |
{(rule.alert_channels || []).map((channel, index) => (
{channel}
))}
|
{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) => {
const droneTypeKeys = {
0: 'consumer',
1: 'orlan',
2: 'professional',
3: 'racing',
4: 'unknown'
};
return (
{t(`alerts.${droneTypeKeys[typeId] || 'unknown'}`)}
{typeId === 1 && '⚠️'}
);
})}
|
{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')} | {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.name === 'Orlan' && (
⚠️ MILITARY
)}
{droneTypeInfo.name === 'Lancet' && (
💥 KAMIKAZE
)}
{droneTypeInfo.name === 'CryptoOrlan' && (
🔴 ENCRYPTED
)}
{droneTypeInfo.name === 'DJI' && (
📱 COMMERCIAL
)}
);
})()}
|
{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}
);
})()}
|
{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)}...
) : (
-
)}
|