214 lines
8.0 KiB
TypeScript
214 lines
8.0 KiB
TypeScript
"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>
|
||
)
|
||
}
|