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
94 lines
4.2 KiB
TypeScript
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>
|
|
)
|
|
}
|