Initial commit
This commit is contained in:
213
components/database-monitor.tsx
Normal file
213
components/database-monitor.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { RefreshCw, Database, Activity, AlertTriangle, CheckCircle } from "lucide-react"
|
||||
|
||||
interface PoolStats {
|
||||
totalConnections: number
|
||||
freeConnections: number
|
||||
acquiringConnections: number
|
||||
connectionLimit: number
|
||||
}
|
||||
|
||||
interface HealthData {
|
||||
status: string
|
||||
responseTime?: number
|
||||
poolStats?: PoolStats
|
||||
recommendations?: string[]
|
||||
error?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export function DatabaseMonitor() {
|
||||
const [healthData, setHealthData] = useState<HealthData | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [autoRefresh, setAutoRefresh] = useState(false)
|
||||
|
||||
const fetchHealth = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch("/api/admin/db-health")
|
||||
const data = await response.json()
|
||||
setHealthData(data)
|
||||
} catch (error) {
|
||||
console.error("Error fetching database health:", error)
|
||||
setHealthData({
|
||||
status: "error",
|
||||
error: "Failed to fetch health data",
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchHealth()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout
|
||||
if (autoRefresh) {
|
||||
interval = setInterval(fetchHealth, 5000) // Refresh every 5 seconds
|
||||
}
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}, [autoRefresh])
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return <CheckCircle className="h-5 w-5 text-green-500" />
|
||||
case "unhealthy":
|
||||
return <AlertTriangle className="h-5 w-5 text-red-500" />
|
||||
default:
|
||||
return <Activity className="h-5 w-5 text-yellow-500" />
|
||||
}
|
||||
}
|
||||
|
||||
const getUtilizationColor = (utilization: number) => {
|
||||
if (utilization > 0.8) return "text-red-600"
|
||||
if (utilization > 0.6) return "text-yellow-600"
|
||||
return "text-green-600"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4" dir="rtl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
מוניטור מסד נתונים
|
||||
</CardTitle>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={autoRefresh ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||
>
|
||||
{autoRefresh ? "עצור רענון" : "רענון אוטומטי"}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={fetchHealth} disabled={loading}>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
||||
רענן
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{healthData ? (
|
||||
<>
|
||||
{/* Status Overview */}
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded">
|
||||
{getStatusIcon(healthData.status)}
|
||||
<span className="font-semibold">סטטוס: {healthData.status === "healthy" ? "תקין" : "לא תקין"}</span>
|
||||
{healthData.responseTime && (
|
||||
<span className="text-sm text-gray-600">({healthData.responseTime}ms)</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pool Statistics */}
|
||||
{healthData.poolStats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-blue-50 p-3 rounded text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">{healthData.poolStats.totalConnections}</div>
|
||||
<div className="text-sm text-gray-600">חיבורים פעילים</div>
|
||||
</div>
|
||||
<div className="bg-green-50 p-3 rounded text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{healthData.poolStats.freeConnections}</div>
|
||||
<div className="text-sm text-gray-600">חיבורים זמינים</div>
|
||||
</div>
|
||||
<div className="bg-yellow-50 p-3 rounded text-center">
|
||||
<div className="text-2xl font-bold text-yellow-600">
|
||||
{healthData.poolStats.acquiringConnections}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">ממתינים לחיבור</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 p-3 rounded text-center">
|
||||
<div className="text-2xl font-bold text-purple-600">{healthData.poolStats.connectionLimit}</div>
|
||||
<div className="text-sm text-gray-600">מגבלת חיבורים</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Utilization Bar */}
|
||||
{healthData.poolStats && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>ניצולת Pool</span>
|
||||
<span
|
||||
className={getUtilizationColor(
|
||||
(healthData.poolStats.totalConnections - healthData.poolStats.freeConnections) /
|
||||
healthData.poolStats.connectionLimit,
|
||||
)}
|
||||
>
|
||||
{Math.round(
|
||||
((healthData.poolStats.totalConnections - healthData.poolStats.freeConnections) /
|
||||
healthData.poolStats.connectionLimit) *
|
||||
100,
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(
|
||||
((healthData.poolStats.totalConnections - healthData.poolStats.freeConnections) /
|
||||
healthData.poolStats.connectionLimit) *
|
||||
100,
|
||||
100,
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
{healthData.recommendations && healthData.recommendations.length > 0 && (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<div className="space-y-1">
|
||||
<strong>המלצות:</strong>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{healthData.recommendations.map((rec, index) => (
|
||||
<li key={index} className="text-sm">
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{healthData.error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{healthData.error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Timestamp */}
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
עודכן: {new Date(healthData.timestamp).toLocaleString("he-IL")}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">טוען נתוני בריאות מסד הנתונים...</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user