329 lines
11 KiB
TypeScript
329 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import Link from 'next/link'
|
|
import {
|
|
Monitor,
|
|
Laptop,
|
|
Server,
|
|
Smartphone,
|
|
Tablet,
|
|
Router,
|
|
Network,
|
|
Shield,
|
|
Wifi,
|
|
Printer,
|
|
HelpCircle,
|
|
MoreVertical,
|
|
ExternalLink,
|
|
Power,
|
|
Terminal,
|
|
FolderOpen,
|
|
} from 'lucide-react'
|
|
import { cn, formatRelativeTime, getStatusColor, getStatusBgColor, getStatusBorderColor } from '@/lib/utils'
|
|
|
|
interface Device {
|
|
id: string
|
|
nombre: string
|
|
tipo: string
|
|
estado: string
|
|
ip?: string | null
|
|
sistemaOperativo?: string | null
|
|
lastSeen?: Date | null
|
|
cpuUsage?: number | null
|
|
ramUsage?: number | null
|
|
cliente?: { nombre: string }
|
|
}
|
|
|
|
interface DeviceGridProps {
|
|
devices: Device[]
|
|
viewMode?: 'grid' | 'list'
|
|
onAction?: (deviceId: string, action: string) => void
|
|
}
|
|
|
|
const deviceIcons: Record<string, React.ReactNode> = {
|
|
PC: <Monitor className="w-8 h-8" />,
|
|
LAPTOP: <Laptop className="w-8 h-8" />,
|
|
SERVIDOR: <Server className="w-8 h-8" />,
|
|
CELULAR: <Smartphone className="w-8 h-8" />,
|
|
TABLET: <Tablet className="w-8 h-8" />,
|
|
ROUTER: <Router className="w-8 h-8" />,
|
|
SWITCH: <Network className="w-8 h-8" />,
|
|
FIREWALL: <Shield className="w-8 h-8" />,
|
|
AP: <Wifi className="w-8 h-8" />,
|
|
IMPRESORA: <Printer className="w-8 h-8" />,
|
|
OTRO: <HelpCircle className="w-8 h-8" />,
|
|
}
|
|
|
|
export default function DeviceGrid({ devices, viewMode = 'grid', onAction }: DeviceGridProps) {
|
|
if (viewMode === 'list') {
|
|
return <DeviceList devices={devices} onAction={onAction} />
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{devices.map((device) => (
|
|
<DeviceCard key={device.id} device={device} onAction={onAction} />
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DeviceCard({
|
|
device,
|
|
onAction,
|
|
}: {
|
|
device: Device
|
|
onAction?: (deviceId: string, action: string) => void
|
|
}) {
|
|
const [showMenu, setShowMenu] = useState(false)
|
|
|
|
const getDeviceUrl = () => {
|
|
const type = device.tipo
|
|
if (['PC', 'LAPTOP', 'SERVIDOR'].includes(type)) return `/devices/${device.id}`
|
|
if (['CELULAR', 'TABLET'].includes(type)) return `/celulares/${device.id}`
|
|
return `/red/${device.id}`
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'card p-4 transition-all hover:border-primary-500/50 relative group border',
|
|
getStatusBorderColor(device.estado)
|
|
)}
|
|
>
|
|
<div className="absolute top-3 right-3 z-10">
|
|
<button
|
|
onClick={() => setShowMenu(!showMenu)}
|
|
className="p-1.5 rounded hover:bg-dark-100 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity touch-manipulation"
|
|
>
|
|
<MoreVertical className="w-4 h-4 text-gray-500" />
|
|
</button>
|
|
|
|
{showMenu && (
|
|
<>
|
|
<div className="fixed inset-0 z-40" onClick={() => setShowMenu(false)} />
|
|
<div className="dropdown right-0 z-50">
|
|
{['PC', 'LAPTOP', 'SERVIDOR'].includes(device.tipo) && device.estado === 'ONLINE' && (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
onAction?.(device.id, 'desktop')
|
|
setShowMenu(false)
|
|
}}
|
|
className="dropdown-item flex items-center gap-2"
|
|
>
|
|
<ExternalLink className="w-4 h-4" />
|
|
Escritorio remoto
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
onAction?.(device.id, 'terminal')
|
|
setShowMenu(false)
|
|
}}
|
|
className="dropdown-item flex items-center gap-2"
|
|
>
|
|
<Terminal className="w-4 h-4" />
|
|
Terminal
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
onAction?.(device.id, 'files')
|
|
setShowMenu(false)
|
|
}}
|
|
className="dropdown-item flex items-center gap-2"
|
|
>
|
|
<FolderOpen className="w-4 h-4" />
|
|
Archivos
|
|
</button>
|
|
<div className="h-px bg-dark-100 my-1" />
|
|
</>
|
|
)}
|
|
<button
|
|
onClick={() => {
|
|
onAction?.(device.id, 'restart')
|
|
setShowMenu(false)
|
|
}}
|
|
className="dropdown-item flex items-center gap-2 text-warning"
|
|
>
|
|
<Power className="w-4 h-4" />
|
|
Reiniciar
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<Link href={getDeviceUrl()} className="block">
|
|
<div className="flex items-center gap-4 mb-3">
|
|
<div className={cn('p-3 rounded-lg shrink-0', getStatusBgColor(device.estado))}>
|
|
<span className={getStatusColor(device.estado)}>
|
|
{deviceIcons[device.tipo] || deviceIcons.OTRO}
|
|
</span>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium truncate">{device.nombre}</h3>
|
|
<p className="text-xs text-gray-500">{device.tipo}</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
|
|
{/* Details */}
|
|
<div className="space-y-2 text-sm">
|
|
{device.ip && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">IP</span>
|
|
<span className="font-mono text-gray-300">{device.ip}</span>
|
|
</div>
|
|
)}
|
|
{device.sistemaOperativo && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">OS</span>
|
|
<span className="text-gray-300 truncate ml-2">{device.sistemaOperativo}</span>
|
|
</div>
|
|
)}
|
|
{device.lastSeen && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Visto</span>
|
|
<span className="text-gray-400">{formatRelativeTime(device.lastSeen)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Metrics bar */}
|
|
{device.estado === 'ONLINE' && (device.cpuUsage != null || device.ramUsage != null) && (
|
|
<div className="mt-3 pt-3 border-t border-dark-100 grid grid-cols-2 gap-2">
|
|
{device.cpuUsage != null && (
|
|
<div>
|
|
<div className="flex justify-between text-xs mb-1">
|
|
<span className="text-gray-500">CPU</span>
|
|
<span className={cn(device.cpuUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
|
{Math.round(device.cpuUsage)}%
|
|
</span>
|
|
</div>
|
|
<div className="h-1 bg-dark-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={cn(
|
|
'h-full rounded-full transition-all',
|
|
device.cpuUsage > 80 ? 'bg-danger' : device.cpuUsage > 60 ? 'bg-warning' : 'bg-success'
|
|
)}
|
|
style={{ width: `${device.cpuUsage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{device.ramUsage != null && (
|
|
<div>
|
|
<div className="flex justify-between text-xs mb-1">
|
|
<span className="text-gray-500">RAM</span>
|
|
<span className={cn(device.ramUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
|
{Math.round(device.ramUsage)}%
|
|
</span>
|
|
</div>
|
|
<div className="h-1 bg-dark-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={cn(
|
|
'h-full rounded-full transition-all',
|
|
device.ramUsage > 80 ? 'bg-danger' : device.ramUsage > 60 ? 'bg-warning' : 'bg-success'
|
|
)}
|
|
style={{ width: `${device.ramUsage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DeviceList({
|
|
devices,
|
|
onAction,
|
|
}: {
|
|
devices: Device[]
|
|
onAction?: (deviceId: string, action: string) => void
|
|
}) {
|
|
return (
|
|
<div className="card overflow-hidden">
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Dispositivo</th>
|
|
<th>Tipo</th>
|
|
<th>IP</th>
|
|
<th>Estado</th>
|
|
<th>CPU</th>
|
|
<th>RAM</th>
|
|
<th>Ultimo contacto</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{devices.map((device) => (
|
|
<tr key={device.id}>
|
|
<td>
|
|
<div className="flex items-center gap-3">
|
|
<span className={getStatusColor(device.estado)}>
|
|
{deviceIcons[device.tipo] || deviceIcons.OTRO}
|
|
</span>
|
|
<div>
|
|
<div className="font-medium">{device.nombre}</div>
|
|
{device.cliente && (
|
|
<div className="text-xs text-gray-500">{device.cliente.nombre}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="text-gray-400">{device.tipo}</td>
|
|
<td className="font-mono text-gray-400">{device.ip || '-'}</td>
|
|
<td>
|
|
<span
|
|
className={cn(
|
|
'badge',
|
|
device.estado === 'ONLINE' && 'badge-success',
|
|
device.estado === 'OFFLINE' && 'badge-gray',
|
|
device.estado === 'ALERTA' && 'badge-danger',
|
|
device.estado === 'MANTENIMIENTO' && 'badge-warning'
|
|
)}
|
|
>
|
|
{device.estado}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{device.cpuUsage != null ? (
|
|
<span className={cn(device.cpuUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
|
{Math.round(device.cpuUsage)}%
|
|
</span>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</td>
|
|
<td>
|
|
{device.ramUsage != null ? (
|
|
<span className={cn(device.ramUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
|
{Math.round(device.ramUsage)}%
|
|
</span>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</td>
|
|
<td className="text-gray-500">
|
|
{device.lastSeen ? formatRelativeTime(device.lastSeen) : '-'}
|
|
</td>
|
|
<td>
|
|
<Link
|
|
href={`/devices/${device.id}`}
|
|
className="btn btn-ghost btn-sm"
|
|
>
|
|
Ver
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)
|
|
}
|