Files
drone-detector/client/src/components/AlertModals.jsx
2025-08-28 13:35:11 +02:00

478 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import api from '../services/api';
import { format } from 'date-fns';
import { XMarkIcon } from '@heroicons/react/24/outline';
// Edit Alert Rule Modal
export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
const [formData, setFormData] = useState({
name: '',
description: '',
priority: 'medium',
min_detections: 1,
time_window: 300,
cooldown_period: 600,
alert_channels: ['sms'],
min_threat_level: '',
drone_types: [],
device_ids: [],
sms_phone_number: '',
webhook_url: ''
});
const [saving, setSaving] = useState(false);
const [droneTypes, setDroneTypes] = useState([]);
useEffect(() => {
const fetchDroneTypes = async () => {
try {
const response = await api.get('/drone-types');
setDroneTypes(response.data.data || []);
} catch (error) {
console.error('Error fetching drone types:', error);
setDroneTypes([]);
}
};
fetchDroneTypes();
}, []);
useEffect(() => {
if (rule) {
setFormData({
name: rule.name || '',
description: rule.description || '',
priority: rule.priority || 'medium',
min_detections: rule.min_detections || 1,
time_window: rule.time_window || 300,
cooldown_period: rule.cooldown_period || 600,
alert_channels: rule.alert_channels || ['sms'],
min_threat_level: rule.min_threat_level || '',
drone_types: rule.drone_types || [],
device_ids: rule.device_ids || [],
sms_phone_number: rule.sms_phone_number || '',
webhook_url: rule.webhook_url || ''
});
}
}, [rule]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleChannelChange = (channel, checked) => {
setFormData(prev => ({
...prev,
alert_channels: checked
? [...prev.alert_channels, channel]
: prev.alert_channels.filter(c => c !== channel)
}));
};
const handleDroneTypeChange = (droneType, checked) => {
setFormData(prev => ({
...prev,
drone_types: checked
? [...prev.drone_types, droneType]
: prev.drone_types.filter(type => type !== droneType)
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
try {
const payload = { ...formData };
// Clean up empty values
if (!payload.description || payload.description.trim() === '') delete payload.description;
if (!payload.device_ids || payload.device_ids.length === 0) delete payload.device_ids;
if (!payload.drone_types || payload.drone_types.length === 0) delete payload.drone_types;
// Only include webhook_url if webhook channel is selected
if (!payload.alert_channels || !payload.alert_channels.includes('webhook')) {
delete payload.webhook_url;
}
await api.put(`/alerts/rules/${rule.id}`, payload);
onSuccess();
onClose();
} catch (error) {
console.error('Error updating alert rule:', error);
alert('Failed to update alert rule');
} finally {
setSaving(false);
}
};
return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="flex items-center justify-between pb-3">
<h3 className="text-lg font-medium">Edit Alert Rule</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name *
</label>
<input
type="text"
name="name"
required
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.name}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
name="description"
rows="2"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.description}
onChange={handleChange}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Priority
</label>
<select
name="priority"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.priority}
onChange={handleChange}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Min Threat Level
</label>
<select
name="min_threat_level"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.min_threat_level}
onChange={handleChange}
>
<option value="">Any Level</option>
<option value="monitoring">Monitoring</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Drone Types Filter
</label>
<div className="space-y-2">
<div className="text-xs text-gray-500 mb-2">
Leave empty to monitor all drone types
</div>
{droneTypes.map(droneType => (
<label key={droneType.id} className="flex items-center">
<input
type="checkbox"
checked={formData.drone_types.includes(droneType.id)}
onChange={(e) => handleDroneTypeChange(droneType.id, e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">
{droneType.name}
{droneType.threat_level === 'critical' && <span className="text-red-600 font-semibold"> ( High Threat)</span>}
</span>
</label>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Min Detections
</label>
<input
type="number"
name="min_detections"
min="1"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.min_detections}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Time Window (seconds)
</label>
<input
type="number"
name="time_window"
min="60"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.time_window}
onChange={handleChange}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cooldown Period (seconds)
</label>
<input
type="number"
name="cooldown_period"
min="0"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.cooldown_period}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Alert Channels
</label>
<div className="space-y-2">
{['sms', 'email', 'webhook'].map(channel => (
<label key={channel} className="flex items-center">
<input
type="checkbox"
checked={formData.alert_channels.includes(channel)}
onChange={(e) => handleChannelChange(channel, e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700 capitalize">
{channel}
</span>
</label>
))}
</div>
</div>
{formData.alert_channels.includes('sms') && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
SMS Phone Number
</label>
<input
type="tel"
name="sms_phone_number"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.sms_phone_number}
onChange={handleChange}
placeholder="+1234567890"
/>
</div>
)}
{formData.alert_channels.includes('webhook') && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Webhook URL
</label>
<input
type="url"
name="webhook_url"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.webhook_url}
onChange={handleChange}
placeholder="https://example.com/webhook"
/>
</div>
)}
<div className="flex space-x-3 pt-4">
<button
type="submit"
disabled={saving}
className="flex-1 bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:opacity-50"
>
{saving ? 'Updating...' : 'Update Rule'}
</button>
<button
type="button"
onClick={onClose}
className="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
};
// Detection Details Modal
export const DetectionDetailsModal = ({ detection, onClose }) => {
if (!detection) return null;
const getPriorityColor = (level) => {
switch (level?.toLowerCase()) {
case 'critical':
return 'bg-red-100 text-red-800';
case 'high':
return 'bg-orange-100 text-orange-800';
case 'medium':
return 'bg-yellow-100 text-yellow-800';
case 'low':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-2/3 max-w-4xl shadow-lg rounded-md bg-white">
<div className="flex items-center justify-between pb-3">
<h3 className="text-lg font-medium">Detection Details</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<div className="space-y-6">
{/* Basic Information */}
<div className="grid grid-cols-2 gap-6">
<div className="space-y-3">
<h4 className="font-medium text-gray-900">Basic Information</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">ID:</span>
<span className="font-mono text-xs">{detection.id}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Detected At:</span>
<span>
{detection.detected_at
? format(new Date(detection.detected_at), 'MMM dd, yyyy HH:mm:ss')
: 'Unknown'
}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Device:</span>
<span>{detection.device?.name || `Device ${detection.device_id}`}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Location:</span>
<span>{detection.device?.location || 'Unknown'}</span>
</div>
</div>
</div>
<div className="space-y-3">
<h4 className="font-medium text-gray-900">Signal Information</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">RSSI:</span>
<span className="font-medium">{detection.rssi} dBm</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Frequency:</span>
<span>{detection.frequency ? `${detection.frequency} MHz` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Drone Type:</span>
<span>{detection.drone_type || 'Unknown'}</span>
</div>
{detection.estimated_distance && (
<div className="flex justify-between">
<span className="text-gray-500">Est. Distance:</span>
<span>{detection.estimated_distance}m</span>
</div>
)}
</div>
</div>
</div>
{/* Threat Assessment */}
{detection.threat_assessment && (
<div className="space-y-3">
<h4 className="font-medium text-gray-900">Threat Assessment</h4>
<div className="bg-gray-50 p-4 rounded-md">
<div className="flex items-center justify-between mb-3">
<span className="text-sm text-gray-500">Threat Level:</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getPriorityColor(detection.threat_assessment.level)}`}>
{detection.threat_assessment.level?.toUpperCase()}
</span>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Priority:</span>
<span>{detection.threat_assessment.priority}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Immediate Action:</span>
<span className={detection.threat_assessment.requiresImmediateAction ? 'text-red-600 font-medium' : 'text-gray-600'}>
{detection.threat_assessment.requiresImmediateAction ? 'Required' : 'Not Required'}
</span>
</div>
{detection.threat_assessment.description && (
<div className="mt-3">
<span className="text-gray-500 block mb-1">Description:</span>
<p className="text-gray-700">{detection.threat_assessment.description}</p>
</div>
)}
</div>
</div>
</div>
)}
{/* Raw Data */}
{detection.raw_data && (
<div className="space-y-3">
<h4 className="font-medium text-gray-900">Raw Data</h4>
<div className="bg-gray-900 text-green-400 p-4 rounded-md font-mono text-xs overflow-x-auto">
<pre>{JSON.stringify(detection.raw_data, null, 2)}</pre>
</div>
</div>
)}
</div>
<div className="flex justify-end pt-4">
<button
onClick={onClose}
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
Close
</button>
</div>
</div>
</div>
);
};