Files
server-manager/frontend/src/pages/Services.tsx
Ernie Butcher 4233734759
All checks were successful
continuous-integration/drone/push Build is passing
feat: live Docker data — containers, services, logs, metrics via dockerode
- 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
2026-03-18 18:33:25 -04:00

94 lines
4.2 KiB
TypeScript

import { useQuery, useQueryClient } from '@tanstack/react-query'
import { api } from '@/lib/api'
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 stacks = data?.services ?? []
return (
<div className="space-y-4">
<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>
)
}