refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics
- Add Workspace domain (entity, repository, service, handler, DTO) - Add multi-tenant K8s client with tenant binding and quota management - Add K8s diagnostics client (instance diagnostics) - Add authorization middleware (authz package) - Restructure frontend to feature-based architecture (features/) - Add User Management page in configuration - Add AccessDenied page and route guards - Refactor shared components (form inputs, layout, UI) - Update Tailwind config for new design system - Add comprehensive documentation (docs/, tasks/, plans) - Improve cluster service with better kubeconfig handling - Add tests for crypto, config, helm client, tenant binding
This commit is contained in:
@ -55,7 +55,7 @@ export const ClusterMonitorCard: React.FC<ClusterMonitorCardProps> = ({ cluster
|
||||
case "unhealthy":
|
||||
return <XCircle className="w-5 h-5 text-red-400" />;
|
||||
default:
|
||||
return <HelpCircle className="w-5 h-5 text-gray-400" />;
|
||||
return <HelpCircle className="w-5 h-5 text-slate-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
@ -64,66 +64,66 @@ export const ClusterMonitorCard: React.FC<ClusterMonitorCardProps> = ({ cluster
|
||||
<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">
|
||||
<div className="p-3 bg-white 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>
|
||||
<h3 className="text-lg font-semibold text-slate-900 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>
|
||||
<p className="text-xs text-slate-500">Uptime</p>
|
||||
<p className="text-sm text-slate-700 font-mono mt-1">{uptime}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Nodes</p>
|
||||
<p className="text-xs text-slate-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>
|
||||
<p className="text-sm text-slate-700 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>
|
||||
<p className="text-xs text-slate-500">Pods</p>
|
||||
<p className="text-sm text-slate-700 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">
|
||||
<p className="text-xs text-slate-500">GPU</p>
|
||||
<p className="text-sm text-slate-700 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 className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-3 p-3 bg-slate-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>
|
||||
<p className="text-xs text-slate-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">
|
||||
<p className="text-sm text-slate-700 font-mono">{usedCpu} / {totalCpu}</p>
|
||||
<div className="mt-1 h-1.5 bg-slate-100 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>
|
||||
<p className="text-xs text-slate-500 mt-1">{cpuUsage.toFixed(1)}%</p>
|
||||
{cluster.maxNodeCpu && (
|
||||
<div className="mt-1.5 pt-1.5 border-t border-gray-700/50">
|
||||
<div className="mt-1.5 pt-1.5 border-t border-slate-200">
|
||||
<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>
|
||||
<p className="text-xs text-slate-500">Max per node</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 font-mono">{cluster.maxNodeCpu}</p>
|
||||
<p className="text-xs text-slate-500 font-mono">{cluster.maxNodeCpu}</p>
|
||||
{cluster.maxNodeCpuUsage && cluster.maxNodeCpuUsage > 0 && (
|
||||
<p className="text-xs text-gray-500">Peak: {cluster.maxNodeCpuUsage.toFixed(1)}%</p>
|
||||
<p className="text-xs text-slate-500">Peak: {cluster.maxNodeCpuUsage.toFixed(1)}%</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -132,25 +132,25 @@ export const ClusterMonitorCard: React.FC<ClusterMonitorCardProps> = ({ cluster
|
||||
<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>
|
||||
<p className="text-xs text-slate-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">
|
||||
<p className="text-sm text-slate-700 font-mono">{usedMemory} / {totalMemory}</p>
|
||||
<div className="mt-1 h-1.5 bg-slate-100 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>
|
||||
<p className="text-xs text-slate-500 mt-1">{memoryUsage.toFixed(1)}%</p>
|
||||
{cluster.maxNodeMemory && (
|
||||
<div className="mt-1.5 pt-1.5 border-t border-gray-700/50">
|
||||
<div className="mt-1.5 pt-1.5 border-t border-slate-200">
|
||||
<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>
|
||||
<p className="text-xs text-slate-500">Max per node</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 font-mono">{cluster.maxNodeMemory}</p>
|
||||
<p className="text-xs text-slate-500 font-mono">{cluster.maxNodeMemory}</p>
|
||||
{cluster.maxNodeMemUsage && cluster.maxNodeMemUsage > 0 && (
|
||||
<p className="text-xs text-gray-500">Peak: {cluster.maxNodeMemUsage.toFixed(1)}%</p>
|
||||
<p className="text-xs text-slate-500">Peak: {cluster.maxNodeMemUsage.toFixed(1)}%</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -160,25 +160,25 @@ export const ClusterMonitorCard: React.FC<ClusterMonitorCardProps> = ({ cluster
|
||||
<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>
|
||||
<p className="text-xs text-slate-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">
|
||||
<p className="text-sm text-slate-700 font-mono">{usedGpu} / {totalGpu}</p>
|
||||
<div className="mt-1 h-1.5 bg-slate-100 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>
|
||||
<p className="text-xs text-slate-500 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="mt-1.5 pt-1.5 border-t border-slate-200">
|
||||
<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>
|
||||
<p className="text-xs text-slate-500">Max per node</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 font-mono">{cluster.maxNodeGpu} GPUs</p>
|
||||
<p className="text-xs text-slate-500 font-mono">{cluster.maxNodeGpu} GPUs</p>
|
||||
{cluster.maxNodeGpuUsage && cluster.maxNodeGpuUsage > 0 && (
|
||||
<p className="text-xs text-gray-500">Peak: {cluster.maxNodeGpuUsage.toFixed(1)}%</p>
|
||||
<p className="text-xs text-slate-500">Peak: {cluster.maxNodeGpuUsage.toFixed(1)}%</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -186,7 +186,7 @@ export const ClusterMonitorCard: React.FC<ClusterMonitorCardProps> = ({ cluster
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-gray-500">
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-slate-500">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>Last checked: {lastCheckedText}</span>
|
||||
</div>
|
||||
@ -218,8 +218,8 @@ export const ClusterMonitorCard: React.FC<ClusterMonitorCardProps> = ({ cluster
|
||||
|
||||
{/* 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">
|
||||
<div className="mt-4 pt-4 border-t border-slate-200">
|
||||
<h4 className="text-sm font-semibold text-slate-900 mb-3 flex items-center gap-2">
|
||||
<ServerIcon className="w-4 h-4 text-blue-400" />
|
||||
Cluster Nodes ({cluster.nodes.length})
|
||||
</h4>
|
||||
|
||||
@ -39,16 +39,16 @@ export const NodeMetricCard: React.FC<NodeMetricCardProps> = ({ node }) => {
|
||||
const gpuCapacity = node.gpuCapacity ?? 0;
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-gray-800/30 rounded-lg border border-gray-700/50 hover:border-gray-600/50 transition">
|
||||
<div className="p-4 bg-white rounded-lg border border-slate-200 hover:border-slate-300/50 transition">
|
||||
{/* Node Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-gray-700/50 rounded">
|
||||
<div className="p-2 bg-slate-100/50 rounded">
|
||||
<Server className="w-4 h-4 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="text-sm font-semibold text-white">{node.nodeName}</h4>
|
||||
<h4 className="text-sm font-semibold text-slate-900">{node.nodeName}</h4>
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -58,8 +58,8 @@ export const NodeMetricCard: React.FC<NodeMetricCardProps> = ({ node }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500">Age</p>
|
||||
<p className="text-xs text-gray-300 font-mono">{node.age}</p>
|
||||
<p className="text-xs text-slate-500">Age</p>
|
||||
<p className="text-xs text-slate-700 font-mono">{node.age}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -69,76 +69,76 @@ export const NodeMetricCard: React.FC<NodeMetricCardProps> = ({ node }) => {
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Cpu className="w-3 h-3 text-blue-400" />
|
||||
<p className="text-xs text-gray-500">CPU</p>
|
||||
<p className="text-xs text-slate-500">CPU</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-300 font-mono mb-1">
|
||||
<p className="text-xs text-slate-700 font-mono mb-1">
|
||||
{node.cpuUsage ?? "N/A"} / {node.cpuAllocatable ?? "N/A"}
|
||||
</p>
|
||||
<div className="h-1 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-1 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(cpuPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-0.5">{cpuPercent.toFixed(1)}%</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{cpuPercent.toFixed(1)}%</p>
|
||||
</div>
|
||||
|
||||
{/* Memory */}
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Database className="w-3 h-3 text-green-400" />
|
||||
<p className="text-xs text-gray-500">Memory</p>
|
||||
<p className="text-xs text-slate-500">Memory</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-300 font-mono mb-1">
|
||||
<p className="text-xs text-slate-700 font-mono mb-1">
|
||||
{node.memoryUsage ?? "N/A"} / {node.memoryAllocatable ?? "N/A"}
|
||||
</p>
|
||||
<div className="h-1 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-1 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(memoryPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-0.5">{memoryPercent.toFixed(1)}%</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{memoryPercent.toFixed(1)}%</p>
|
||||
</div>
|
||||
|
||||
{/* GPU */}
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Activity className="w-3 h-3 text-purple-400" />
|
||||
<p className="text-xs text-gray-500">GPU</p>
|
||||
<p className="text-xs text-slate-500">GPU</p>
|
||||
</div>
|
||||
{gpuCapacity > 0 ? (
|
||||
<>
|
||||
<p className="text-xs text-gray-300 font-mono mb-1">
|
||||
<p className="text-xs text-slate-700 font-mono mb-1">
|
||||
{node.gpuUsage ?? "N/A"} / {gpuCapacity}
|
||||
</p>
|
||||
<div className="h-1 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-1 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-purple-500 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(gpuPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
{gpuPercent.toFixed(1)}%
|
||||
{node.gpuType && <span className="ml-1 text-gray-500">({node.gpuType})</span>}
|
||||
{node.gpuType && <span className="ml-1 text-slate-500">({node.gpuType})</span>}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500 mt-1">No GPU</p>
|
||||
<p className="text-xs text-slate-500 mt-1">No GPU</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="mt-3 pt-3 border-t border-gray-700/50 grid grid-cols-2 gap-2">
|
||||
<div className="mt-3 pt-3 border-t border-slate-200 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Pods</p>
|
||||
<p className="text-xs text-gray-300 font-mono">{node.podCount ?? 0}</p>
|
||||
<p className="text-xs text-slate-500">Pods</p>
|
||||
<p className="text-xs text-slate-700 font-mono">{node.podCount ?? 0}</p>
|
||||
</div>
|
||||
{node.kubeletVersion && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Kubelet</p>
|
||||
<p className="text-xs text-gray-300 font-mono">{node.kubeletVersion}</p>
|
||||
<p className="text-xs text-slate-500">Kubelet</p>
|
||||
<p className="text-xs text-slate-700 font-mono">{node.kubeletVersion}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -155,7 +155,7 @@ const MonitoringClustersPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Auto-refresh Info */}
|
||||
<div className="text-sm text-gray-400">
|
||||
<div className="text-sm text-slate-500">
|
||||
Auto-refresh every 30 seconds {refreshing && "• Refreshing..."}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user