This commit is contained in:
mangomqy
2025-11-13 02:54:06 +00:00
commit c5e51ed069
254 changed files with 54901 additions and 0 deletions

View File

@ -0,0 +1,235 @@
/**
* Cluster Monitor Card Component
* 显示单个集群的监控信息
*/
import React, { useState } from "react";
import { Activity, CheckCircle, AlertTriangle, XCircle, HelpCircle, Clock, Cpu, Database, Server as ServerIcon, ChevronDown, ChevronUp, TrendingUp } from "lucide-react";
import { Card, Badge } from "@/shared/components";
import type { ClusterMetrics } from "@/core/types";
import { NodeMetricCard } from "./NodeMetricCard";
interface ClusterMonitorCardProps {
cluster: ClusterMetrics;
}
export const ClusterMonitorCard: React.FC<ClusterMonitorCardProps> = ({ cluster }) => {
const [showNodes, setShowNodes] = useState(false);
const status = cluster.status ?? "unknown";
const uptime = cluster.uptime ?? "N/A";
const nodeCount = cluster.nodeCount ?? 0;
const podCount = cluster.podCount ?? 0;
const totalGpu = cluster.totalGpu ?? 0;
const usedGpu = cluster.usedGpu ?? 0;
const cpuUsage = cluster.cpuUsage ?? 0;
const memoryUsage = cluster.memoryUsage ?? 0;
const gpuUsage = cluster.gpuUsage ?? 0;
const usedCpu = cluster.usedCpu ?? "N/A";
const totalCpu = cluster.totalCpu ?? "N/A";
const usedMemory = cluster.usedMemory ?? "N/A";
const totalMemory = cluster.totalMemory ?? "N/A";
const lastCheckedText = cluster.lastCheck ? new Date(cluster.lastCheck).toLocaleString() : "N/A";
const getStatusBadge = () => {
switch (status) {
case "healthy":
return <Badge variant="success">Healthy</Badge>;
case "warning":
case "unknown":
return <Badge variant="warning">Warning</Badge>;
case "error":
case "unhealthy":
return <Badge variant="danger">Error</Badge>;
default:
return <Badge variant="gray">Unknown</Badge>;
}
};
const getStatusIcon = () => {
switch (status) {
case "healthy":
return <CheckCircle className="w-5 h-5 text-green-400" />;
case "warning":
case "unknown":
return <AlertTriangle className="w-5 h-5 text-yellow-400" />;
case "error":
case "unhealthy":
return <XCircle className="w-5 h-5 text-red-400" />;
default:
return <HelpCircle className="w-5 h-5 text-gray-400" />;
}
};
return (
<Card className="p-5">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
{/* Status Icon */}
<div className="p-3 bg-gray-800 rounded-lg">
{getStatusIcon()}
</div>
{/* Cluster Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-white truncate">{cluster.clusterName || "Unnamed Cluster"}</h3>
{getStatusBadge()}
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-3">
<div>
<p className="text-xs text-gray-500">Uptime</p>
<p className="text-sm text-gray-300 font-mono mt-1">{uptime}</p>
</div>
<div>
<p className="text-xs text-gray-500">Nodes</p>
<div className="flex items-center gap-1 mt-1">
<ServerIcon className="w-3 h-3 text-blue-400" />
<p className="text-sm text-gray-300 font-mono">{nodeCount}</p>
</div>
</div>
<div>
<p className="text-xs text-gray-500">Pods</p>
<p className="text-sm text-gray-300 font-mono mt-1">{podCount}</p>
</div>
<div>
<p className="text-xs text-gray-500">GPU</p>
<p className="text-sm text-gray-300 font-mono mt-1">
{usedGpu}/{totalGpu || "N/A"}
</p>
</div>
</div>
{/* Resource Usage */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-3 p-3 bg-gray-800/50 rounded-lg">
<div>
<div className="flex items-center gap-2 mb-1">
<Cpu className="w-3 h-3 text-blue-400" />
<p className="text-xs text-gray-500">CPU (Cluster Total)</p>
</div>
<p className="text-sm text-gray-300 font-mono">{usedCpu} / {totalCpu}</p>
<div className="mt-1 h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${Math.min(cpuUsage, 100)}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">{cpuUsage.toFixed(1)}%</p>
{cluster.maxNodeCpu && (
<div className="mt-1.5 pt-1.5 border-t border-gray-700/50">
<div className="flex items-center gap-1">
<TrendingUp className="w-3 h-3 text-blue-400/60" />
<p className="text-xs text-gray-500">Max per node</p>
</div>
<p className="text-xs text-gray-400 font-mono">{cluster.maxNodeCpu}</p>
{cluster.maxNodeCpuUsage && cluster.maxNodeCpuUsage > 0 && (
<p className="text-xs text-gray-500">Peak: {cluster.maxNodeCpuUsage.toFixed(1)}%</p>
)}
</div>
)}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<Database className="w-3 h-3 text-green-400" />
<p className="text-xs text-gray-500">Memory (Cluster Total)</p>
</div>
<p className="text-sm text-gray-300 font-mono">{usedMemory} / {totalMemory}</p>
<div className="mt-1 h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-green-500 rounded-full transition-all"
style={{ width: `${Math.min(memoryUsage, 100)}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">{memoryUsage.toFixed(1)}%</p>
{cluster.maxNodeMemory && (
<div className="mt-1.5 pt-1.5 border-t border-gray-700/50">
<div className="flex items-center gap-1">
<TrendingUp className="w-3 h-3 text-green-400/60" />
<p className="text-xs text-gray-500">Max per node</p>
</div>
<p className="text-xs text-gray-400 font-mono">{cluster.maxNodeMemory}</p>
{cluster.maxNodeMemUsage && cluster.maxNodeMemUsage > 0 && (
<p className="text-xs text-gray-500">Peak: {cluster.maxNodeMemUsage.toFixed(1)}%</p>
)}
</div>
)}
</div>
{totalGpu > 0 && (
<div>
<div className="flex items-center gap-2 mb-1">
<Activity className="w-3 h-3 text-purple-400" />
<p className="text-xs text-gray-500">GPU (Cluster Total)</p>
</div>
<p className="text-sm text-gray-300 font-mono">{usedGpu} / {totalGpu}</p>
<div className="mt-1 h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 rounded-full transition-all"
style={{ width: `${Math.min(gpuUsage, 100)}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">{gpuUsage.toFixed(1)}%</p>
{cluster.maxNodeGpu && cluster.maxNodeGpu > 0 && (
<div className="mt-1.5 pt-1.5 border-t border-gray-700/50">
<div className="flex items-center gap-1">
<TrendingUp className="w-3 h-3 text-purple-400/60" />
<p className="text-xs text-gray-500">Max per node</p>
</div>
<p className="text-xs text-gray-400 font-mono">{cluster.maxNodeGpu} GPUs</p>
{cluster.maxNodeGpuUsage && cluster.maxNodeGpuUsage > 0 && (
<p className="text-xs text-gray-500">Peak: {cluster.maxNodeGpuUsage.toFixed(1)}%</p>
)}
</div>
)}
</div>
)}
</div>
<div className="mt-3 flex items-center gap-2 text-xs text-gray-500">
<Clock className="w-3 h-3" />
<span>Last checked: {lastCheckedText}</span>
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
{cluster.nodes && cluster.nodes.length > 0 && (
<button
onClick={() => setShowNodes(!showNodes)}
className="px-3 py-1.5 text-sm text-blue-400 hover:text-blue-300 hover:bg-blue-400/10 rounded-lg transition flex items-center gap-2"
>
{showNodes ? (
<>
<ChevronUp className="w-4 h-4" />
Hide Nodes
</>
) : (
<>
<ChevronDown className="w-4 h-4" />
Show Nodes ({cluster.nodes.length})
</>
)}
</button>
)}
</div>
</div>
{/* Nodes List */}
{showNodes && cluster.nodes && cluster.nodes.length > 0 && (
<div className="mt-4 pt-4 border-t border-gray-700/50">
<h4 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<ServerIcon className="w-4 h-4 text-blue-400" />
Cluster Nodes ({cluster.nodes.length})
</h4>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{cluster.nodes.map((node) => (
<NodeMetricCard key={node.nodeName} node={node} />
))}
</div>
</div>
)}
</Card>
);
};