project.tsx (9555B)
1 // web/src/screen/project.tsx - one project: settings, variables, jobs 2 3 import m from 'mithril'; 4 import { get, page, patch, post, put, del, ApiError } from '../util/api'; 5 import { Badge, Empty, Failed, Gone, Loading, When, manages } from '../component/layout'; 6 import { duration, shortSha } from '../util/format'; 7 8 // Empty means follow the server default, zero means keep forever. They are 9 // different answers and the form has to keep them apart. 10 function retentionValue(raw: string): number | null { 11 const trimmed = raw.trim(); 12 return trimmed === '' ? null : Number(trimmed); 13 } 14 15 export const Project: m.Component = { 16 oninit(vnode: any) { 17 const id = m.route.param('project'); 18 vnode.state.id = id; 19 vnode.state.project = null; 20 vnode.state.variables = []; 21 vnode.state.jobs = []; 22 vnode.state.error = null; 23 vnode.state.gone = false; 24 vnode.state.secret = null; 25 vnode.state.newVar = { name: '', value: '', masked: true }; 26 vnode.state.values = {}; 27 vnode.state.shown = {}; 28 29 vnode.state.load = async () => { 30 try { 31 vnode.state.project = await get<any>(`/projects/${id}`); 32 vnode.state.jobs = (await page<any>(`/projects/${id}/jobs`, { query: { limit: 10 } })).rows; 33 vnode.state.values = {}; 34 vnode.state.shown = {}; 35 vnode.state.variables = manages(vnode.state.project) 36 ? await get<any[]>(`/projects/${id}/variables`) 37 : null; 38 vnode.state.error = null; 39 } catch (e) { 40 if ((e as ApiError).status === 404) vnode.state.gone = true; 41 else vnode.state.error = e; 42 } 43 m.redraw(); 44 }; 45 vnode.state.load(); 46 }, 47 48 view(vnode: any) { 49 const { project, error, gone, id } = vnode.state; 50 if (gone) return <Gone what="project" />; 51 if (error && !project) return <Failed error={error} />; 52 if (!project) return <Loading />; 53 54 const save = async (body: unknown) => { 55 try { 56 vnode.state.project = await patch<any>(`/projects/${id}`, body); 57 vnode.state.error = null; 58 } catch (e) { 59 vnode.state.error = e; 60 } 61 }; 62 63 const rotate = async () => { 64 try { 65 vnode.state.secret = (await post<any>(`/projects/${id}/trigger-secret`)).trigger_secret; 66 } catch (e) { 67 vnode.state.error = e; 68 } 69 }; 70 71 const setVariable = async (e: Event) => { 72 e.preventDefault(); 73 const { name, value, masked } = vnode.state.newVar; 74 try { 75 await put(`/projects/${id}/variables/${encodeURIComponent(name)}`, { value, masked }); 76 vnode.state.newVar = { name: '', value: '', masked: true }; 77 await vnode.state.load(); 78 } catch (err) { 79 vnode.state.error = err; 80 } 81 }; 82 83 const valueOf = async (name: string): Promise<string> => { 84 if (vnode.state.values[name] === undefined) { 85 const got = await get<any>(`/projects/${id}/variables/${encodeURIComponent(name)}/value`); 86 vnode.state.values[name] = got.value; 87 } 88 return vnode.state.values[name]; 89 }; 90 91 const toggleValue = async (name: string) => { 92 if (vnode.state.shown[name]) { 93 vnode.state.shown[name] = false; 94 return; 95 } 96 try { 97 await valueOf(name); 98 vnode.state.shown[name] = true; 99 } catch (e) { 100 vnode.state.error = e; 101 } 102 }; 103 104 const copyValue = async (name: string) => { 105 try { 106 const value = await valueOf(name); 107 // Undefined outside a secure context, so show the value instead of 108 // failing on a plain http deployment. 109 if (!navigator.clipboard) vnode.state.shown[name] = true; 110 else await navigator.clipboard.writeText(value); 111 } catch (e) { 112 vnode.state.error = e; 113 } 114 }; 115 116 const retention = (key: string, label: string) => ( 117 <label> 118 <span>{label}</span> 119 <input 120 type="number" 121 min="0" 122 placeholder="server default" 123 value={project[key] === null ? '' : project[key]} 124 onchange={(e: any) => save({ [key]: retentionValue(e.target.value) })} 125 /> 126 </label> 127 ); 128 129 const mine = manages(project); 130 131 return [ 132 <h1>{project.name}</h1>, 133 error ? <Failed error={error} /> : null, 134 135 <section class="panel"> 136 <h2>settings</h2> 137 <dl class="facts"> 138 <dt>repository</dt><dd class="mono">{project.repo_url}</dd> 139 <dt>branch</dt><dd class="ref">{project.default_branch}</dd> 140 <dt>config</dt><dd class="mono">{project.config_path}</dd> 141 </dl> 142 {mine ? ( 143 <label> 144 <span>visibility</span> 145 <select value={project.visibility} onchange={(e: any) => save({ visibility: e.target.value })}> 146 <option value="private">private</option> 147 <option value="public">public</option> 148 </select> 149 </label> 150 ) : null} 151 {mine ? ( 152 <label class="check"> 153 <input 154 type="checkbox" 155 checked={project.enabled === 1} 156 onchange={(e: any) => save({ enabled: e.target.checked })} 157 /> 158 <span>enabled</span> 159 </label> 160 ) : null} 161 </section>, 162 163 mine ? ( 164 <section class="panel"> 165 <h2>retention</h2> 166 <p class="muted">Empty follows the server default. Zero keeps forever.</p> 167 {retention('artifact_keep_jobs', 'artifact jobs')} 168 {retention('artifact_keep_days', 'artifact days')} 169 {retention('log_keep_days', 'log days')} 170 </section> 171 ) : null, 172 173 mine ? ( 174 <section class="panel"> 175 <h2>trigger secret</h2> 176 {vnode.state.secret 177 ? ( 178 <div class="reveal"> 179 <p class="muted">Copy it now. It cannot be shown again.</p> 180 <code class="mono">{vnode.state.secret}</code> 181 <button onclick={() => { vnode.state.secret = null; }}>done</button> 182 </div> 183 ) 184 : <p class="muted">{project.has_trigger_secret ? 'A secret is set.' : 'No secret set.'}</p>} 185 <button onclick={rotate}>generate a new secret</button> 186 </section> 187 ) : null, 188 189 vnode.state.variables === null ? null : ( 190 <section class="panel"> 191 <h2>variables</h2> 192 {vnode.state.variables.length === 0 ? <Empty what="variables" /> : ( 193 <table> 194 <thead> 195 <tr> 196 <th>name</th> 197 <th>in logs</th> 198 <th>value</th> 199 <th /> 200 </tr> 201 </thead> 202 <tbody> 203 {vnode.state.variables.map((v: any) => ( 204 <tr key={v.name}> 205 <td class="mono">{v.name}</td> 206 <td>{v.masked ? 'masked' : 'visible'}</td> 207 <td class="mono"> 208 {vnode.state.shown[v.name] ? vnode.state.values[v.name] : '***'} 209 </td> 210 <td> 211 <div class="rowactions"> 212 <button class="quiet" onclick={() => toggleValue(v.name)}> 213 {vnode.state.shown[v.name] ? 'hide' : 'view'} 214 </button> 215 <button class="quiet" onclick={() => copyValue(v.name)}>copy</button> 216 <button 217 class="quiet" 218 onclick={async () => { 219 await del(`/projects/${id}/variables/${encodeURIComponent(v.name)}`); 220 await vnode.state.load(); 221 }} 222 > 223 remove 224 </button> 225 </div> 226 </td> 227 </tr> 228 ))} 229 </tbody> 230 </table> 231 )} 232 <form onsubmit={setVariable}> 233 <label> 234 <span>name</span> 235 <input 236 required 237 value={vnode.state.newVar.name} 238 oninput={(e: any) => { vnode.state.newVar.name = e.target.value; }} 239 /> 240 </label> 241 <label> 242 <span>value</span> 243 <input 244 required 245 value={vnode.state.newVar.value} 246 oninput={(e: any) => { vnode.state.newVar.value = e.target.value; }} 247 /> 248 </label> 249 <label class="check"> 250 <input 251 type="checkbox" 252 checked={vnode.state.newVar.masked} 253 onchange={(e: any) => { vnode.state.newVar.masked = e.target.checked; }} 254 /> 255 <span>mask in logs</span> 256 </label> 257 <button type="submit">set</button> 258 </form> 259 </section> 260 ), 261 262 <h2>recent jobs</h2>, 263 vnode.state.jobs.length === 0 ? <Empty what="jobs" /> : ( 264 <table> 265 <tbody> 266 {vnode.state.jobs.map((job: any) => ( 267 <tr key={job.id}> 268 <td><m.route.Link href={`/jobs/${job.id}`}>{`#${job.number}`}</m.route.Link></td> 269 <td class="mono">{shortSha(job.head_sha)}</td> 270 <td><Badge state={job.state} /></td> 271 <td>{duration(job.started_at, job.finished_at)}</td> 272 <td><When at={job.created_at} /></td> 273 </tr> 274 ))} 275 </tbody> 276 </table> 277 ), 278 ]; 279 }, 280 };