Fix jwt-token
This commit is contained in:
407
client/src/components/AlertModals.jsx
Normal file
407
client/src/components/AlertModals.jsx
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
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: '',
|
||||||
|
sms_phone_number: '',
|
||||||
|
webhook_url: ''
|
||||||
|
});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
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 || '',
|
||||||
|
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 handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.put(`/alerts/rules/${rule.id}`, formData);
|
||||||
|
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 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>{format(new Date(detection.detected_at), 'MMM dd, yyyy HH:mm:ss')}</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
ExclamationTriangleIcon
|
ExclamationTriangleIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { EditAlertModal, DetectionDetailsModal } from '../components/AlertModals';
|
||||||
|
|
||||||
const Alerts = () => {
|
const Alerts = () => {
|
||||||
const [alertRules, setAlertRules] = useState([]);
|
const [alertRules, setAlertRules] = useState([]);
|
||||||
@@ -388,6 +389,27 @@ const Alerts = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showEditModal && editingRule && (
|
||||||
|
<EditAlertModal
|
||||||
|
rule={editingRule}
|
||||||
|
onClose={() => {
|
||||||
|
setShowEditModal(false);
|
||||||
|
setEditingRule(null);
|
||||||
|
}}
|
||||||
|
onSuccess={fetchAlertData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDetectionModal && selectedDetection && (
|
||||||
|
<DetectionDetailsModal
|
||||||
|
detection={selectedDetection}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDetectionModal(false);
|
||||||
|
setSelectedDetection(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user