conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 };