Initial commit
This commit is contained in:
323
client/src/pages/Detections.jsx
Normal file
323
client/src/pages/Detections.jsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import api from '../services/api';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
FunnelIcon,
|
||||
EyeIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const Detections = () => {
|
||||
const [detections, setDetections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pagination, setPagination] = useState({});
|
||||
const [filters, setFilters] = useState({
|
||||
device_id: '',
|
||||
drone_id: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
limit: 50,
|
||||
offset: 0
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDetections();
|
||||
}, [filters]);
|
||||
|
||||
const fetchDetections = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value) params.append(key, value);
|
||||
});
|
||||
|
||||
const response = await api.get(`/detections?${params}`);
|
||||
setDetections(response.data.data);
|
||||
setPagination(response.data.pagination);
|
||||
} catch (error) {
|
||||
console.error('Error fetching detections:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = (key, value) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
offset: 0 // Reset to first page when filtering
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePageChange = (newOffset) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
offset: newOffset
|
||||
}));
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
device_id: '',
|
||||
drone_id: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
limit: 50,
|
||||
offset: 0
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
Drone Detections
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
History of all drone detections from your devices
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="btn btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FunnelIcon className="h-4 w-4" />
|
||||
<span>Filters</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
{showFilters && (
|
||||
<div className="bg-white p-6 rounded-lg shadow border">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Device ID
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Device ID"
|
||||
value={filters.device_id}
|
||||
onChange={(e) => handleFilterChange('device_id', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Drone ID
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Drone ID"
|
||||
value={filters.drone_id}
|
||||
onChange={(e) => handleFilterChange('drone_id', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
value={filters.start_date}
|
||||
onChange={(e) => handleFilterChange('start_date', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
value={filters.end_date}
|
||||
onChange={(e) => handleFilterChange('end_date', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex space-x-2">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detection List */}
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="table-responsive">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Drone ID</th>
|
||||
<th>Type</th>
|
||||
<th>Frequency</th>
|
||||
<th>RSSI</th>
|
||||
<th>Location</th>
|
||||
<th>Detected At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detections.map((detection) => (
|
||||
<tr key={detection.id} className="hover:bg-gray-50">
|
||||
<td>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{detection.device?.name || `Device ${detection.device_id}`}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
ID: {detection.device_id}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className="px-2 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
|
||||
{detection.drone_id}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="text-sm text-gray-900">
|
||||
{detection.drone_type === 0 ? 'Unknown' : `Type ${detection.drone_type}`}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="text-sm text-gray-900">
|
||||
{detection.freq} MHz
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`text-sm font-medium ${
|
||||
detection.rssi > -60 ? 'text-red-600' :
|
||||
detection.rssi > -80 ? 'text-yellow-600' : 'text-green-600'
|
||||
}`}>
|
||||
{detection.rssi} dBm
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-sm text-gray-900">
|
||||
{detection.device?.location_description ||
|
||||
(detection.geo_lat && detection.geo_lon ?
|
||||
`${detection.geo_lat}, ${detection.geo_lon}` :
|
||||
'Unknown')}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-sm text-gray-900">
|
||||
{format(new Date(detection.server_timestamp), 'MMM dd, yyyy')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{format(new Date(detection.server_timestamp), 'HH:mm:ss')}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="text-primary-600 hover:text-primary-900 text-sm"
|
||||
onClick={() => {
|
||||
// TODO: Open detection details modal
|
||||
console.log('View detection details:', detection);
|
||||
}}
|
||||
>
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{detections.length === 0 && !loading && (
|
||||
<div className="text-center py-12">
|
||||
<MagnifyingGlassIcon className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No detections found</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Try adjusting your search filters.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination.total > 0 && (
|
||||
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||
<div className="flex-1 flex justify-between sm:hidden">
|
||||
<button
|
||||
onClick={() => handlePageChange(Math.max(0, filters.offset - filters.limit))}
|
||||
disabled={filters.offset === 0}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(filters.offset + filters.limit)}
|
||||
disabled={filters.offset + filters.limit >= pagination.total}
|
||||
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-700">
|
||||
Showing{' '}
|
||||
<span className="font-medium">{filters.offset + 1}</span>
|
||||
{' '}to{' '}
|
||||
<span className="font-medium">
|
||||
{Math.min(filters.offset + filters.limit, pagination.total)}
|
||||
</span>
|
||||
{' '}of{' '}
|
||||
<span className="font-medium">{pagination.total}</span>
|
||||
{' '}results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
|
||||
<button
|
||||
onClick={() => handlePageChange(Math.max(0, filters.offset - filters.limit))}
|
||||
disabled={filters.offset === 0}
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(filters.offset + filters.limit)}
|
||||
disabled={filters.offset + filters.limit >= pagination.total}
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Detections;
|
||||
Reference in New Issue
Block a user