conductor

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

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