import { Trans, useLingui } from "@lingui/react/macro"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import {
AlertTriangle,
ArrowDownIcon,
ArrowUpIcon,
CheckCircleIcon,
Edit3Icon,
FilterIcon,
GlobeIcon,
LayoutGridIcon,
LayoutListIcon,
PauseIcon,
PlayIcon,
PlusIcon,
RefreshCwIcon,
Settings2Icon,
TagIcon,
Trash2Icon,
XCircle,
XCircleIcon,
} from "lucide-react"
import { memo, useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useToast } from "@/components/ui/use-toast"
import {
deleteMonitor,
getMonitorTypeLabel,
listMonitors,
manualCheck,
pauseMonitor,
resumeMonitor,
type Monitor,
type MonitorStatus,
type MonitorType,
formatUptime,
formatPing,
} from "@/lib/monitors"
import { cn, useBrowserStorage } from "@/lib/utils"
import { AddMonitorDialog } from "./add-monitor-dialog"
import { Link } from "@/components/router"
import ContainersTable from "../containers-table/containers-table"
// Status indicator component
function StatusIndicator({ status }: { status: MonitorStatus }) {
const colors = {
up: "bg-green-500",
down: "bg-red-500",
pending: "bg-yellow-400",
paused: "bg-gray-400",
maintenance: "bg-blue-500",
}
const icons = {
up: CheckCircleIcon,
down: XCircleIcon,
pending: RefreshCwIcon,
paused: PauseIcon,
maintenance: RefreshCwIcon,
}
const Icon = icons[status] || RefreshCwIcon
return (
)
}
// Monitor Card component for grid view
function MonitorCard({
monitor,
onEdit,
}: {
monitor: Monitor
onEdit: (m: Monitor) => void
}) {
const { toast } = useToast()
const queryClient = useQueryClient()
const checkMutation = useMutation({
mutationFn: manualCheck,
onSuccess: (result) => {
toast({
title: `Check complete`,
description: `${monitor.name} is ${result.status} (${formatPing(result.ping)})`,
})
queryClient.invalidateQueries({ queryKey: ["monitors"] })
},
})
const pauseMutation = useMutation({
mutationFn: monitor.status === "paused" ? resumeMonitor : pauseMonitor,
onSuccess: () => {
toast({
title: monitor.status === "paused" ? "Monitor resumed" : "Monitor paused",
})
queryClient.invalidateQueries({ queryKey: ["monitors"] })
},
})
const deleteMutation = useMutation({
mutationFn: deleteMonitor,
onSuccess: () => {
toast({ title: "Monitor deleted" })
queryClient.invalidateQueries({ queryKey: ["monitors"] })
},
})
return (
{monitor.name}
{monitor.url || monitor.hostname}
{monitor.port ? `:${monitor.port}` : ""}
onEdit(monitor)}>
Edit
deleteMutation.mutate(monitor.id)}
className="text-destructive"
>
Delete
Type
{getMonitorTypeLabel(monitor.type)}
{/* Uptime - Prominent pill display */}
{monitor.uptime_stats?.uptime_7d !== undefined && monitor.uptime_stats.uptime_7d !== monitor.uptime_stats?.uptime_24h && (
)}
Response
{monitor.last_check ? (
formatPing(monitor.uptime_stats?.last_ping || 0)
) : (
-
)}
{monitor.tags && monitor.tags.length > 0 && (
{monitor.tags.map((tag) => (
{tag}
))}
)}
{/* Containers Section */}
Check now
{monitor.status === "paused" ? "Resume" : "Pause"}
)
}
// Uptime pill badge component - big and visible
function UptimePill({ uptime, label = "24h" }: { uptime: number; label?: string }) {
let colorClass = "bg-green-500 text-white border-green-500"
let icon =
if (uptime < 99.9) {
colorClass = "bg-green-500 text-white border-green-500"
}
if (uptime < 95) {
colorClass = "bg-yellow-500 text-white border-yellow-500"
icon =
}
if (uptime < 90) {
colorClass = "bg-red-500 text-white border-red-500"
icon =
}
return (
{icon}
{formatUptime(uptime)}
{label}
)
}
// Uptime bar component with pill style
function UptimeBar({ stats }: { stats?: Record }) {
const uptime24h = stats?.uptime_24h ?? 100
const uptime7d = stats?.uptime_7d ?? 100
const uptime30d = stats?.uptime_30d ?? 100
return (
{uptime7d !== 100 && uptime7d !== uptime24h && (
)}
{uptime30d !== 100 && uptime30d !== uptime24h && uptime30d !== uptime7d && (
)}
)
}
// Mini uptime dots visualization
function UptimeDots({ heartbeats }: { heartbeats?: Array<{ status: string; time: string }> }) {
if (!heartbeats || heartbeats.length === 0) {
return (
{Array.from({ length: 12 }).map((_, i) => (
))}
)
}
// Take last 12 heartbeats
const recent = heartbeats.slice(-12)
return (
{recent.map((hb, i) => (
))}
{recent.length < 12 && Array.from({ length: 12 - recent.length }).map((_, i) => (
))}
)
}
// Monitor row component
function MonitorRow({
monitor,
onEdit,
}: {
monitor: Monitor
onEdit: (m: Monitor) => void
}) {
const { toast } = useToast()
const queryClient = useQueryClient()
const checkMutation = useMutation({
mutationFn: manualCheck,
onSuccess: (result) => {
toast({
title: `Check complete`,
description: `${monitor.name} is ${result.status} (${formatPing(result.ping)})`,
})
queryClient.invalidateQueries({ queryKey: ["monitors"] })
},
onError: () => {
toast({
title: "Check failed",
variant: "destructive",
})
},
})
const pauseMutation = useMutation({
mutationFn: monitor.status === "paused" ? resumeMonitor : pauseMonitor,
onSuccess: () => {
toast({
title: monitor.status === "paused" ? "Monitor resumed" : "Monitor paused",
})
queryClient.invalidateQueries({ queryKey: ["monitors"] })
},
})
const deleteMutation = useMutation({
mutationFn: deleteMonitor,
onSuccess: () => {
toast({ title: "Monitor deleted" })
queryClient.invalidateQueries({ queryKey: ["monitors"] })
},
})
return (
{monitor.name}
{monitor.url || monitor.hostname}
{monitor.port ? `:${monitor.port}` : ""}
{getMonitorTypeLabel(monitor.type)}
{monitor.last_check ? (
{formatPing(monitor.uptime_stats?.last_ping || 0)}
) : (
-
)}
{monitor.tags?.map((tag) => (
{tag}
))}
Check now
{monitor.status === "paused" ? "Resume" : "Pause"}
onEdit(monitor)}>
Edit
deleteMutation.mutate(monitor.id)}
>
Delete
)
}
type ViewMode = "table" | "grid"
type StatusFilter = "all" | MonitorStatus
type TypeFilter = "all" | MonitorType
// Main component
export default memo(function MonitorsTable() {
const { t } = useLingui()
const [filter, setFilter] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [tagFilter, setTagFilter] = useState("all")
const [typeFilter, setTypeFilter] = useState("all")
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [editingMonitor, setEditingMonitor] = useState(null)
const [viewMode, setViewMode] = useBrowserStorage(
"monitorsViewMode",
window.innerWidth < 1024 ? "grid" : "table"
)
const { data: monitors = [], isLoading } = useQuery({
queryKey: ["monitors"],
queryFn: listMonitors,
refetchInterval: 30000,
})
// Extract all unique types
const allTypes = useMemo(() => {
const typeSet = new Set()
monitors.forEach((m) => typeSet.add(m.type))
return Array.from(typeSet).sort()
}, [monitors])
// Filter by status first
const statusFilteredMonitors = useMemo(() => {
if (statusFilter === "all") return monitors
return monitors.filter((m) => m.status === statusFilter)
}, [monitors, statusFilter])
// Then filter by search text and type
const filteredMonitors = useMemo(() => {
let result = statusFilteredMonitors
if (filter) {
const f = filter.toLowerCase()
result = result.filter(
(m) =>
m.name.toLowerCase().includes(f) ||
(m.url || "").toLowerCase().includes(f) ||
(m.hostname || "").toLowerCase().includes(f)
)
}
if (tagFilter !== "all") {
result = result.filter((m) => m.tags?.includes(tagFilter))
}
if (typeFilter !== "all") {
result = result.filter((m) => m.type === typeFilter)
}
return result
}, [statusFilteredMonitors, filter, tagFilter, typeFilter])
// Extract all unique tags
const allTags = useMemo(() => {
const tagSet = new Set()
monitors.forEach((m) => m.tags?.forEach((tag) => tagSet.add(tag)))
return Array.from(tagSet).sort()
}, [monitors])
const stats = useMemo(() => {
const total = monitors.length
const up = monitors.filter((m) => m.status === "up").length
const down = monitors.filter((m) => m.status === "down").length
const paused = monitors.filter((m) => m.status === "paused").length
const pending = monitors.filter((m) => m.status === "pending").length
const maintenance = monitors.filter((m) => m.status === "maintenance").length
return { total, up, down, paused, pending, maintenance }
}, [monitors])
return (
{/* Title row */}
Status
Monitor websites, APIs, and services
({stats.up}
{stats.down > 0 && (
<>
{" "}
{stats.down}{" "}
>
)}
{stats.paused > 0 && (
<>
{" "}
{stats.paused}
>
)}
/ {stats.total})
{/* Quick status filters */}
{[
{ key: "all", label: `All ${stats.total}`, color: "bg-primary" },
{ key: "up", label: `Up ${stats.up}`, color: "bg-green-500" },
{ key: "down", label: `Down ${stats.down}`, color: "bg-red-500" },
{ key: "paused", label: `Paused ${stats.paused}`, color: "bg-gray-400" },
].map((s) => (
))}
{/* Filter row */}
setFilter(e.target.value)}
value={filter}
className="w-full"
/>
{allTypes.length > 0 && (
setTypeFilter(v as TypeFilter)}>
All Types
{allTypes.map((type) => (
{getMonitorTypeLabel(type)}
))}
)}
{allTags.length > 0 && (
All Tags
{allTags.map((tag) => (
{tag}
))}
)}
{/* Layout */}
Layout
setViewMode(v as ViewMode)}>
Table
Grid
{/* Status Filter */}
Status
setStatusFilter(v as StatusFilter)}>
All ({stats.total})
Up ({stats.up})
Down ({stats.down})
Paused ({stats.paused})
{stats.pending > 0 && (
Pending ({stats.pending})
)}
{stats.maintenance > 0 && (
Maintenance ({stats.maintenance})
)}
{isLoading ? (
Loading...
) : filteredMonitors.length === 0 ? (
{filter || statusFilter !== "all" || tagFilter !== "all" || typeFilter !== "all" ? (
No monitors match your filters.
) : (
No monitors configured yet.
)}
) : viewMode === "table" ? (
Name
Type
Status
Response
Uptime (24h)
Tags
Actions
{filteredMonitors.map((monitor) => (
))}
) : (
{filteredMonitors.map((monitor) => (
))}
)}
{/* Add Monitor Dialog */}
{/* Edit Monitor Dialog */}
{editingMonitor && (
!open && setEditingMonitor(null)}
monitor={editingMonitor}
isEdit
/>
)}
)
})