'use client' import { useCallback, useEffect, useState } from 'react' import Link from 'next/link' import { type Container, parseDockerPs } from '@/lib/parse-docker' import { useFlowRun } from '@/hooks/useFlowRun' import ConfirmDialog from '@/components/ConfirmDialog' import StreamingTerminal from '@/components/StreamingTerminal' import { apiFetch } from '@/lib/csrf' async function fetchContainers(): Promise { const res = await apiFetch('/api/agent/exec', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command_key: 'docker_ps' }), }) if (!res.ok) { const text = await res.text() throw new Error(`agent ${res.status}: ${text}`) } const reader = res.body?.getReader() if (!reader) throw new Error('no response body') const decoder = new TextDecoder() let buffer = '' let output = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() ?? '' for (const line of lines) { if (line.startsWith('data:')) { try { const parsed = JSON.parse(line.slice(5).trim()) as { data?: string } if (parsed.data !== undefined) output += parsed.data } catch { // ignore malformed } } } } return parseDockerPs(output) } function statusBadge(status: string) { const up = status.toLowerCase().startsWith('up') return ( {status} ) } type ActionDef = { commandKey: string args: string[] preview: string title: string } type Props = { initialContainers: Container[] initialError: string | null } export default function DockerTable({ initialContainers, initialError }: Props) { const [containers, setContainers] = useState(initialContainers) const [error, setError] = useState(initialError) const [refreshing, setRefreshing] = useState(false) const [lastUpdated, setLastUpdated] = useState(new Date()) const [pendingAction, setPendingAction] = useState(null) const flowRun = useFlowRun() const refresh = useCallback(async () => { setRefreshing(true) try { const data = await fetchContainers() setContainers(data) setError(null) setLastUpdated(new Date()) } catch (err) { setError(err instanceof Error ? err.message : 'refresh failed') } finally { setRefreshing(false) } }, []) useEffect(() => { const id = setInterval(refresh, 5000) return () => clearInterval(id) }, [refresh]) const handleConfirm = useCallback(() => { if (!pendingAction) return flowRun.start(pendingAction.commandKey, pendingAction.args) setPendingAction(null) }, [pendingAction, flowRun.start]) return (
{containers.length} container{containers.length !== 1 ? 's' : ''} {refreshing && ( refreshing… )}
updated {lastUpdated.toLocaleTimeString()}
{error && (
{error}
)}
{containers.length === 0 && !error ? ( ) : ( containers.map((c) => ( )) )}
Name Image Status Ports Uptime Actions
No containers running
{c.name} {c.image} {statusBadge(c.status)} {c.ports || '—'} {c.created}
{flowRun.status !== 'idle' && (
Output {flowRun.status !== 'running' && ( )}
)} setPendingAction(null)} />
) }