import { useEffect, useMemo, useRef, useState } from 'react' import { api } from './api' import type { JobSnapshot } from './types' import { duration, humanBytes, Notice, Progress, StateDot } from './ui' export function Jobs({ jobs, activeJob, setActiveJob, reload, reloadPackages, }: { jobs: JobSnapshot[] activeJob: string setActiveJob: (id: string) => void reload: () => void reloadPackages: () => void }) { const selected = activeJob || jobs[0]?.id || '' if (jobs.length === 0) { return
no migrations yet — select containers and start one
} return (
{jobs.map((j) => (
setActiveJob(j.id)} >
{j.kind === 'ssh' ? 'ssh' : 'package'}
{j.title}
{new Date(j.createdAt).toLocaleTimeString()} · {duration(j.startedAt, j.endedAt)} {j.dryRun && ' · dry run'}
))}
{selected && }
) } function JobDetail({ id, reload, reloadPackages, }: { id: string reload: () => void reloadPackages: () => void }) { const [job, setJob] = useState(null) const [showCmds, setShowCmds] = useState(true) const logRef = useRef(null) const stick = useRef(true) // Each job detail holds one server-sent events stream, which pushes a full // snapshot whenever anything changes. useEffect(() => { setJob(null) let closed = false api.job(id).then((j) => !closed && setJob(j)).catch(() => undefined) const es = api.jobEvents(id) es.onmessage = (ev) => { try { setJob(JSON.parse(ev.data) as JobSnapshot) } catch { /* a truncated frame is replaced by the next one */ } } es.addEventListener('done', () => { es.close() reload() reloadPackages() }) es.onerror = () => es.close() return () => { closed = true es.close() } }, [id, reload, reloadPackages]) const lines = useMemo( () => (job?.log ?? []).filter((l) => showCmds || l.level !== 'cmd'), [job, showCmds], ) useEffect(() => { const el = logRef.current if (el && stick.current) el.scrollTop = el.scrollHeight }, [lines]) if (!job) return
loading…
const running = job.state === 'running' || job.state === 'pending' return (
{job.title} {job.dryRun && dry run} {humanBytes(job.bytesDone)} {job.bytesTotal > 0 && <> of {humanBytes(job.bytesTotal)}} · {duration(job.startedAt, job.endedAt)} {running ? ( ) : ( )}
{job.error && {job.error}} {job.state === 'succeeded' && job.artifact && ( Package ready at {job.artifact} ({humanBytes(job.artifactBytes ?? 0)}). {' '}Open the Packages tab to download it. )} {job.items.map((it) => (
{it.name} {it.steps.filter((s) => s.state === 'succeeded').length}/{it.steps.length} steps
{it.error &&
{it.error}
} {(it.warnings ?? []).map((w, i) => (
! {w}
))}
{it.steps.map((s) => (
{s.label} {s.bytesTotal > 0 || s.bytesDone > 0 ? ( ) : null} {s.bytesDone > 0 ? humanBytes(s.bytesDone) : s.state === 'skipped' ? 'skipped' : ''}
))}
))}

log

{ const el = e.currentTarget stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24 }} > {lines.map((l) => (
{new Date(l.at).toLocaleTimeString()} {l.message}
))} {lines.length === 0 && nothing logged yet}
) }