Initial commit: MSP Monitor Dashboard
- Next.js 14 frontend with dark cyan/navy theme - tRPC API with Prisma ORM - MeshCentral, LibreNMS, Headwind MDM integrations - Multi-tenant architecture - Alert system with email/SMS/webhook notifications - Docker Compose deployment - Complete documentation
This commit is contained in:
253
src/app/(dashboard)/page.tsx
Normal file
253
src/app/(dashboard)/page.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { RefreshCw, Grid, List, Filter } from 'lucide-react'
|
||||
import KPICards from '@/components/dashboard/KPICards'
|
||||
import DeviceGrid from '@/components/dashboard/DeviceGrid'
|
||||
import AlertsFeed from '@/components/dashboard/AlertsFeed'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// Mock data - en produccion vendria de la API
|
||||
const mockStats = {
|
||||
totalDispositivos: 127,
|
||||
dispositivosOnline: 98,
|
||||
dispositivosOffline: 24,
|
||||
dispositivosAlerta: 5,
|
||||
alertasActivas: 8,
|
||||
alertasCriticas: 2,
|
||||
sesionesActivas: 3,
|
||||
}
|
||||
|
||||
const mockDevices = [
|
||||
{
|
||||
id: '1',
|
||||
nombre: 'SRV-PRINCIPAL',
|
||||
tipo: 'SERVIDOR',
|
||||
estado: 'ONLINE',
|
||||
ip: '192.168.1.10',
|
||||
sistemaOperativo: 'Windows Server 2022',
|
||||
lastSeen: new Date(),
|
||||
cpuUsage: 45,
|
||||
ramUsage: 72,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
nombre: 'PC-ADMIN-01',
|
||||
tipo: 'PC',
|
||||
estado: 'ONLINE',
|
||||
ip: '192.168.1.101',
|
||||
sistemaOperativo: 'Windows 11 Pro',
|
||||
lastSeen: new Date(),
|
||||
cpuUsage: 23,
|
||||
ramUsage: 56,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
nombre: 'LAPTOP-VENTAS',
|
||||
tipo: 'LAPTOP',
|
||||
estado: 'ALERTA',
|
||||
ip: '192.168.1.105',
|
||||
sistemaOperativo: 'Windows 11 Pro',
|
||||
lastSeen: new Date(Date.now() - 1000 * 60 * 5),
|
||||
cpuUsage: 95,
|
||||
ramUsage: 88,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
nombre: 'ROUTER-PRINCIPAL',
|
||||
tipo: 'ROUTER',
|
||||
estado: 'ONLINE',
|
||||
ip: '192.168.1.1',
|
||||
sistemaOperativo: 'RouterOS 7.12',
|
||||
lastSeen: new Date(),
|
||||
cpuUsage: null,
|
||||
ramUsage: null,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
nombre: 'SW-CORE-01',
|
||||
tipo: 'SWITCH',
|
||||
estado: 'ONLINE',
|
||||
ip: '192.168.1.2',
|
||||
sistemaOperativo: 'Cisco IOS',
|
||||
lastSeen: new Date(),
|
||||
cpuUsage: null,
|
||||
ramUsage: null,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
nombre: 'CELULAR-GERENTE',
|
||||
tipo: 'CELULAR',
|
||||
estado: 'ONLINE',
|
||||
ip: null,
|
||||
sistemaOperativo: 'Android 14',
|
||||
lastSeen: new Date(),
|
||||
cpuUsage: null,
|
||||
ramUsage: null,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
nombre: 'SRV-BACKUP',
|
||||
tipo: 'SERVIDOR',
|
||||
estado: 'OFFLINE',
|
||||
ip: '192.168.1.11',
|
||||
sistemaOperativo: 'Ubuntu 22.04',
|
||||
lastSeen: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
||||
cpuUsage: null,
|
||||
ramUsage: null,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
nombre: 'AP-OFICINA-01',
|
||||
tipo: 'AP',
|
||||
estado: 'ONLINE',
|
||||
ip: '192.168.1.50',
|
||||
sistemaOperativo: 'UniFi AP',
|
||||
lastSeen: new Date(),
|
||||
cpuUsage: null,
|
||||
ramUsage: null,
|
||||
},
|
||||
]
|
||||
|
||||
const mockAlerts = [
|
||||
{
|
||||
id: '1',
|
||||
severidad: 'CRITICAL' as const,
|
||||
estado: 'ACTIVA' as const,
|
||||
titulo: 'Servidor de backup offline',
|
||||
mensaje: 'El servidor SRV-BACKUP no responde desde hace 2 horas',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 120),
|
||||
dispositivo: { nombre: 'SRV-BACKUP' },
|
||||
cliente: { nombre: 'Cliente A' },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
severidad: 'WARNING' as const,
|
||||
estado: 'ACTIVA' as const,
|
||||
titulo: 'CPU alta',
|
||||
mensaje: 'Uso de CPU al 95% en LAPTOP-VENTAS',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 15),
|
||||
dispositivo: { nombre: 'LAPTOP-VENTAS' },
|
||||
cliente: { nombre: 'Cliente A' },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
severidad: 'INFO' as const,
|
||||
estado: 'RECONOCIDA' as const,
|
||||
titulo: 'Actualizacion disponible',
|
||||
mensaje: 'Windows Update pendiente en PC-ADMIN-01',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60),
|
||||
dispositivo: { nombre: 'PC-ADMIN-01' },
|
||||
cliente: { nombre: 'Cliente A' },
|
||||
},
|
||||
]
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [stats, setStats] = useState(mockStats)
|
||||
const [devices, setDevices] = useState(mockDevices)
|
||||
const [alerts, setAlerts] = useState(mockAlerts)
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true)
|
||||
// TODO: Recargar datos de la API
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
|
||||
const handleDeviceAction = (deviceId: string, action: string) => {
|
||||
console.log(`Action ${action} on device ${deviceId}`)
|
||||
// TODO: Implementar acciones
|
||||
}
|
||||
|
||||
const handleAcknowledgeAlert = (alertId: string) => {
|
||||
setAlerts((prev) =>
|
||||
prev.map((a) => (a.id === alertId ? { ...a, estado: 'RECONOCIDA' as const } : a))
|
||||
)
|
||||
// TODO: Llamar API
|
||||
}
|
||||
|
||||
const handleResolveAlert = (alertId: string) => {
|
||||
setAlerts((prev) =>
|
||||
prev.map((a) => (a.id === alertId ? { ...a, estado: 'RESUELTA' as const } : a))
|
||||
)
|
||||
// TODO: Llamar API
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<p className="text-gray-500">Vision general del sistema</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="btn btn-secondary"
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={cn('w-4 h-4 mr-2', isRefreshing && 'animate-spin')} />
|
||||
Actualizar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<KPICards stats={stats} />
|
||||
|
||||
{/* Main content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Devices */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-medium">Dispositivos</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn btn-ghost btn-sm">
|
||||
<Filter className="w-4 h-4 mr-1" />
|
||||
Filtrar
|
||||
</button>
|
||||
<div className="flex border border-dark-100 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={cn(
|
||||
'p-2 transition-colors',
|
||||
viewMode === 'grid' ? 'bg-dark-100 text-primary-400' : 'text-gray-500'
|
||||
)}
|
||||
>
|
||||
<Grid className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={cn(
|
||||
'p-2 transition-colors',
|
||||
viewMode === 'list' ? 'bg-dark-100 text-primary-400' : 'text-gray-500'
|
||||
)}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeviceGrid
|
||||
devices={devices}
|
||||
viewMode={viewMode}
|
||||
onAction={handleDeviceAction}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Alerts */}
|
||||
<div>
|
||||
<AlertsFeed
|
||||
alerts={alerts}
|
||||
onAcknowledge={handleAcknowledgeAlert}
|
||||
onResolve={handleResolveAlert}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user