import { memo, useMemo, useState } from "react" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { Trans } from "@lingui/react/macro" import { useToast } from "@/components/ui/use-toast" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Globe, Clock, Activity, RefreshCw, ExternalLink, Edit3, Trash2, CheckCircle2, XCircle, PauseIcon, PlayIcon, TrendingUp, TrendingDown, } from "lucide-react" import { getMonitor, getMonitorStats, getMonitorHeartbeats, manualCheck, pauseMonitor, resumeMonitor, deleteMonitor, updateMonitor, getMonitorTypeLabel, formatUptime, formatPing, } from "@/lib/monitors" import { formatDate } from "@/lib/domains" import { getStatusPages, createStatusPage } from "@/lib/statuspages" import { Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, Cell, ComposedChart, Legend, } from "recharts" import { Link, navigate } from "@/components/router" import { AddMonitorDialog } from "@/components/monitors-table/add-monitor-dialog" import { cn } from "@/lib/utils" // Status badge component function StatusBadge({ status }: { status: string }) { const configs = { up: { color: "bg-green-500", icon: CheckCircle2, text: "Up" }, down: { color: "bg-red-500", icon: XCircle, text: "Down" }, pending: { color: "bg-yellow-500", icon: Clock, text: "Pending" }, paused: { color: "bg-gray-500", icon: PauseIcon, text: "Paused" }, maintenance: { color: "bg-blue-500", icon: Activity, text: "Maintenance" }, } const config = configs[status as keyof typeof configs] || configs.pending const Icon = config.icon return (
{config.text}
) } // Stat card component function StatCard({ title, value, icon: Icon, subtitle, trend, className, }: { title: string value: string icon: any subtitle?: string trend?: "up" | "down" | "neutral" className?: string }) { const TrendIcon = trend === "up" ? TrendingUp : trend === "down" ? TrendingDown : null return (

{title}

{value}

{TrendIcon && }
{subtitle &&

{subtitle}

}
) } export default memo(function MonitorDetail({ id }: { id: string }) { const { toast } = useToast() const queryClient = useQueryClient() const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [timeRange, setTimeRange] = useState<"24h" | "7d" | "30d">("24h") const { data: monitor, isLoading: isMonitorLoading } = useQuery({ queryKey: ["monitor", id], queryFn: () => getMonitor(id), staleTime: Infinity, refetchInterval: 30000, }) const { data: stats } = useQuery({ queryKey: ["monitor-stats", id], queryFn: () => getMonitorStats(id), refetchInterval: 30000, }) const { data: heartbeatsData } = useQuery({ queryKey: ["monitor-heartbeats", id], queryFn: () => getMonitorHeartbeats(id), refetchInterval: 30000, }) const heartbeats = heartbeatsData?.heartbeats const checkMutation = useMutation({ mutationFn: () => manualCheck(id), onSuccess: (result) => { toast({ title: `Check complete`, description: `${monitor?.name} is ${result.status}`, }) queryClient.invalidateQueries({ queryKey: ["monitor", id] }) queryClient.invalidateQueries({ queryKey: ["monitor-heartbeats", id] }) }, }) const pauseMutation = useMutation({ mutationFn: () => (monitor?.status === "paused" ? resumeMonitor(id) : pauseMonitor(id)), onSuccess: () => { toast({ title: monitor?.status === "paused" ? "Monitor resumed" : "Monitor paused", }) queryClient.invalidateQueries({ queryKey: ["monitor", id] }) }, }) const deleteMutation = useMutation({ mutationFn: () => deleteMonitor(id), onSuccess: () => { toast({ title: "Monitor deleted" }) navigate("/") }, }) const [isCreateStatusPageOpen, setIsCreateStatusPageOpen] = useState(false) const [statusPageName, setStatusPageName] = useState("") const [statusPageSlug, setStatusPageSlug] = useState("") const { data: statusPages } = useQuery({ queryKey: ["status-pages"], queryFn: () => getStatusPages(), }) const updateStatusPagesMutation = useMutation({ mutationFn: (status_pages: string[]) => updateMonitor(id, { status_pages } as any), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["monitor", id] }) toast({ title: "Status pages updated" }) }, }) const createStatusPageMutation = useMutation({ mutationFn: () => createStatusPage({ name: statusPageName || `${monitor?.name} Status`, slug: statusPageSlug || monitor?.name?.toLowerCase().replace(/\s+/g, "-") || "status", title: statusPageName || `${monitor?.name} Status Page`, public: true, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["status-pages"] }) toast({ title: "Status page created" }) setIsCreateStatusPageOpen(false) setStatusPageName("") setStatusPageSlug("") }, }) const handleDelete = () => { setIsDeleteDialogOpen(true) } // Filter heartbeats by time range const filteredHeartbeats = useMemo(() => { if (!heartbeats) return [] const now = Date.now() const ranges: Record = { "24h": 24 * 60 * 60 * 1000, "7d": 7 * 24 * 60 * 60 * 1000, "30d": 30 * 24 * 60 * 60 * 1000, } const cutoff = now - (ranges[timeRange] || ranges["24h"]) return heartbeats.filter((h: any) => { const t = new Date(h.time || h.timestamp).getTime() return t >= cutoff }) }, [heartbeats, timeRange]) // Prepare chart data from heartbeats const chartData = useMemo(() => { if (!filteredHeartbeats.length) return [] return filteredHeartbeats .slice() .reverse() .map((h: any) => ({ time: new Date(h.time || h.timestamp).toLocaleTimeString(), responseTime: h.ping || 0, status: h.status === "up" ? 1 : 0, })) }, [filteredHeartbeats]) // Calculate stats const uptimeStats = useMemo(() => { if (!heartbeats || !Array.isArray(heartbeats) || heartbeats.length === 0) return null const total = heartbeats.length const up = heartbeats.filter((h: any) => h.status === "up").length const avgResponse = heartbeats.reduce((sum: number, h: any) => sum + (h.ping || 0), 0) / total return { uptime: ((up / total) * 100).toFixed(2), avgResponse: avgResponse.toFixed(0), totalChecks: total, } }, [heartbeats]) if (isMonitorLoading) { return (
) } if (!monitor) { return (

Monitor not found

The monitor you are looking for does not exist.

) } const isUp = monitor.status === "up" const isPaused = monitor.status === "paused" return (
{/* Header */}

{monitor.name}

{getMonitorTypeLabel(monitor.type)} {monitor.interval && ( {monitor.interval}s interval )}
{monitor.url && (

{monitor.url}

)}
{monitor.url && ( )}
{/* Summary Bar */}
{/* Combined Uptime & Response Chart */}
Uptime & Response Time Status and response time over the selected period
{(["24h", "7d", "30d"] as const).map((range) => ( ))}
{chartData.length > 0 ? ( (v === 1 ? "Up" : "Down")} /> {chartData.map((entry, index) => ( ))} ) : (
No data available for selected time range
)}
Monitor Details
Type {getMonitorTypeLabel(monitor.type)}
Interval {monitor.interval}s
Retries {monitor.retries}
Created {formatDate(monitor.created)}
{monitor.last_check && (
Last Check {formatDate(monitor.last_check)}
)}
Status Page Link or create a public status page {statusPages && statusPages.length > 0 ? (
{statusPages.map((page) => { const isLinked = monitor.status_pages?.includes(page.id) || false return (
{page.name}
) })}
) : (

No status pages yet.

)}
Recent Checks Last 50 monitor checks Time Status Response Time Message {heartbeats?.slice(0, 50).map((hb: any) => ( {formatDate(hb.time || hb.timestamp)} {hb.status} {formatPing(hb.ping)} {hb.msg || "-"} ))} {!heartbeats?.length && ( No check history available )}
{/* Create Status Page Dialog */} {isCreateStatusPageOpen && ( Create Status Page Create a public status page for this monitor.
setStatusPageName(e.target.value)} placeholder={`${monitor.name} Status`} />
setStatusPageSlug(e.target.value)} placeholder={monitor.name?.toLowerCase().replace(/\s+/g, "-")} />
setIsCreateStatusPageOpen(false)}>Cancel createStatusPageMutation.mutate()} disabled={createStatusPageMutation.isPending} > Create
)} Delete Monitor Are you sure you want to delete this monitor? This action cannot be undone. Cancel { deleteMutation.mutate() setIsDeleteDialogOpen(false) }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > Delete
) })