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:
162
src/components/dashboard/AlertsFeed.tsx
Normal file
162
src/components/dashboard/AlertsFeed.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
'use client'
|
||||
|
||||
import { AlertTriangle, CheckCircle, Info, Clock } from 'lucide-react'
|
||||
import { cn, formatRelativeTime } from '@/lib/utils'
|
||||
|
||||
interface Alert {
|
||||
id: string
|
||||
severidad: 'INFO' | 'WARNING' | 'CRITICAL'
|
||||
estado: 'ACTIVA' | 'RECONOCIDA' | 'RESUELTA'
|
||||
titulo: string
|
||||
mensaje: string
|
||||
createdAt: Date
|
||||
dispositivo?: { nombre: string } | null
|
||||
cliente: { nombre: string }
|
||||
}
|
||||
|
||||
interface AlertsFeedProps {
|
||||
alerts: Alert[]
|
||||
onAcknowledge?: (alertId: string) => void
|
||||
onResolve?: (alertId: string) => void
|
||||
maxItems?: number
|
||||
}
|
||||
|
||||
export default function AlertsFeed({
|
||||
alerts,
|
||||
onAcknowledge,
|
||||
onResolve,
|
||||
maxItems = 10,
|
||||
}: AlertsFeedProps) {
|
||||
const displayAlerts = alerts.slice(0, maxItems)
|
||||
|
||||
if (displayAlerts.length === 0) {
|
||||
return (
|
||||
<div className="card p-8 text-center">
|
||||
<CheckCircle className="w-12 h-12 text-success mx-auto mb-3" />
|
||||
<p className="text-gray-400">No hay alertas activas</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card overflow-hidden">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="font-medium">Alertas Recientes</h3>
|
||||
<a href="/alertas" className="text-sm text-primary-500 hover:underline">
|
||||
Ver todas
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-dark-100">
|
||||
{displayAlerts.map((alert) => (
|
||||
<AlertItem
|
||||
key={alert.id}
|
||||
alert={alert}
|
||||
onAcknowledge={onAcknowledge}
|
||||
onResolve={onResolve}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertItem({
|
||||
alert,
|
||||
onAcknowledge,
|
||||
onResolve,
|
||||
}: {
|
||||
alert: Alert
|
||||
onAcknowledge?: (alertId: string) => void
|
||||
onResolve?: (alertId: string) => void
|
||||
}) {
|
||||
const severityConfig = {
|
||||
CRITICAL: {
|
||||
icon: <AlertTriangle className="w-5 h-5" />,
|
||||
color: 'text-danger',
|
||||
bgColor: 'bg-danger/20',
|
||||
borderColor: 'border-l-danger',
|
||||
},
|
||||
WARNING: {
|
||||
icon: <AlertTriangle className="w-5 h-5" />,
|
||||
color: 'text-warning',
|
||||
bgColor: 'bg-warning/20',
|
||||
borderColor: 'border-l-warning',
|
||||
},
|
||||
INFO: {
|
||||
icon: <Info className="w-5 h-5" />,
|
||||
color: 'text-info',
|
||||
bgColor: 'bg-info/20',
|
||||
borderColor: 'border-l-info',
|
||||
},
|
||||
}
|
||||
|
||||
const config = severityConfig[alert.severidad]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'p-4 border-l-4 hover:bg-dark-300/30 transition-colors',
|
||||
config.borderColor,
|
||||
alert.severidad === 'CRITICAL' && 'animate-pulse-slow'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={cn('p-2 rounded-lg', config.bgColor)}>
|
||||
<span className={config.color}>{config.icon}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm">{alert.titulo}</h4>
|
||||
<p className="text-xs text-gray-400 mt-0.5">{alert.mensaje}</p>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'badge shrink-0',
|
||||
alert.estado === 'ACTIVA' && 'badge-danger',
|
||||
alert.estado === 'RECONOCIDA' && 'badge-warning',
|
||||
alert.estado === 'RESUELTA' && 'badge-success'
|
||||
)}
|
||||
>
|
||||
{alert.estado}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<Clock className="w-3 h-3" />
|
||||
{formatRelativeTime(alert.createdAt)}
|
||||
</div>
|
||||
{alert.dispositivo && (
|
||||
<span className="text-xs text-gray-500">
|
||||
{alert.dispositivo.nombre}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-600">
|
||||
{alert.cliente.nombre}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{alert.estado === 'ACTIVA' && (
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button
|
||||
onClick={() => onAcknowledge?.(alert.id)}
|
||||
className="btn btn-ghost btn-sm"
|
||||
>
|
||||
Reconocer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onResolve?.(alert.id)}
|
||||
className="btn btn-ghost btn-sm text-success"
|
||||
>
|
||||
Resolver
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user