workers.tsx (5733B)
1 // web/src/screen/workers.tsx - registered workers, and one worker's machines 2 3 import m from 'mithril'; 4 import { get, page, patch, post, del, ApiError } from '../util/api'; 5 import { Badge, Empty, Failed, Gone, Loading, Pager, When } from '../component/layout'; 6 import { duration } from '../util/format'; 7 8 const LIMIT = 25; 9 10 export const Workers: m.Component = { 11 oninit(vnode: any) { 12 vnode.state.offset = 0; 13 vnode.state.rows = null; 14 vnode.state.error = null; 15 vnode.state.token = null; 16 vnode.state.name = ''; 17 18 vnode.state.load = async () => { 19 try { 20 vnode.state.rows = await page<any>('/workers', { 21 query: { offset: vnode.state.offset, limit: LIMIT }, 22 }); 23 vnode.state.error = null; 24 } catch (e) { 25 vnode.state.error = e; 26 } 27 m.redraw(); 28 }; 29 vnode.state.load(); 30 }, 31 32 view(vnode: any) { 33 const { rows, error } = vnode.state; 34 if (error && !rows) return <Failed error={error} />; 35 if (!rows) return <Loading />; 36 37 const create = async (e: Event) => { 38 e.preventDefault(); 39 try { 40 vnode.state.token = await post<any>('/workers', { name: vnode.state.name }); 41 vnode.state.name = ''; 42 await vnode.state.load(); 43 } catch (err) { 44 vnode.state.error = err; 45 } 46 }; 47 48 return [ 49 <h1>workers</h1>, 50 error ? <Failed error={error} /> : null, 51 52 vnode.state.token 53 ? ( 54 <div class="reveal"> 55 <strong>{`Token for ${vnode.state.token.name}`}</strong> 56 <p class="muted">Copy it now. It cannot be shown again.</p> 57 <code class="mono">{vnode.state.token.token}</code> 58 <button onclick={() => { vnode.state.token = null; }}>done</button> 59 </div> 60 ) 61 : null, 62 63 <form class="panel" onsubmit={create}> 64 <label> 65 <span>name</span> 66 <input required value={vnode.state.name} oninput={(e: any) => { vnode.state.name = e.target.value; }} /> 67 </label> 68 <button type="submit">register a worker</button> 69 </form>, 70 71 rows.rows.length === 0 ? <Empty what="workers" /> : ( 72 <table> 73 <thead><tr><th>worker</th><th>enabled</th><th>last seen</th><th>address</th></tr></thead> 74 <tbody> 75 {rows.rows.map((w: any) => ( 76 <tr key={w.id}> 77 <td><m.route.Link href={`/workers/${w.id}`}>{w.name}</m.route.Link></td> 78 <td>{w.enabled ? 'yes' : 'no'}</td> 79 <td><When at={w.last_seen_at} /></td> 80 <td class="mono">{w.last_ip ?? ''}</td> 81 </tr> 82 ))} 83 </tbody> 84 </table> 85 ), 86 87 <Pager 88 offset={rows.offset} 89 total={rows.total} 90 limit={LIMIT} 91 onmove={(offset: number) => { vnode.state.offset = offset; vnode.state.load(); }} 92 />, 93 ]; 94 }, 95 }; 96 97 export const Worker: m.Component = { 98 oninit(vnode: any) { 99 const id = m.route.param('worker'); 100 vnode.state.id = id; 101 vnode.state.worker = null; 102 vnode.state.agents = []; 103 vnode.state.tasks = []; 104 vnode.state.error = null; 105 vnode.state.gone = false; 106 107 vnode.state.load = async () => { 108 try { 109 vnode.state.worker = await get<any>(`/workers/${id}`); 110 vnode.state.agents = await get<any[]>(`/workers/${id}/agents`); 111 vnode.state.tasks = (await page<any>(`/workers/${id}/tasks`, { query: { limit: 25 } })).rows; 112 vnode.state.error = null; 113 } catch (e) { 114 if ((e as ApiError).status === 404) vnode.state.gone = true; 115 else vnode.state.error = e; 116 } 117 m.redraw(); 118 }; 119 vnode.state.load(); 120 }, 121 122 view(vnode: any) { 123 const { worker, error, gone, id } = vnode.state; 124 if (gone) return <Gone what="worker" />; 125 if (error && !worker) return <Failed error={error} />; 126 if (!worker) return <Loading />; 127 128 return [ 129 <h1>{worker.name}</h1>, 130 error ? <Failed error={error} /> : null, 131 132 <div class="actions"> 133 <label class="check"> 134 <input 135 type="checkbox" 136 checked={worker.enabled === 1} 137 onchange={async (e: any) => { 138 vnode.state.worker = await patch<any>(`/workers/${id}`, { enabled: e.target.checked }); 139 }} 140 /> 141 <span>enabled</span> 142 </label> 143 <button 144 onclick={async () => { 145 await del(`/workers/${id}`); 146 m.route.set('/workers'); 147 }} 148 > 149 remove 150 </button> 151 </div>, 152 153 <h2>machines</h2>, 154 vnode.state.agents.length === 0 ? <Empty what="machines" /> : ( 155 <table> 156 <thead><tr><th>machine</th><th>architectures</th><th>features</th><th>last seen</th></tr></thead> 157 <tbody> 158 {vnode.state.agents.map((a: any) => ( 159 <tr key={a.id}> 160 <td>{a.name}</td> 161 <td class="mono">{(a.arches ?? []).join(', ')}</td> 162 <td class="mono">{(a.features ?? []).join(', ')}</td> 163 <td><When at={a.last_seen_at} /></td> 164 </tr> 165 ))} 166 </tbody> 167 </table> 168 ), 169 170 <h2>recent tasks</h2>, 171 vnode.state.tasks.length === 0 ? <Empty what="tasks" /> : ( 172 <table> 173 <tbody> 174 {vnode.state.tasks.map((t: any) => ( 175 <tr key={t.id}> 176 <td><m.route.Link href={`/tasks/${t.id}`}>{t.name}</m.route.Link></td> 177 <td><Badge state={t.state} /></td> 178 <td>{duration(t.started_at, t.finished_at)}</td> 179 <td><When at={t.created_at} /></td> 180 </tr> 181 ))} 182 </tbody> 183 </table> 184 ), 185 ]; 186 }, 187 };