194 lines
7.3 KiB
TypeScript
194 lines
7.3 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import {
|
|
AlertCircle,
|
|
AlertTriangle,
|
|
Zap,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Minus,
|
|
RefreshCw,
|
|
Loader2,
|
|
Building2,
|
|
Server as ServerIcon,
|
|
ArrowRight,
|
|
} from 'lucide-react'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { useAllClientsErrorSummary } from '@/hooks/use-enterprise-clients'
|
|
|
|
const TREND_CONFIG = {
|
|
increasing: {
|
|
icon: TrendingUp,
|
|
color: 'text-red-600',
|
|
bg: 'bg-red-50',
|
|
label: 'increasing',
|
|
},
|
|
decreasing: {
|
|
icon: TrendingDown,
|
|
color: 'text-green-600',
|
|
bg: 'bg-green-50',
|
|
label: 'decreasing',
|
|
},
|
|
stable: {
|
|
icon: Minus,
|
|
color: 'text-slate-600',
|
|
bg: 'bg-slate-50',
|
|
label: 'stable',
|
|
},
|
|
}
|
|
|
|
export function EnterpriseErrorSummaryWidget() {
|
|
const { data, isLoading, isError, refetch, isFetching } = useAllClientsErrorSummary()
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center h-48">
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
if (isError || !data) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center h-48 gap-2">
|
|
<AlertCircle className="h-8 w-8 text-destructive" />
|
|
<p className="text-sm text-muted-foreground">Failed to load error summary</p>
|
|
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
|
Retry
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const { totals, clients } = data
|
|
const hasCriticalIssues = totals.criticalErrors24h > 0 || totals.crashes24h > 0
|
|
const clientsWithIssues = clients.filter(
|
|
(c) => c.criticalErrors24h > 0 || c.crashes24h > 0
|
|
).slice(0, 3)
|
|
|
|
const trendConfig = TREND_CONFIG[totals.overallTrend]
|
|
const TrendIcon = trendConfig.icon
|
|
|
|
return (
|
|
<Card className={hasCriticalIssues ? 'border-red-200 dark:border-red-900' : ''}>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<div className={`p-2 rounded-lg ${hasCriticalIssues ? 'bg-red-100 dark:bg-red-900/30' : 'bg-slate-100 dark:bg-slate-800'}`}>
|
|
<AlertTriangle className={`h-4 w-4 ${hasCriticalIssues ? 'text-red-600 dark:text-red-400' : 'text-slate-600 dark:text-slate-400'}`} />
|
|
</div>
|
|
<CardTitle className="text-base font-semibold">System Health</CardTitle>
|
|
{hasCriticalIssues && (
|
|
<span className="relative flex h-2 w-2">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => refetch()}
|
|
disabled={isFetching}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Main Stats */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<p className="text-xs text-muted-foreground">Critical Errors (24h)</p>
|
|
<div className="flex items-center gap-2">
|
|
<Zap className={`h-4 w-4 ${totals.criticalErrors24h > 0 ? 'text-red-600' : 'text-muted-foreground'}`} />
|
|
<span className={`text-2xl font-bold tabular-nums ${totals.criticalErrors24h > 0 ? 'text-red-600' : ''}`}>
|
|
{totals.criticalErrors24h}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-xs text-muted-foreground">Crashed Containers (24h)</p>
|
|
<div className="flex items-center gap-2">
|
|
<ServerIcon className={`h-4 w-4 ${totals.crashes24h > 0 ? 'text-orange-600' : 'text-muted-foreground'}`} />
|
|
<span className={`text-2xl font-bold tabular-nums ${totals.crashes24h > 0 ? 'text-orange-600' : ''}`}>
|
|
{totals.crashes24h}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trend */}
|
|
<div className={`flex items-center gap-2 p-2 rounded-lg ${trendConfig.bg}`}>
|
|
<TrendIcon className={`h-4 w-4 ${trendConfig.color}`} />
|
|
<span className={`text-sm ${trendConfig.color}`}>
|
|
Error trend: <span className="font-medium">{trendConfig.label}</span>
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
({totals.totalErrors24h} total errors today)
|
|
</span>
|
|
</div>
|
|
|
|
{/* Clients with Issues */}
|
|
{clientsWithIssues.length > 0 && (
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-muted-foreground font-medium">
|
|
Clients with Issues ({totals.clientsWithIssues} total)
|
|
</p>
|
|
<div className="space-y-2">
|
|
{clientsWithIssues.map((client) => (
|
|
<Link
|
|
key={client.clientId}
|
|
href={`/admin/enterprise-clients/${client.clientId}`}
|
|
className="flex items-center justify-between p-2 rounded-lg border hover:bg-muted/50 transition-colors group"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">{client.clientName}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{client.criticalErrors24h > 0 && (
|
|
<span className="inline-flex items-center gap-1 text-xs bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 px-2 py-0.5 rounded">
|
|
<Zap className="h-3 w-3" />
|
|
{client.criticalErrors24h} critical
|
|
</span>
|
|
)}
|
|
{client.crashes24h > 0 && (
|
|
<span className="inline-flex items-center gap-1 text-xs bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400 px-2 py-0.5 rounded">
|
|
<AlertCircle className="h-3 w-3" />
|
|
{client.crashes24h} crashed
|
|
</span>
|
|
)}
|
|
<ArrowRight className="h-4 w-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* No Issues State */}
|
|
{clientsWithIssues.length === 0 && totals.totalClients > 0 && (
|
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
|
<span className="inline-flex items-center gap-2">
|
|
All {totals.totalClients} clients are healthy
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* No Clients State */}
|
|
{totals.totalClients === 0 && (
|
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
|
No enterprise clients configured
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|