task.tsx (4847B)
1 // web/src/screen/task.tsx - one task, its log, outputs and artifacts 2 3 import m from 'mithril'; 4 import { get, page, ApiError } from '../util/api'; 5 import { poll } from '../util/poll'; 6 import { ansi } from '../util/ansi'; 7 import { settled, duration, bytes, stamp } from '../util/format'; 8 import { Badge, Empty, Failed, Gone, Loading, When } from '../component/layout'; 9 10 type Log = { offset: number; size: number; data: string; expired: boolean }; 11 12 const FOLLOW_SLACK = 24; 13 14 export const Task: m.Component = { 15 oninit(vnode: any) { 16 const id = m.route.param('task'); 17 vnode.state.task = null; 18 vnode.state.log = ''; 19 vnode.state.at = 0; 20 vnode.state.expired = false; 21 vnode.state.outputs = []; 22 vnode.state.artifacts = []; 23 vnode.state.error = null; 24 vnode.state.gone = false; 25 vnode.state.follow = true; 26 27 vnode.state.load = async () => { 28 try { 29 const detail = await get<any>(`/tasks/${id}`); 30 vnode.state.task = detail.task; 31 32 const chunk = await get<Log>(`/tasks/${id}/log`, { offset: vnode.state.at }); 33 vnode.state.expired = chunk.expired; 34 if (chunk.data) { 35 vnode.state.log += chunk.data; 36 vnode.state.at = chunk.offset + new TextEncoder().encode(chunk.data).length; 37 } 38 39 if (settled(detail.task.state)) { 40 vnode.state.outputs = (await page<any>(`/tasks/${id}/outputs`, { query: { limit: 200 } })).rows; 41 vnode.state.artifacts = (await page<any>(`/tasks/${id}/artifacts`, { query: { limit: 200 } })).rows; 42 } 43 vnode.state.error = null; 44 } catch (e) { 45 if ((e as ApiError).status === 404) vnode.state.gone = true; 46 else vnode.state.error = e; 47 return false; 48 } 49 return !settled(vnode.state.task.state); 50 }; 51 52 vnode.state.poller = poll(vnode.state.load, 3000); 53 vnode.state.poller.start(); 54 }, 55 56 onremove: (vnode: any) => vnode.state.poller.stop(), 57 58 // Whether the reader was at the bottom has to be decided before the new 59 // bytes land, or the answer is always no. 60 onbeforeupdate(vnode: any) { 61 const pane = vnode.state.pane as HTMLElement | undefined; 62 if (pane) { 63 vnode.state.follow = pane.scrollTop + pane.clientHeight >= pane.scrollHeight - FOLLOW_SLACK; 64 } 65 return true; 66 }, 67 68 onupdate(vnode: any) { 69 const pane = vnode.state.pane as HTMLElement | undefined; 70 if (pane && vnode.state.follow) pane.scrollTop = pane.scrollHeight; 71 }, 72 73 view(vnode: any) { 74 const { task, error, gone } = vnode.state; 75 if (gone) return <Gone what="task" />; 76 if (error) return <Failed error={error} />; 77 if (!task) return <Loading />; 78 79 return [ 80 <h1> 81 <m.route.Link href={`/jobs/${task.job_id}`}>{`#${task.job_number}`}</m.route.Link> 82 {` ${task.name}`} 83 <Badge state={task.state} /> 84 </h1>, 85 86 <dl class="facts"> 87 <dt>image</dt><dd class="mono">{task.image}</dd> 88 {task.arch ? [<dt>arch</dt>, <dd>{task.arch}</dd>] : null} 89 <dt>worker</dt><dd>{task.worker_name ?? ''}</dd> 90 <dt>attempt</dt><dd>{`${task.attempt} of ${task.max_attempts}`}</dd> 91 {task.exit_code !== null ? [<dt>exit</dt>, <dd class="mono">{task.exit_code}</dd>] : null} 92 <dt>took</dt><dd>{duration(task.started_at, task.finished_at)}</dd> 93 <dt>started</dt><dd><When at={task.started_at} /></dd> 94 </dl>, 95 96 task.error ? <div class="error"><p>{task.error}</p></div> : null, 97 98 <h2>log</h2>, 99 vnode.state.expired 100 ? <p class="muted">This log has been removed by the retention policy.</p> 101 : ( 102 <pre class="log" oncreate={(v: any) => { vnode.state.pane = v.dom; v.dom.scrollTop = v.dom.scrollHeight; }}> 103 {vnode.state.log ? ansi(vnode.state.log) : <span class="muted">no output yet</span>} 104 </pre> 105 ), 106 107 <h2>outputs</h2>, 108 vnode.state.outputs.length === 0 109 ? <Empty what="outputs" /> 110 : ( 111 <table> 112 <tbody> 113 {vnode.state.outputs.map((o: any) => ( 114 <tr key={o.name}> 115 <td class="mono">{o.name}</td> 116 <td class="mono">{o.masked ? <span class="muted">masked</span> : o.value}</td> 117 </tr> 118 ))} 119 </tbody> 120 </table> 121 ), 122 123 <h2>artifacts</h2>, 124 vnode.state.artifacts.length === 0 125 ? <Empty what="artifacts" /> 126 : ( 127 <table> 128 <tbody> 129 {vnode.state.artifacts.map((a: any) => ( 130 <tr key={a.id}> 131 <td><a href={a.url}>{a.path}</a></td> 132 <td>{bytes(a.size)}</td> 133 <td title={stamp(a.expires_at)}>{a.expires_at ? 'expires' : ''}</td> 134 </tr> 135 ))} 136 </tbody> 137 </table> 138 ), 139 ]; 140 }, 141 };