408 lines
15 KiB
JavaScript
408 lines
15 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { useSocket } from '../contexts/SocketContext';
|
|
import MovementAlertsPanel from '../components/MovementAlertsPanel';
|
|
import api from '../services/api';
|
|
import { t } from '../utils/tempTranslations'; // Temporary translation system
|
|
import {
|
|
ServerIcon,
|
|
ExclamationTriangleIcon,
|
|
BellIcon,
|
|
SignalIcon,
|
|
EyeIcon
|
|
} from '@heroicons/react/24/outline';
|
|
import {
|
|
AreaChart,
|
|
Area,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
BarChart,
|
|
Bar,
|
|
PieChart,
|
|
Pie,
|
|
Cell
|
|
} from 'recharts';
|
|
import { format } from 'date-fns';
|
|
|
|
const Dashboard = () => {
|
|
const [overview, setOverview] = useState(null);
|
|
const [chartData, setChartData] = useState([]);
|
|
const [deviceActivity, setDeviceActivity] = useState([]);
|
|
const [recentActivity, setRecentActivity] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showMovementAlerts, setShowMovementAlerts] = useState(true);
|
|
const { recentDetections, deviceStatus, connected, movementAlerts, notificationsEnabled, toggleNotifications } = useSocket();
|
|
|
|
useEffect(() => {
|
|
fetchDashboardData();
|
|
const interval = setInterval(fetchDashboardData, 30000); // Refresh every 30 seconds
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const fetchDashboardData = async () => {
|
|
try {
|
|
const [overviewRes, chartRes, deviceRes, activityRes] = await Promise.all([
|
|
api.get('/dashboard/overview?hours=24'),
|
|
api.get('/dashboard/charts/detections?hours=24&interval=hour'),
|
|
api.get('/dashboard/charts/devices?hours=24'),
|
|
api.get('/dashboard/activity?hours=24&limit=10')
|
|
]);
|
|
|
|
setOverview(overviewRes.data.data);
|
|
setChartData(chartRes.data.data);
|
|
setDeviceActivity(deviceRes.data.data);
|
|
setRecentActivity(activityRes.data.data);
|
|
} catch (error) {
|
|
console.error('Error fetching dashboard data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const stats = [
|
|
{
|
|
id: 1,
|
|
name: t('dashboard.totalDetections'),
|
|
stat: overview?.summary?.total_devices || 0,
|
|
icon: ServerIcon,
|
|
change: null,
|
|
changeType: 'neutral',
|
|
color: 'bg-blue-500'
|
|
},
|
|
{
|
|
id: 2,
|
|
name: t('dashboard.connectedDevices'),
|
|
stat: overview?.summary?.online_devices || 0,
|
|
icon: SignalIcon,
|
|
change: null,
|
|
changeType: 'positive',
|
|
color: 'bg-green-500'
|
|
},
|
|
{
|
|
id: 3,
|
|
name: t('dashboard.recentDetections'),
|
|
stat: overview?.summary?.recent_detections || 0,
|
|
icon: ExclamationTriangleIcon,
|
|
change: null,
|
|
changeType: 'negative',
|
|
color: 'bg-red-500'
|
|
},
|
|
{
|
|
id: 4,
|
|
name: t('dashboard.activeAlerts'),
|
|
stat: overview?.summary?.unique_drones_detected || 0,
|
|
icon: EyeIcon,
|
|
change: null,
|
|
changeType: 'neutral',
|
|
color: 'bg-purple-500'
|
|
}
|
|
];
|
|
|
|
const deviceStatusData = [
|
|
{ name: 'Online', value: overview?.device_status?.online || 0, color: '#22c55e' },
|
|
{ name: 'Offline', value: overview?.device_status?.offline || 0, color: '#ef4444' },
|
|
{ name: 'Inactive', value: overview?.device_status?.inactive || 0, color: '#6b7280' }
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Stats */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
{t('dashboard.title')}
|
|
</h3>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={toggleNotifications}
|
|
className={`inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md transition-colors ${
|
|
notificationsEnabled
|
|
? 'text-gray-700 bg-gray-100 hover:bg-gray-200'
|
|
: 'text-red-700 bg-red-100 hover:bg-red-200'
|
|
}`}
|
|
>
|
|
<BellIcon className={`h-4 w-4 mr-2 ${notificationsEnabled ? '' : 'line-through'}`} />
|
|
{notificationsEnabled ? 'Notifications On' : 'Notifications Off'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
|
{stats.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="relative bg-white pt-5 px-4 pb-12 sm:pt-6 sm:px-6 shadow rounded-lg overflow-hidden"
|
|
>
|
|
<dt>
|
|
<div className={`absolute ${item.color} rounded-md p-3`}>
|
|
<item.icon className="h-6 w-6 text-white" aria-hidden="true" />
|
|
</div>
|
|
<p className="ml-16 text-sm font-medium text-gray-500 truncate">
|
|
{item.name}
|
|
</p>
|
|
</dt>
|
|
<dd className="ml-16 pb-6 flex items-baseline sm:pb-7">
|
|
<p className="text-2xl font-semibold text-gray-900">
|
|
{item.stat}
|
|
</p>
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
|
|
{/* Charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Detection Timeline */}
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
|
Detections Timeline (24h)
|
|
</h3>
|
|
<div className="h-64">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="timestamp"
|
|
tickFormatter={(value) => format(new Date(value), 'HH:mm')}
|
|
/>
|
|
<YAxis />
|
|
<Tooltip
|
|
labelFormatter={(value) => format(new Date(value), 'MMM dd, HH:mm')}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="count"
|
|
stroke="#ef4444"
|
|
fill="#ef4444"
|
|
fillOpacity={0.3}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Device Status */}
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
|
Device Status
|
|
</h3>
|
|
<div className="h-64">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={deviceStatusData}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={60}
|
|
outerRadius={80}
|
|
paddingAngle={5}
|
|
dataKey="value"
|
|
>
|
|
{deviceStatusData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
<div className="mt-4 flex justify-center space-x-4">
|
|
{deviceStatusData.map((item, index) => (
|
|
<div key={index} className="flex items-center">
|
|
<div
|
|
className="w-3 h-3 rounded-full mr-2"
|
|
style={{ backgroundColor: item.color }}
|
|
/>
|
|
<span className="text-sm text-gray-600">
|
|
{item.name}: {item.value}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Device Activity */}
|
|
{deviceActivity.length > 0 && (
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
|
Device Activity (24h)
|
|
</h3>
|
|
<div className="h-64">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={deviceActivity}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="device_name"
|
|
angle={-45}
|
|
textAnchor="end"
|
|
height={60}
|
|
/>
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Bar dataKey="detection_count" fill="#3b82f6" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent Activity & Real-time Detections */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Recent Activity */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h3 className="text-lg font-medium text-gray-900">Recent Activity</h3>
|
|
</div>
|
|
<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
|
|
{recentActivity.map((activity, index) => (
|
|
<div key={index} className="px-6 py-4">
|
|
<div className="flex items-center space-x-3">
|
|
<div className={`flex-shrink-0 w-2 h-2 rounded-full ${
|
|
activity.type === 'detection' ? 'bg-red-400' : 'bg-green-400'
|
|
}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-gray-900">
|
|
{activity.type === 'detection' ? (
|
|
<>Drone {activity.data.drone_id} detected by {activity.data.device_name}</>
|
|
) : (
|
|
<>Heartbeat from {activity.data.device_name}</>
|
|
)}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{format(new Date(activity.timestamp), 'MMM dd, HH:mm:ss')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{recentActivity.length === 0 && (
|
|
<div className="px-6 py-8 text-center text-gray-500">
|
|
No recent activity
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Real-time Detections */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 className="text-lg font-medium text-gray-900">Live Detections</h3>
|
|
<div className={`flex items-center space-x-2 px-2 py-1 rounded-full text-xs ${
|
|
connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
}`}>
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
connected ? 'bg-green-400' : 'bg-red-400'
|
|
}`} />
|
|
<span>{connected ? 'Live' : 'Disconnected'}</span>
|
|
</div>
|
|
</div>
|
|
<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
|
|
{recentDetections.map((detection, index) => (
|
|
<div key={index} className="px-6 py-4 animate-fade-in">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="flex-shrink-0 w-2 h-2 rounded-full bg-red-400 animate-pulse" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-gray-900">
|
|
Drone {detection.drone_id} detected
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{detection.device.name || `Device ${detection.device_id}`} •
|
|
RSSI: {detection.rssi}dBm •
|
|
Freq: {detection.freq}MHz
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{format(new Date(detection.server_timestamp), 'HH:mm:ss')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{recentDetections.length === 0 && (
|
|
<div className="px-6 py-8 text-center text-gray-500">
|
|
No recent detections
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Movement Alerts Panel */}
|
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
|
<div className="xl:col-span-2">
|
|
<MovementAlertsPanel />
|
|
</div>
|
|
|
|
{/* Movement Summary Stats */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
|
Movement Tracking
|
|
</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-3 bg-red-50 rounded-lg">
|
|
<div>
|
|
<div className="font-medium text-red-900">Critical Alerts</div>
|
|
<div className="text-sm text-red-700">Very close approaches</div>
|
|
</div>
|
|
<div className="text-2xl font-bold text-red-600">
|
|
{movementAlerts.filter(a => a.analysis.alertLevel >= 3).length}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-3 bg-orange-50 rounded-lg">
|
|
<div>
|
|
<div className="font-medium text-orange-900">High Priority</div>
|
|
<div className="text-sm text-orange-700">Approaching drones</div>
|
|
</div>
|
|
<div className="text-2xl font-bold text-orange-600">
|
|
{movementAlerts.filter(a => a.analysis.alertLevel === 2).length}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
|
<div>
|
|
<div className="font-medium text-blue-900">Medium Priority</div>
|
|
<div className="text-sm text-blue-700">Movement changes</div>
|
|
</div>
|
|
<div className="text-2xl font-bold text-blue-600">
|
|
{movementAlerts.filter(a => a.analysis.alertLevel === 1).length}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-gray-200">
|
|
<div className="text-sm text-gray-600">
|
|
<div className="flex justify-between">
|
|
<span>Total Tracked:</span>
|
|
<span className="font-medium">{movementAlerts.length} events</span>
|
|
</div>
|
|
<div className="flex justify-between mt-1">
|
|
<span>Last Alert:</span>
|
|
<span className="font-medium">
|
|
{movementAlerts.length > 0
|
|
? format(new Date(movementAlerts[0].timestamp), 'HH:mm:ss')
|
|
: 'None'
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard;
|