- Add hardware fingerprinting with cross-platform support - Implement secure device registration flow with X.509 certificates - Add WebSocket real-time communication for device status - Create comprehensive device management dashboard - Establish zero-trust security architecture with multi-layer protection - Add database migrations for device registration entities - Implement Rust edge client with hardware identification - Add certificate management and automated provisioning system 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
235 lines
9.5 KiB
TypeScript
235 lines
9.5 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
||
import { Plus, RefreshCw, Search, Filter } from 'lucide-react'
|
||
import { Button } from '../ui/button'
|
||
import { Input } from '../ui/input'
|
||
import { LoadingSpinner } from '../ui/loading-spinner'
|
||
import { DeviceCard } from './device-card'
|
||
import { DeviceRegistrationWizard } from './device-registration-wizard'
|
||
import { useDeviceRegistration } from '../../contexts/device-registration-context'
|
||
import { DeviceDto, DeviceStatus } from '../../types/device'
|
||
|
||
interface DeviceManagementDashboardProps {
|
||
className?: string
|
||
}
|
||
|
||
export function DeviceManagementDashboard({ className }: DeviceManagementDashboardProps) {
|
||
const { state, refreshDevices, updateDevice, deleteDevice } = useDeviceRegistration()
|
||
const [showRegistrationWizard, setShowRegistrationWizard] = useState(false)
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [statusFilter, setStatusFilter] = useState<DeviceStatus | 'all'>('all')
|
||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||
|
||
// Load devices on mount
|
||
useEffect(() => {
|
||
refreshDevices()
|
||
}, [refreshDevices])
|
||
|
||
const handleRefresh = async () => {
|
||
setIsRefreshing(true)
|
||
try {
|
||
await refreshDevices()
|
||
} finally {
|
||
setIsRefreshing(false)
|
||
}
|
||
}
|
||
|
||
const handleDeleteDevice = async (device: DeviceDto) => {
|
||
if (window.confirm(`确定要删除设备 "${device.deviceName || '此设备'}" 吗?此操作无法撤销。`)) {
|
||
await deleteDevice(device.id)
|
||
}
|
||
}
|
||
|
||
const handleEditDevice = async (device: DeviceDto) => {
|
||
const newName = window.prompt('请输入新的设备名称:', device.deviceName || '')
|
||
if (newName && newName.trim() !== device.deviceName) {
|
||
await updateDevice(device.id, { deviceName: newName.trim() })
|
||
}
|
||
}
|
||
|
||
const handleConfigureDevice = (device: DeviceDto) => {
|
||
// TODO: Implement device configuration dialog
|
||
console.log('Configure device:', device)
|
||
}
|
||
|
||
// Filter devices based on search query and status
|
||
const filteredDevices = state.registeredDevices.filter(device => {
|
||
const matchesSearch = !searchQuery ||
|
||
(device.deviceName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
device.hardwareId.toLowerCase().includes(searchQuery.toLowerCase()))
|
||
|
||
const matchesStatus = statusFilter === 'all' || device.status === statusFilter
|
||
|
||
return matchesSearch && matchesStatus
|
||
})
|
||
|
||
const getDeviceStats = () => {
|
||
const total = state.registeredDevices.length
|
||
const online = state.registeredDevices.filter(d =>
|
||
d.status === DeviceStatus.ONLINE || d.status === DeviceStatus.ACTIVE
|
||
).length
|
||
const offline = state.registeredDevices.filter(d =>
|
||
d.status === DeviceStatus.OFFLINE || d.status === DeviceStatus.INACTIVE
|
||
).length
|
||
const maintenance = state.registeredDevices.filter(d =>
|
||
d.status === DeviceStatus.MAINTENANCE
|
||
).length
|
||
|
||
return { total, online, offline, maintenance }
|
||
}
|
||
|
||
const stats = getDeviceStats()
|
||
|
||
return (
|
||
<div className={`space-y-6 ${className}`}>
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">设备管理</h1>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||
管理您的流星监测设备
|
||
</p>
|
||
</div>
|
||
|
||
<Button
|
||
onClick={() => setShowRegistrationWizard(true)}
|
||
className="flex items-center space-x-2"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>添加设备</span>
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||
<div className="text-2xl font-bold text-blue-600">{stats.total}</div>
|
||
<div className="text-sm text-gray-500 dark:text-gray-400">设备总数</div>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||
<div className="text-2xl font-bold text-green-600">{stats.online}</div>
|
||
<div className="text-sm text-gray-500 dark:text-gray-400">在线</div>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||
<div className="text-2xl font-bold text-red-600">{stats.offline}</div>
|
||
<div className="text-sm text-gray-500 dark:text-gray-400">离线</div>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||
<div className="text-2xl font-bold text-yellow-600">{stats.maintenance}</div>
|
||
<div className="text-sm text-gray-500 dark:text-gray-400">维护中</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 筛选和搜索 - 参考天气页面样式 */}
|
||
<div className="mb-6 flex flex-col md:flex-row md:items-center space-y-2 md:space-y-0 md:space-x-4">
|
||
<div className="flex-1 relative">
|
||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||
<Search size={18} className="text-gray-400" />
|
||
</div>
|
||
<input
|
||
type="text"
|
||
placeholder="搜索设备..."
|
||
className="bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 w-full pl-10 pr-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<Filter size={18} className="text-gray-400" />
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">设备状态:</span>
|
||
<select
|
||
className="bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-md border border-gray-300 dark:border-gray-600 px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm"
|
||
value={statusFilter}
|
||
onChange={(e) => setStatusFilter(e.target.value as DeviceStatus | 'all')}
|
||
>
|
||
<option value="all">全部状态</option>
|
||
<option value={DeviceStatus.ONLINE}>在线</option>
|
||
<option value={DeviceStatus.OFFLINE}>离线</option>
|
||
<option value={DeviceStatus.ACTIVE}>活跃</option>
|
||
<option value={DeviceStatus.INACTIVE}>非活跃</option>
|
||
<option value={DeviceStatus.MAINTENANCE}>维护中</option>
|
||
</select>
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleRefresh}
|
||
disabled={isRefreshing}
|
||
className="flex items-center space-x-2"
|
||
>
|
||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||
<span>刷新</span>
|
||
</Button>
|
||
</div>
|
||
|
||
{/* WebSocket Connection Status */}
|
||
{!state.wsConnected && (
|
||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 p-3 rounded-lg flex items-center space-x-2">
|
||
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
||
<span className="text-sm text-yellow-700 dark:text-yellow-300">
|
||
实时更新不可用,设备状态可能不是最新的。
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Device Grid */}
|
||
{state.isLoading && filteredDevices.length === 0 ? (
|
||
<div className="flex items-center justify-center py-12">
|
||
<LoadingSpinner />
|
||
<span className="ml-2 text-gray-500 dark:text-gray-400">加载设备中...</span>
|
||
</div>
|
||
) : filteredDevices.length === 0 ? (
|
||
<div className="text-center py-12">
|
||
<div className="text-gray-400 dark:text-gray-500 text-lg mb-2">
|
||
{searchQuery || statusFilter !== 'all'
|
||
? '没有符合筛选条件的设备'
|
||
: '尚未注册任何设备'}
|
||
</div>
|
||
{!searchQuery && statusFilter === 'all' && (
|
||
<div className="space-y-4">
|
||
<p className="text-gray-500 dark:text-gray-400">
|
||
注册您的第一台流星监测设备,开始使用。
|
||
</p>
|
||
<Button
|
||
onClick={() => setShowRegistrationWizard(true)}
|
||
className="flex items-center space-x-2"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>注册设备</span>
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{filteredDevices.map((device) => (
|
||
<DeviceCard
|
||
key={device.id}
|
||
device={device}
|
||
onEdit={handleEditDevice}
|
||
onDelete={handleDeleteDevice}
|
||
onConfigure={handleConfigureDevice}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Error Display */}
|
||
{state.error && (
|
||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4 rounded-lg">
|
||
<p className="text-sm text-red-800 dark:text-red-300 font-medium">错误</p>
|
||
<p className="text-sm text-red-600 dark:text-red-400">{state.error}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Registration Wizard */}
|
||
<DeviceRegistrationWizard
|
||
open={showRegistrationWizard}
|
||
onOpenChange={setShowRegistrationWizard}
|
||
/>
|
||
</div>
|
||
)
|
||
} |