Files
ai-showcase-platform/components/admin/database-monitor.tsx

304 lines
9.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
// =====================================================
// 資料庫監控組件
// =====================================================
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { RefreshCw, Database, Server, AlertTriangle, CheckCircle } from 'lucide-react';
interface DatabaseStatus {
isEnabled: boolean;
currentDatabase: 'master' | 'slave';
masterHealthy: boolean;
slaveHealthy: boolean;
lastHealthCheck: string;
consecutiveFailures: number;
uptime: number;
timestamp: string;
}
interface DatabaseMonitorProps {
className?: string;
}
export function DatabaseMonitor({ className }: DatabaseMonitorProps) {
const [status, setStatus] = useState<DatabaseStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [switching, setSwitching] = useState(false);
// 獲取資料庫狀態
const fetchStatus = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/admin/database-status');
const data = await response.json();
if (data.success) {
setStatus(data.data);
} else {
setError(data.message);
}
} catch (err) {
setError('無法連接到監控服務');
console.error('獲取資料庫狀態失敗:', err);
} finally {
setLoading(false);
}
};
// 切換資料庫
const switchDatabase = async (database: 'master' | 'slave') => {
try {
setSwitching(true);
const response = await fetch('/api/admin/database-status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'switch',
database: database
})
});
const data = await response.json();
if (data.success) {
// 重新獲取狀態
await fetchStatus();
} else {
setError(data.message);
}
} catch (err) {
setError('切換資料庫失敗');
console.error('切換資料庫失敗:', err);
} finally {
setSwitching(false);
}
};
// 格式化時間
const formatTime = (timestamp: string) => {
return new Date(timestamp).toLocaleString('zh-TW');
};
// 格式化運行時間
const formatUptime = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours}${minutes}${secs}`;
};
// 組件掛載時獲取狀態
useEffect(() => {
fetchStatus();
// 每30秒自動刷新
const interval = setInterval(fetchStatus, 30000);
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<RefreshCw className="h-6 w-6 animate-spin" />
<span className="ml-2">...</span>
</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent>
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
<Button
onClick={fetchStatus}
variant="outline"
className="mt-4"
>
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</CardContent>
</Card>
);
}
if (!status) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent>
<p></p>
</CardContent>
</Card>
);
}
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Database className="h-5 w-5" />
</div>
<Button
onClick={fetchStatus}
variant="outline"
size="sm"
disabled={loading}
>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
</Button>
</CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 整體狀態 */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<Badge variant={status.isEnabled ? "default" : "secondary"}>
{status.isEnabled ? "啟用" : "停用"}
</Badge>
</div>
{/* 當前資料庫 */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<Badge variant={status.currentDatabase === 'master' ? "default" : "secondary"}>
{status.currentDatabase === 'master' ? "主機" : "備機"}
</Badge>
</div>
{/* 主機狀態 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Server className="h-4 w-4" />
<span className="text-sm font-medium"></span>
</div>
<div className="flex items-center gap-2">
{status.masterHealthy ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<AlertTriangle className="h-4 w-4 text-red-500" />
)}
<Badge variant={status.masterHealthy ? "default" : "destructive"}>
{status.masterHealthy ? "正常" : "異常"}
</Badge>
</div>
</div>
{status.masterHealthy && (
<Button
onClick={() => switchDatabase('master')}
variant="outline"
size="sm"
disabled={switching || status.currentDatabase === 'master'}
className="w-full"
>
</Button>
)}
</div>
{/* 備機狀態 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Server className="h-4 w-4" />
<span className="text-sm font-medium"></span>
</div>
<div className="flex items-center gap-2">
{status.slaveHealthy ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<AlertTriangle className="h-4 w-4 text-red-500" />
)}
<Badge variant={status.slaveHealthy ? "default" : "destructive"}>
{status.slaveHealthy ? "正常" : "異常"}
</Badge>
</div>
</div>
{status.slaveHealthy && (
<Button
onClick={() => switchDatabase('slave')}
variant="outline"
size="sm"
disabled={switching || status.currentDatabase === 'slave'}
className="w-full"
>
</Button>
)}
</div>
{/* 統計信息 */}
<div className="pt-4 border-t space-y-2">
<div className="flex items-center justify-between text-sm">
<span></span>
<span className="font-mono">{status.consecutiveFailures}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span></span>
<span className="font-mono">{formatTime(status.lastHealthCheck)}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span></span>
<span className="font-mono">{formatUptime(status.uptime)}</span>
</div>
</div>
{/* 警告信息 */}
{status.consecutiveFailures > 0 && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
{status.consecutiveFailures}
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
);
}