Initial commit
This commit is contained in:
325
client/src/pages/Dashboard.jsx
Normal file
325
client/src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,325 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSocket } from '../contexts/SocketContext';
|
||||
import api from '../services/api';
|
||||
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 { recentDetections, deviceStatus, connected } = 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: 'Total Devices',
|
||||
stat: overview?.summary?.total_devices || 0,
|
||||
icon: ServerIcon,
|
||||
change: null,
|
||||
changeType: 'neutral',
|
||||
color: 'bg-blue-500'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Online Devices',
|
||||
stat: overview?.summary?.online_devices || 0,
|
||||
icon: SignalIcon,
|
||||
change: null,
|
||||
changeType: 'positive',
|
||||
color: 'bg-green-500'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Recent Detections',
|
||||
stat: overview?.summary?.recent_detections || 0,
|
||||
icon: ExclamationTriangleIcon,
|
||||
change: null,
|
||||
changeType: 'negative',
|
||||
color: 'bg-red-500'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Unique Drones',
|
||||
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>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
System Overview
|
||||
</h3>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
Reference in New Issue
Block a user