feat : update agent 2
Some checks failed
Build and Push Docker Images / docker (push) Failing after 8s
Some checks failed
Build and Push Docker Images / docker (push) Failing after 8s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { fetchAllStatus, containerAction, addVps, deleteVps, fetchLogs, authStatus, getToken, setToken } from './api/client'
|
||||
import { fetchAllStatus, containerAction, addVps, deleteVps, fetchLogs, authStatus, getToken, setToken, composeUpdate } from './api/client'
|
||||
import Header from './components/Header'
|
||||
import VpsCard from './components/VpsCard'
|
||||
import LogsModal from './components/LogsModal'
|
||||
@@ -24,6 +24,10 @@ export default function App() {
|
||||
const [logsLoading, setLogsLoading] = useState(false)
|
||||
const [showAddVps, setShowAddVps] = useState(false)
|
||||
|
||||
const [updateModal, setUpdateModal] = useState(null) // { vpsId, project }
|
||||
const [updateContent, setUpdateContent] = useState('')
|
||||
const [updateLoading, setUpdateLoading] = useState(false)
|
||||
|
||||
// Vérifie si des utilisateurs existent (pour afficher login ou register)
|
||||
useEffect(() => {
|
||||
authStatus()
|
||||
@@ -114,6 +118,21 @@ export default function App() {
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const handleUpdate = async (vpsId, project) => {
|
||||
setUpdateModal({ vpsId, project })
|
||||
setUpdateLoading(true)
|
||||
setUpdateContent('')
|
||||
try {
|
||||
const data = await composeUpdate(vpsId, project)
|
||||
setUpdateContent(data.output || '(aucune sortie)')
|
||||
} catch (e) {
|
||||
setUpdateContent(`Erreur lors de la mise à jour :\n${e.message}`)
|
||||
} finally {
|
||||
setUpdateLoading(false)
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddVps = async (formData) => {
|
||||
await addVps(formData)
|
||||
setShowAddVps(false)
|
||||
@@ -214,6 +233,7 @@ export default function App() {
|
||||
onAction={handleAction}
|
||||
onLogs={openLogs}
|
||||
onDelete={handleDeleteVps}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -230,6 +250,16 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Modal mise à jour compose */}
|
||||
{updateModal && (
|
||||
<LogsModal
|
||||
name={`🔄 Update — ${updateModal.project}`}
|
||||
logs={updateContent}
|
||||
loading={updateLoading}
|
||||
onClose={() => setUpdateModal(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Modal ajout VPS */}
|
||||
{showAddVps && (
|
||||
<AddVpsModal
|
||||
|
||||
@@ -90,3 +90,12 @@ export async function deleteVps(vpsId) {
|
||||
})
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
export async function composeUpdate(vpsId, project) {
|
||||
const res = await fetch(`${BASE}/vps/${vpsId}/compose/update`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ project }),
|
||||
})
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import { useState } from 'react'
|
||||
import { Play, Square, RotateCcw, FileText, Loader2 } from 'lucide-react'
|
||||
import { Play, Square, RotateCcw, FileText, Loader2, Heart } from 'lucide-react'
|
||||
import StatusBadge from './StatusBadge'
|
||||
|
||||
const HEALTH_STYLES = {
|
||||
healthy: { dot: 'bg-emerald-400', text: 'text-emerald-400', bg: 'bg-emerald-500/10 border-emerald-500/20', label: 'healthy' },
|
||||
unhealthy: { dot: 'bg-red-400', text: 'text-red-400', bg: 'bg-red-500/10 border-red-500/20', label: 'unhealthy' },
|
||||
starting: { dot: 'bg-yellow-400 animate-pulse', text: 'text-yellow-400', bg: 'bg-yellow-500/10 border-yellow-500/20', label: 'starting' },
|
||||
}
|
||||
|
||||
function HealthBadge({ health }) {
|
||||
const s = HEALTH_STYLES[health]
|
||||
if (!s) return null
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs font-medium border ${s.bg} ${s.text}`}>
|
||||
<Heart size={9} className="flex-shrink-0" />
|
||||
{s.label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ContainerRow({ container, onAction, onLogs }) {
|
||||
const [pending, setPending] = useState(null)
|
||||
const isRunning = container.status === 'running'
|
||||
@@ -16,8 +33,7 @@ export default function ContainerRow({ container, onAction, onLogs }) {
|
||||
<div className="min-w-0 flex-1 pr-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium truncate max-w-[160px]">{container.name}</span>
|
||||
<StatusBadge status={container.status} />
|
||||
{container.compose_project && (
|
||||
<StatusBadge status={container.status} /> <HealthBadge health={container.health} /> {container.compose_project && (
|
||||
<span className="hidden sm:inline text-xs text-gray-600 bg-gray-800 px-1.5 py-0.5 rounded">
|
||||
{container.compose_project}
|
||||
</span>
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp, RefreshCw, Cpu, MemoryStick, ArrowUp, ArrowDown } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import ContainerRow from './ContainerRow'
|
||||
|
||||
export default function VpsCard({ vps, onAction, onLogs, onDelete }) {
|
||||
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 }) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [updatingProject, setUpdatingProject] = useState(null)
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
@@ -59,6 +78,25 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete }) {
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{vps.description && !collapsed && (
|
||||
<p className="px-4 py-2 text-xs text-gray-500 border-b border-gray-800/60">{vps.description}</p>
|
||||
@@ -82,6 +120,24 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Boutons de mise à jour par projet compose */}
|
||||
{!collapsed && vps.online && composeProjects.length > 0 && (
|
||||
<div className="px-4 py-2 border-t 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>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
|
||||
Reference in New Issue
Block a user