212 lines
11 KiB
TypeScript
212 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { monitorAPI } from '@/lib/api'
|
|
import { DashboardLayout } from '@/components/layout/dashboard-layout'
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
type TimeRange = '7d' | '30d' | '90d' | 'all'
|
|
|
|
export default function AnalyticsPage() {
|
|
const [timeRange, setTimeRange] = useState<TimeRange>('30d')
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['monitors'],
|
|
queryFn: async () => {
|
|
const response = await monitorAPI.list()
|
|
return response.monitors
|
|
},
|
|
})
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<DashboardLayout title="Analytics" description="Monitor performance and statistics">
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
<p className="text-muted-foreground">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
)
|
|
}
|
|
|
|
const monitors = data || []
|
|
const totalMonitors = monitors.length
|
|
const activeMonitors = monitors.filter((m: any) => m.status === 'active').length
|
|
const errorMonitors = monitors.filter((m: any) => m.status === 'error').length
|
|
const avgFrequency = totalMonitors > 0
|
|
? Math.round(monitors.reduce((sum: number, m: any) => sum + m.frequency, 0) / totalMonitors)
|
|
: 0
|
|
|
|
// Calculate additional stats
|
|
const pausedMonitors = monitors.filter((m: any) => m.status === 'paused').length
|
|
const recentChanges = monitors.filter((m: any) => {
|
|
if (!m.last_change_at) return false
|
|
const changeDate = new Date(m.last_change_at)
|
|
const daysAgo = timeRange === '7d' ? 7 : timeRange === '30d' ? 30 : timeRange === '90d' ? 90 : 365
|
|
const cutoff = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
|
|
return changeDate >= cutoff
|
|
}).length
|
|
|
|
return (
|
|
<DashboardLayout title="Analytics" description="Monitor performance and statistics">
|
|
{/* Time Range Selector */}
|
|
<div className="mb-6 flex flex-wrap gap-2">
|
|
{(['7d', '30d', '90d', 'all'] as const).map((range) => (
|
|
<Button
|
|
key={range}
|
|
variant={timeRange === range ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setTimeRange(range)}
|
|
>
|
|
{range === 'all' ? 'All Time' : range === '7d' ? 'Last 7 Days' : range === '30d' ? 'Last 30 Days' : 'Last 90 Days'}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Stats Overview */}
|
|
<div className="mb-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Total Monitors</p>
|
|
<p className="text-3xl font-bold">{totalMonitors}</p>
|
|
</div>
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
|
<svg className="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Uptime Rate</p>
|
|
<p className="text-3xl font-bold text-green-600">
|
|
{totalMonitors > 0 ? Math.round((activeMonitors / totalMonitors) * 100) : 0}%
|
|
</p>
|
|
</div>
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-green-100">
|
|
<svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Error Rate</p>
|
|
<p className="text-3xl font-bold text-red-600">
|
|
{totalMonitors > 0 ? Math.round((errorMonitors / totalMonitors) * 100) : 0}%
|
|
</p>
|
|
</div>
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-red-100">
|
|
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Avg. Frequency</p>
|
|
<p className="text-3xl font-bold">{avgFrequency} min</p>
|
|
</div>
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100">
|
|
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Charts Placeholder */}
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Monitor Status Distribution</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="relative h-40 w-40">
|
|
<svg viewBox="0 0 100 100" className="h-full w-full -rotate-90">
|
|
<circle cx="50" cy="50" r="40" fill="none" stroke="hsl(var(--muted))" strokeWidth="12" />
|
|
<circle
|
|
cx="50"
|
|
cy="50"
|
|
r="40"
|
|
fill="none"
|
|
stroke="hsl(var(--success))"
|
|
strokeWidth="12"
|
|
strokeDasharray={`${(activeMonitors / (totalMonitors || 1)) * 251.2} 251.2`}
|
|
/>
|
|
</svg>
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
<span className="text-2xl font-bold">{activeMonitors}</span>
|
|
<span className="text-xs text-muted-foreground">Active</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-center gap-6">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-3 w-3 rounded-full bg-green-500" />
|
|
<span className="text-sm">Active ({activeMonitors})</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-3 w-3 rounded-full bg-red-500" />
|
|
<span className="text-sm">Error ({errorMonitors})</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Check Frequency Distribution</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{[
|
|
{ label: '5 min', count: monitors.filter((m: any) => m.frequency === 5).length },
|
|
{ label: '30 min', count: monitors.filter((m: any) => m.frequency === 30).length },
|
|
{ label: '1 hour', count: monitors.filter((m: any) => m.frequency === 60).length },
|
|
{ label: '6 hours', count: monitors.filter((m: any) => m.frequency === 360).length },
|
|
{ label: '24 hours', count: monitors.filter((m: any) => m.frequency === 1440).length },
|
|
].map((item) => (
|
|
<div key={item.label} className="flex items-center gap-3">
|
|
<span className="w-16 text-sm text-muted-foreground">{item.label}</span>
|
|
<div className="flex-1 h-4 rounded-full bg-muted overflow-hidden">
|
|
<div
|
|
className="h-full bg-primary transition-all duration-500"
|
|
style={{ width: `${totalMonitors > 0 ? (item.count / totalMonitors) * 100 : 0}%` }}
|
|
/>
|
|
</div>
|
|
<span className="w-8 text-sm font-medium">{item.count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</DashboardLayout>
|
|
)
|
|
}
|