Files
hamlab/web/src/TaskOutput.tsx
T

49 lines
2.3 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import { api, TaskInfo, wsURLPath } from './api'
import { Btn, Spinner } from './ui'
// Follows one background task: backlog, then live chunks over a WebSocket, until it exits.
export default function TaskOutput({ id, onDone }: { id: string; onDone?: (t: TaskInfo) => void }) {
const [task, setTask] = useState<TaskInfo | null>(null)
const [text, setText] = useState('')
const [connected, setConnected] = useState(false)
const box = useRef<HTMLPreElement>(null)
const doneRef = useRef(onDone)
doneRef.current = onDone
useEffect(() => {
setText(''); setTask(null); setConnected(false)
api.task(id).then(setTask).catch(() => { /* shown as unknown */ })
const s = new WebSocket(wsURLPath(`/api/tasks/${id}/stream`))
s.binaryType = 'arraybuffer'
const dec = new TextDecoder()
s.onopen = () => setConnected(true)
s.onmessage = (ev) => {
if (ev.data instanceof ArrayBuffer) { setText((t) => t + dec.decode(ev.data, { stream: true })); return }
try {
const m = JSON.parse(ev.data)
if (m.type === 'exit') api.task(id).then((t) => { setTask(t); doneRef.current?.(t) }).catch(() => { /* ignore */ })
} catch { /* ignore */ }
}
return () => s.close()
}, [id])
useEffect(() => { if (box.current) box.current.scrollTop = box.current.scrollHeight }, [text])
const running = !task || task.running
const failed = !!task && !task.running && (task.exit_code !== 0 || !!task.error)
const status = !task ? 'starting…' : task.running ? 'running…' : task.error ? `failed: ${task.error}` : task.exit_code === 0 ? 'finished' : `exited ${task.exit_code}`
return (
<div className={'task' + (running ? ' running' : failed ? ' failed' : ' done')}>
<div className="logs-bar">
{running ? <Spinner /> : <span className={'dot ' + (failed ? 'bad' : 'ok')} />}
<span>{task?.title ?? '…'}</span>
<span className="muted">· {status}</span>
<span className="spacer" />
{task?.running && <Btn className="btn" onClick={() => api.cancelTask(id)}>Cancel</Btn>}
</div>
<pre className="run-out task-out" ref={box}>{text || (running ? (connected ? 'waiting for output…' : 'connecting…') : '(no output)')}</pre>
</div>
)
}