'use client'; import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '@/lib/api'; import { toast } from 'react-toastify'; import type { Cluster, ClusterResources } from '@/types'; import { Server, CheckCircle, XCircle, BarChart3, Clock, RotateCw, Plug, X, ScrollText } from 'lucide-react'; import { useConfirm } from '@/components/confirm-modal'; function ResourcePanel({ clusterId }: { clusterId: string }) { const { data, isLoading, error } = useQuery({ queryKey: ['cluster-resources', clusterId], queryFn: () => api.get(`/clusters/${clusterId}/resources`).then((r) => r.data), refetchInterval: 30000, }); if (isLoading) return
Loading resources...
; if (error) return
Failed to load resources
; if (!data) return null; const cpuCap = parseFloat(data.totalCpuCapacity); const cpuAlloc = parseFloat(data.totalCpuAllocatable); const memCap = parseFloat(data.totalMemoryCapacity); const memAlloc = parseFloat(data.totalMemoryAllocatable); const cpuUsedPct = cpuCap > 0 ? ((cpuCap - cpuAlloc) / cpuCap * 100) : 0; const memUsedPct = memCap > 0 ? ((memCap - memAlloc) / memCap * 100) : 0; return (
{/* Summary cards */}
{data.nodeCount}
Nodes
{data.podCount}
Pods
{data.appCount}
Apps
{data.totalCpuCapacity}
Total CPU
{/* CPU & Memory bars */}
CPU Reserved {cpuUsedPct.toFixed(1)}%
80 ? 'bg-red-500' : cpuUsedPct > 60 ? 'bg-yellow-500' : 'bg-blue-500'}`} style={{ width: `${Math.min(cpuUsedPct, 100)}%` }} />
{data.totalCpuCapacity} capacity · {data.totalCpuAllocatable} allocatable
Memory Reserved {memUsedPct.toFixed(1)}%
80 ? 'bg-red-500' : memUsedPct > 60 ? 'bg-yellow-500' : 'bg-purple-500'}`} style={{ width: `${Math.min(memUsedPct, 100)}%` }} />
{data.totalMemoryCapacity} capacity · {data.totalMemoryAllocatable} allocatable
{/* Nodes table */}

Nodes

{data.nodes.map((node) => ( ))}
Name Status Roles CPU (Cap / Alloc) Memory (Cap / Alloc)
{node.name} {node.status} {node.roles} {node.cpuCapacity} / {node.cpuAllocatable} {node.memoryCapacity} / {node.memoryAllocatable}
); } function apiErrorMessage(err: unknown, fallback: string): string { const e = err as { response?: { data?: { message?: string | string[] } } }; const msg = e?.response?.data?.message; if (Array.isArray(msg)) return msg.join(', '); if (typeof msg === 'string' && msg.trim()) return msg; return fallback; } interface ClusterToolField { key: string; label: string; type: 'text' | 'email'; required: boolean; placeholder?: string; helpText?: string; } interface ClusterTool { id: string; name: string; description: string; category: string; dependencies: string[]; installFields: ClusterToolField[]; status: 'not_installed' | 'installing' | 'installed' | 'failed' | 'unknown'; message?: string; details?: Record; } const TOOL_STATUS_BADGE: Record = { installed: { label: 'Installed', cls: 'badge-green' }, installing: { label: 'Installing…', cls: 'badge-yellow' }, failed: { label: 'Failed', cls: 'badge-red' }, not_installed: { label: 'Not installed', cls: 'badge-gray' }, unknown: { label: 'Unknown', cls: 'badge-gray' }, }; function ToolRow({ clusterId, tool, tools }: { clusterId: string; tool: ClusterTool; tools: ClusterTool[] }) { const queryClient = useQueryClient(); const confirm = useConfirm(); const [showForm, setShowForm] = useState(false); const [fields, setFields] = useState>({}); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['cluster-tools', clusterId] }); const installMutation = useMutation({ mutationFn: (params: Record) => api.post(`/clusters/${clusterId}/tools/${tool.id}/install`, params), onSuccess: (res) => { invalidate(); setShowForm(false); setFields({}); toast.success(res.data?.message || `${tool.name} install started`); }, onError: (err) => toast.error(apiErrorMessage(err, `Failed to install ${tool.name}`)), }); const uninstallMutation = useMutation({ mutationFn: () => api.delete(`/clusters/${clusterId}/tools/${tool.id}`), onSuccess: (res) => { invalidate(); toast.success(res.data?.message || `${tool.name} removed`); }, onError: (err) => toast.error(apiErrorMessage(err, `Failed to remove ${tool.name}`)), }); const unmetDeps = tool.dependencies.filter( (depId) => tools.find((t) => t.id === depId)?.status !== 'installed', ); const depsBlocked = unmetDeps.length > 0; const isInstalled = tool.status === 'installed'; const isBusy = installMutation.isPending || uninstallMutation.isPending; const badge = TOOL_STATUS_BADGE[tool.status]; const startInstall = () => { if (tool.installFields.length > 0) { setShowForm((s) => !s); } else { installMutation.mutate({}); } }; const submitForm = () => { for (const f of tool.installFields) { if (f.required && !fields[f.key]?.trim()) { toast.error(`${f.label} is required`); return; } } installMutation.mutate(fields); }; return (

{tool.name}

{badge.label} {tool.category}

{tool.description}

{tool.message && (

{tool.message}

)} {depsBlocked && !isInstalled && (

Requires:{' '} {unmetDeps .map((d) => tools.find((t) => t.id === d)?.name || d) .join(', ')}

)}
{tool.status === 'installing' && ( )} {isInstalled ? ( ) : ( )}
{showForm && !isInstalled && (
{tool.installFields.map((f) => (
setFields({ ...fields, [f.key]: e.target.value })} /> {f.helpText &&

{f.helpText}

}
))}
)}
); } function ClusterToolsPanel({ clusters, selectedClusterId, onSelectCluster, }: { clusters: Cluster[]; selectedClusterId: string | null; onSelectCluster: (id: string) => void; }) { const clusterId = selectedClusterId || clusters.find((c) => c.isDefault)?.id || clusters[0]?.id; const { data: tools = [], isLoading, error } = useQuery({ queryKey: ['cluster-tools', clusterId], queryFn: () => api.get(`/clusters/${clusterId}/tools`).then((r) => r.data), enabled: !!clusterId, refetchInterval: (query) => (query.state.data || []).some((t) => t.status === 'installing') ? 8000 : 30000, }); return (

Tools Management

Install and manage infrastructure tools per cluster. Nothing is installed automatically — add only what each cluster needs.

{clusters.length > 1 && ( )}
{isLoading ? (

Loading tools…

) : error ? (

Failed to load tools for this cluster.

) : (
{clusterId && tools.map((tool) => ( ))}
)}
); } export default function AdminClustersPage() { const queryClient = useQueryClient(); const confirm = useConfirm(); const [showForm, setShowForm] = useState(false); const [testingId, setTestingId] = useState(null); const [expandedResources, setExpandedResources] = useState>(new Set()); const [loggingClusterId, setLoggingClusterId] = useState(null); const [form, setForm] = useState({ name: '', description: '', apiServer: '', kubeconfig: '', region: '', provider: '', weight: 1, tags: '', isDefault: false, }); const { data: clusters = [], isLoading } = useQuery({ queryKey: ['admin-clusters'], queryFn: () => api.get('/clusters').then((r) => r.data), }); const createMutation = useMutation({ mutationFn: (data: typeof form) => api.post('/clusters', { ...data, tags: data.tags.split(',').map((tag) => tag.trim()).filter(Boolean), weight: Number(data.weight) || 1, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-clusters'] }); toast.success('Cluster added & connection verified ✓'); setShowForm(false); setForm({ name: '', description: '', apiServer: '', kubeconfig: '', region: '', provider: '', weight: 1, tags: '', isDefault: false }); }, onError: (err: any) => { const message = err?.response?.data?.message || 'Failed to add cluster'; toast.error(message); }, }); const testMutation = useMutation({ mutationFn: (id: string) => { setTestingId(id); return api.post(`/clusters/${id}/test`); }, onSuccess: (res) => { queryClient.invalidateQueries({ queryKey: ['admin-clusters'] }); const data = res.data; if (data.connected) { toast.success(`Connection OK — Kubernetes ${data.version}`); } else { toast.error(`Connection failed: ${data.error}`); } setTestingId(null); }, onError: () => { toast.error('Failed to test connection'); setTestingId(null); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/clusters/${id}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-clusters'] }); toast.success('Cluster removed'); }, }); const toggleResources = (id: string) => { setExpandedResources((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; return (

Cluster Management

{clusters.length} cluster{clusters.length !== 1 ? 's' : ''} registered

{clusters.length > 0 && ( )} {showForm && (

Register New Cluster

The system will verify the Kubernetes connection before registering.

setForm({ ...form, name: e.target.value })} />
setForm({ ...form, apiServer: e.target.value })} />
setForm({ ...form, region: e.target.value })} />
setForm({ ...form, weight: Number(e.target.value) || 1 })} />
setForm({ ...form, tags: e.target.value })} />
setForm({ ...form, description: e.target.value })} />