feat: live Docker data — containers, services, logs, metrics via dockerode
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Backend: rewrote containers/services/logs/metrics routes to use dockerode - docker-compose: mount /var/run/docker.sock, run backend as root - .drone.yml: sync docker-compose.yml from Gitea on deploy - Frontend: Containers page shows real data with wired start/stop/restart - Frontend: Services page shows Docker Compose stacks with health status - Frontend: Metrics page adds disk (docker df) and containers cards + chart legend - Frontend: Logs page replaces text input with container dropdown + auto-refresh
This commit is contained in:
@@ -1,50 +1,93 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api'
|
||||
import { Layers } from 'lucide-react'
|
||||
import { Layers, CheckCircle, AlertTriangle, XCircle, RefreshCw } from 'lucide-react'
|
||||
|
||||
function StackStatus({ status }: { status: string }) {
|
||||
const cfg: Record<string, { icon: any; color: string; bg: string }> = {
|
||||
healthy: { icon: CheckCircle, color: 'text-green-400', bg: 'bg-green-500/10' },
|
||||
degraded: { icon: AlertTriangle, color: 'text-amber-400', bg: 'bg-amber-500/10' },
|
||||
stopped: { icon: XCircle, color: 'text-red-400', bg: 'bg-red-500/10' },
|
||||
unknown: { icon: Layers, color: 'text-zinc-400', bg: 'bg-zinc-500/10' },
|
||||
}
|
||||
const { icon: Icon, color, bg } = cfg[status] ?? cfg.unknown
|
||||
return (
|
||||
<span className={`flex items-center gap-1.5 text-xs px-2 py-1 rounded w-fit ${bg} ${color}`}>
|
||||
<Icon className="w-3 h-3" /> {status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Services() {
|
||||
const qc = useQueryClient()
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['services'],
|
||||
queryFn: () => api.get('/services').then(r => r.data),
|
||||
refetchInterval: 15000,
|
||||
})
|
||||
const services = data?.services ?? []
|
||||
|
||||
const stacks = data?.services ?? []
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-[hsl(217.2_32.6%_17.5%)] bg-[hsl(222.2_84%_6%)] overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-[hsl(217.2_32.6%_17.5%)]">
|
||||
{['Service', 'Type', 'Port', 'Status'].map(h => (
|
||||
<th key={h} className="text-left px-5 py-3 text-xs font-medium text-[hsl(215_20.2%_65.1%)] uppercase tracking-wider">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr><td colSpan={4} className="text-center py-10 text-sm text-[hsl(215_20.2%_65.1%)]">Loading...</td></tr>
|
||||
) : services.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="py-12 text-center">
|
||||
<Layers className="w-10 h-10 mx-auto mb-3 text-[hsl(215_20.2%_40%)]" />
|
||||
<p className="text-sm text-[hsl(215_20.2%_65.1%)]">No services configured yet.</p>
|
||||
</td>
|
||||
</tr>
|
||||
) : services.map((s: any) => (
|
||||
<tr key={s.id} className="border-t border-[hsl(217.2_32.6%_17.5%)] hover:bg-[hsl(217.2_32.6%_10%)]">
|
||||
<td className="px-5 py-4 text-sm font-medium text-white">{s.name}</td>
|
||||
<td className="px-5 py-4 text-sm text-[hsl(215_20.2%_65.1%)]">{s.type}</td>
|
||||
<td className="px-5 py-4 text-sm text-[hsl(215_20.2%_65.1%)]">{s.port ?? '—'}</td>
|
||||
<td className="px-5 py-4">
|
||||
<span className={`text-xs px-2 py-1 rounded ${s.status === 'active' ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}`}>
|
||||
{s.status ?? 'unknown'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-[hsl(215_20.2%_65.1%)]">{stacks.length} stack{stacks.length !== 1 ? 's' : ''}</p>
|
||||
<button
|
||||
onClick={() => qc.invalidateQueries({ queryKey: ['services'] })}
|
||||
className="p-2 rounded-lg text-[hsl(215_20.2%_65.1%)] hover:bg-[hsl(217.2_32.6%_17.5%)] hover:text-white transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="text-center py-10 text-[hsl(215_20.2%_65.1%)] text-sm">Loading...</div>
|
||||
)}
|
||||
{!isLoading && stacks.length === 0 && (
|
||||
<div className="py-12 text-center">
|
||||
<Layers className="w-10 h-10 mx-auto mb-3 text-[hsl(215_20.2%_40%)]" />
|
||||
<p className="text-sm text-[hsl(215_20.2%_65.1%)]">No services found.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stacks.map((stack: any) => (
|
||||
<div key={stack.id} className="rounded-xl border border-[hsl(217.2_32.6%_17.5%)] bg-[hsl(222.2_84%_6%)] overflow-hidden">
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[hsl(217.2_32.6%_17.5%)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<Layers className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm font-semibold text-white">{stack.name}</span>
|
||||
<span className="text-xs text-[hsl(215_20.2%_65.1%)] px-2 py-0.5 rounded bg-[hsl(217.2_32.6%_17.5%)]">{stack.type}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-[hsl(215_20.2%_65.1%)]">{stack.running}/{stack.total} running</span>
|
||||
<StackStatus status={stack.status} />
|
||||
</div>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-[hsl(217.2_32.6%_17.5%)]">
|
||||
{['Service', 'Container', 'Image', 'State'].map(h => (
|
||||
<th key={h} className="text-left px-5 py-2 text-xs font-medium text-[hsl(215_20.2%_40%)] uppercase tracking-wider">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stack.services.map((svc: any) => (
|
||||
<tr key={svc.name} className="border-t border-[hsl(217.2_32.6%_17.5%)] hover:bg-[hsl(217.2_32.6%_10%)]">
|
||||
<td className="px-5 py-3 text-sm font-medium text-white">{svc.name}</td>
|
||||
<td className="px-5 py-3 text-xs text-[hsl(215_20.2%_65.1%)] font-mono">{svc.container}</td>
|
||||
<td className="px-5 py-3 text-xs text-[hsl(215_20.2%_65.1%)] font-mono max-w-[200px] truncate" title={svc.image}>{svc.image}</td>
|
||||
<td className="px-5 py-3">
|
||||
<span className={`text-xs px-2 py-1 rounded ${svc.state === 'running' ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}`}>
|
||||
{svc.state}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user