grabbit 13ce6ae442 feat: implement complete edge device registration system
- 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>
2025-08-13 08:46:25 +08:00

235 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}