All checks were successful
Build and Push Docker Images / docker (push) Successful in 25s
240 lines
10 KiB
JavaScript
240 lines
10 KiB
JavaScript
import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp, RefreshCw, Cpu, MemoryStick, ArrowUp, ArrowDown, Pencil, BarChart2, CloudDownload, Copy, Check } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import ContainerRow from './ContainerRow'
|
|
import { tagColor } from './TagInput'
|
|
|
|
function formatBytes(bps) {
|
|
if (bps < 1024) return `${bps.toFixed(0)} B/s`
|
|
if (bps < 1024 * 1024) return `${(bps / 1024).toFixed(1)} KB/s`
|
|
return `${(bps / 1024 / 1024).toFixed(1)} MB/s`
|
|
}
|
|
|
|
function formatRam(bytes) {
|
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(0)} MB`
|
|
return `${(bytes / 1024 ** 3).toFixed(1)} GB`
|
|
}
|
|
|
|
export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onEdit, onStats, onUpdateAgent, onExport }) {
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
const [updatingProject, setUpdatingProject] = useState(null)
|
|
const [updatingAgent, setUpdatingAgent] = useState(false)
|
|
const [exported, setExported] = useState(false)
|
|
|
|
const handleExport = async () => {
|
|
await onExport(vps.id)
|
|
setExported(true)
|
|
setTimeout(() => setExported(false), 2000)
|
|
}
|
|
|
|
const running = vps.containers.filter(c => c.status === 'running').length
|
|
const total = vps.containers.length
|
|
|
|
const composeProjects = [...new Set(vps.containers.map(c => c.compose_project).filter(Boolean))]
|
|
|
|
const handleUpdate = async (project) => {
|
|
setUpdatingProject(project)
|
|
try { await onUpdate(vps.id, project) } finally { setUpdatingProject(null) }
|
|
}
|
|
|
|
const handleUpdateAgent = async () => {
|
|
setUpdatingAgent(true)
|
|
try { await onUpdateAgent(vps.id) } finally { setUpdatingAgent(false) }
|
|
}
|
|
|
|
return (
|
|
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-gray-800">
|
|
<div className={`p-2 rounded-lg flex-shrink-0 ${vps.online ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
|
<Server size={15} className={vps.online ? 'text-emerald-400' : 'text-red-400'} />
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-sm truncate">{vps.name}</h3>
|
|
<p className="text-xs text-gray-500 truncate">{vps.host}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{vps.online ? (
|
|
<span className="flex items-center gap-1.5 text-xs text-emerald-400">
|
|
<Wifi size={12} />
|
|
<span className="hidden sm:inline">{running}/{total} actifs</span>
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-1.5 text-xs text-red-400">
|
|
<WifiOff size={12} />
|
|
<span className="hidden sm:inline">Hors ligne</span>
|
|
</span>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => setCollapsed(c => !c)}
|
|
className="p-1.5 rounded hover:bg-gray-800 text-gray-500 hover:text-gray-300 transition-colors"
|
|
title={collapsed ? 'Déplier' : 'Replier'}
|
|
>
|
|
{collapsed ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
|
</button>
|
|
|
|
{vps.online && (
|
|
<button
|
|
onClick={() => onStats(vps.id, vps.name)}
|
|
className="p-1.5 rounded hover:bg-gray-800 text-gray-500 hover:text-indigo-400 transition-colors"
|
|
title="Graphiques de performance"
|
|
>
|
|
<BarChart2 size={14} />
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleExport}
|
|
className={`p-1.5 rounded hover:bg-gray-800 transition-colors ${exported ? 'text-emerald-400' : 'text-gray-500 hover:text-gray-300'}`}
|
|
title={exported ? 'Config copiée !' : 'Exporter la config (copier JSON)'}
|
|
>
|
|
{exported ? <Check size={14} /> : <Copy size={14} />}
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => onEdit(vps)}
|
|
className="p-1.5 rounded hover:bg-gray-800 text-gray-500 hover:text-gray-300 transition-colors"
|
|
title="Modifier ce VPS"
|
|
>
|
|
<Pencil size={14} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => onDelete(vps.id)}
|
|
className="p-1.5 rounded hover:bg-red-500/20 text-gray-500 hover:text-red-400 transition-colors"
|
|
title="Supprimer ce VPS"
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Erreur de connexion */}
|
|
{!vps.online && vps.error && (
|
|
<div className="px-4 py-2.5 bg-red-950/30 border-b border-red-900/30 text-xs text-red-400 font-mono">
|
|
{vps.error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Version de l'agent + bouton mise à jour */}
|
|
{vps.online && (
|
|
<div className="px-4 py-2 border-b border-gray-800/60 flex items-center gap-3">
|
|
<span className="text-xs text-gray-500">Agent :</span>
|
|
{vps.agent_version ? (
|
|
<span
|
|
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border ${
|
|
vps.agent_up_to_date
|
|
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
|
|
: 'bg-orange-500/10 border-orange-500/30 text-orange-400'
|
|
}`}
|
|
title={vps.agent_up_to_date ? 'Agent à jour' : `Mise à jour disponible (attendu : ${vps.expected_agent_version})`}
|
|
>
|
|
{vps.agent_up_to_date ? '✓' : '⚠'} v{vps.agent_version}
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border bg-gray-700/30 border-gray-600/30 text-gray-500">
|
|
inconnu
|
|
</span>
|
|
)}
|
|
{(!vps.agent_up_to_date || vps.agent_version === 'unknown') && (
|
|
<button
|
|
onClick={handleUpdateAgent}
|
|
disabled={updatingAgent}
|
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs bg-orange-500/10 border border-orange-500/30 text-orange-300 hover:bg-orange-500/30 hover:text-orange-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title={`Mettre à jour l'agent vers v${vps.expected_agent_version}`}
|
|
>
|
|
<CloudDownload size={11} className={updatingAgent ? 'animate-bounce' : ''} />
|
|
{updatingAgent ? 'Mise à jour...' : 'Mettre à jour l\'agent'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Boutons de mise à jour par projet compose */}
|
|
{vps.online && composeProjects.length > 0 && (
|
|
<div className="px-4 py-2 border-b border-gray-800/60 flex flex-wrap gap-2">
|
|
{composeProjects.map(project => (
|
|
<button
|
|
key={project}
|
|
onClick={() => handleUpdate(project)}
|
|
disabled={!!updatingProject}
|
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs bg-indigo-600/20 border border-indigo-500/30 text-indigo-300 hover:bg-indigo-600/40 hover:text-indigo-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title={`docker compose pull && up -d (${project})`}
|
|
>
|
|
<RefreshCw size={11} className={updatingProject === project ? 'animate-spin' : ''} />
|
|
Update {project}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Informations système */}
|
|
{vps.online && vps.system && !collapsed && (
|
|
<div className="px-4 py-2 border-b border-gray-800/60 flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-400 bg-gray-900/50">
|
|
<span className="flex items-center gap-1">
|
|
<Cpu size={11} className="text-indigo-400" />
|
|
CPU <span className={`font-medium ml-0.5 ${vps.system.cpu_percent > 80 ? 'text-red-400' : vps.system.cpu_percent > 60 ? 'text-yellow-400' : 'text-emerald-400'}`}>{vps.system.cpu_percent.toFixed(1)}%</span>
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<MemoryStick size={11} className="text-indigo-400" />
|
|
RAM <span className={`font-medium ml-0.5 ${vps.system.ram_percent > 80 ? 'text-red-400' : vps.system.ram_percent > 60 ? 'text-yellow-400' : 'text-emerald-400'}`}>{formatRam(vps.system.ram_used)}/{formatRam(vps.system.ram_total)}</span>
|
|
<span className="text-gray-600">({vps.system.ram_percent.toFixed(0)}%)</span>
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<ArrowUp size={11} className="text-sky-400" />{formatBytes(vps.system.net_sent_per_sec)}
|
|
<ArrowDown size={11} className="text-violet-400 ml-1" />{formatBytes(vps.system.net_recv_per_sec)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tags */}
|
|
{vps.tags?.length > 0 && !collapsed && (
|
|
<div className="px-4 py-2 border-b border-gray-800/60 flex flex-wrap gap-1.5">
|
|
{vps.tags.map(tag => (
|
|
<span
|
|
key={tag}
|
|
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${tagColor(tag)}`}
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Description */}
|
|
{vps.description && !collapsed && (
|
|
<p className="px-4 py-2 text-xs text-gray-500 border-b border-gray-800/60">{vps.description}</p>
|
|
)}
|
|
|
|
{/* Conteneurs */}
|
|
{!collapsed && vps.online && (
|
|
<div className="divide-y divide-gray-800/50 flex-1">
|
|
{total === 0 ? (
|
|
<p className="px-4 py-6 text-sm text-gray-600 text-center">Aucun conteneur détecté.</p>
|
|
) : (
|
|
vps.containers.map(c => (
|
|
<ContainerRow
|
|
key={c.id}
|
|
container={c}
|
|
onAction={(action) => onAction(vps.id, c.id, action)}
|
|
onLogs={() => onLogs(vps.id, c.id, `${vps.name} / ${c.name}`)}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer stats */}
|
|
{!collapsed && vps.online && total > 0 && (
|
|
<div className="px-4 py-2 border-t border-gray-800/60 flex gap-4 text-xs text-gray-600">
|
|
<span><span className="text-emerald-500">{running}</span> en cours</span>
|
|
<span><span className="text-gray-400">{total - running}</span> arrêtés</span>
|
|
<span><span className="text-gray-400">{total}</span> total</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|