conductor

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

users.tsx (6533B)


      1 // web/src/screen/users.tsx - accounts and live sessions
      2 
      3 import m from 'mithril';
      4 import { page, patch, post, del } from '../util/api';
      5 import { session, Empty, Failed, Loading, Pager, When } from '../component/layout';
      6 
      7 const LIMIT = 25;
      8 
      9 export const Users: m.Component = {
     10   oninit(vnode: any) {
     11     vnode.state.offset = 0;
     12     vnode.state.rows = null;
     13     vnode.state.error = null;
     14     vnode.state.form = { username: '', password: '', role: 'user' };
     15 
     16     vnode.state.load = async () => {
     17       try {
     18         vnode.state.rows = await page<any>('/users', {
     19           query: { offset: vnode.state.offset, limit: LIMIT },
     20         });
     21         vnode.state.error = null;
     22       } catch (e) {
     23         vnode.state.error = e;
     24       }
     25       m.redraw();
     26     };
     27     vnode.state.load();
     28   },
     29 
     30   view(vnode: any) {
     31     const { rows, error } = vnode.state;
     32     if (error && !rows) return <Failed error={error} />;
     33     if (!rows) return <Loading />;
     34 
     35     const admin = session.user?.role === 'admin';
     36 
     37     const create = async (e: Event) => {
     38       e.preventDefault();
     39       try {
     40         await post('/users', vnode.state.form);
     41         vnode.state.form = { username: '', password: '', role: 'user' };
     42         await vnode.state.load();
     43       } catch (err) {
     44         vnode.state.error = err;
     45       }
     46     };
     47 
     48     const change = async (id: string, body: unknown) => {
     49       try {
     50         await patch(`/users/${id}`, body);
     51         await vnode.state.load();
     52       } catch (err) {
     53         vnode.state.error = err;
     54       }
     55     };
     56 
     57     return [
     58       <h1>users</h1>,
     59       error ? <Failed error={error} /> : null,
     60 
     61       admin
     62         ? (
     63           <form class="panel" onsubmit={create}>
     64             <label>
     65               <span>username</span>
     66               <input
     67                 required
     68                 value={vnode.state.form.username}
     69                 oninput={(e: any) => { vnode.state.form.username = e.target.value; }}
     70               />
     71             </label>
     72             <label>
     73               <span>password</span>
     74               <input
     75                 required
     76                 type="password"
     77                 value={vnode.state.form.password}
     78                 oninput={(e: any) => { vnode.state.form.password = e.target.value; }}
     79               />
     80             </label>
     81             <label>
     82               <span>role</span>
     83               <select
     84                 value={vnode.state.form.role}
     85                 onchange={(e: any) => { vnode.state.form.role = e.target.value; }}
     86               >
     87                 <option value="user">user</option>
     88                 <option value="admin">admin</option>
     89               </select>
     90             </label>
     91             <button type="submit">create</button>
     92           </form>
     93         )
     94         : null,
     95 
     96       <table>
     97         <thead><tr><th>username</th><th>role</th><th>source</th><th>holdings</th><th>last seen</th><th /></tr></thead>
     98         <tbody>
     99           {rows.rows.map((u: any) => (
    100             <tr key={u.id} class={u.disabled ? 'disabled' : ''}>
    101               <td>{u.username}</td>
    102               <td>
    103                 {admin && u.id !== session.user?.id
    104                   ? (
    105                     <select value={u.role} onchange={(e: any) => change(u.id, { role: e.target.value })}>
    106                       <option value="user">user</option>
    107                       <option value="admin">admin</option>
    108                     </select>
    109                   )
    110                   : u.role}
    111               </td>
    112               <td>{u.source}</td>
    113               <td class="muted">{`${u.project_count ?? 0} projects, ${u.worker_count ?? 0} workers`}</td>
    114               <td><When at={u.last_login_at} /></td>
    115               <td>
    116                 {admin && u.id !== session.user?.id
    117                   ? (
    118                     <div class="rowactions">
    119                       <button class="quiet" onclick={() => change(u.id, { disabled: !u.disabled })}>
    120                         {u.disabled ? 'enable' : 'disable'}
    121                       </button>
    122                       <button
    123                         class="quiet"
    124                         onclick={async () => { await del(`/users/${u.id}`); await vnode.state.load(); }}
    125                       >
    126                         remove
    127                       </button>
    128                     </div>
    129                   )
    130                   : null}
    131               </td>
    132             </tr>
    133           ))}
    134         </tbody>
    135       </table>,
    136 
    137       <Pager
    138         offset={rows.offset}
    139         total={rows.total}
    140         limit={LIMIT}
    141         onmove={(offset: number) => { vnode.state.offset = offset; vnode.state.load(); }}
    142       />,
    143     ];
    144   },
    145 };
    146 
    147 export const Sessions: m.Component = {
    148   oninit(vnode: any) {
    149     vnode.state.offset = 0;
    150     vnode.state.rows = null;
    151     vnode.state.error = null;
    152 
    153     vnode.state.load = async () => {
    154       try {
    155         vnode.state.rows = await page<any>('/sessions', {
    156           query: { offset: vnode.state.offset, limit: LIMIT },
    157         });
    158         vnode.state.error = null;
    159       } catch (e) {
    160         vnode.state.error = e;
    161       }
    162       m.redraw();
    163     };
    164     vnode.state.load();
    165   },
    166 
    167   view(vnode: any) {
    168     const { rows, error } = vnode.state;
    169     if (error && !rows) return <Failed error={error} />;
    170     if (!rows) return <Loading />;
    171 
    172     return [
    173       <h1>sessions</h1>,
    174       <p class="muted">Revoking a session signs that browser out the next time it asks for a token.</p>,
    175       error ? <Failed error={error} /> : null,
    176 
    177       rows.rows.length === 0 ? <Empty what="sessions" /> : (
    178         <table>
    179           <thead><tr><th>browser</th><th>address</th><th>signed in</th><th>last used</th><th>expires</th><th /></tr></thead>
    180           <tbody>
    181             {rows.rows.map((s: any) => (
    182               <tr key={s.id}>
    183                 <td class="ellipsis">{s.user_agent ?? 'unknown'}</td>
    184                 <td class="mono">{s.ip ?? ''}</td>
    185                 <td><When at={s.issued_at} /></td>
    186                 <td><When at={s.last_used_at} /></td>
    187                 <td><When at={s.expires_at} /></td>
    188                 <td>
    189                   <button
    190                     class="quiet"
    191                     onclick={async () => { await del(`/sessions/${s.id}`); await vnode.state.load(); }}
    192                   >
    193                     revoke
    194                   </button>
    195                 </td>
    196               </tr>
    197             ))}
    198           </tbody>
    199         </table>
    200       ),
    201 
    202       <Pager
    203         offset={rows.offset}
    204         total={rows.total}
    205         limit={LIMIT}
    206         onmove={(offset: number) => { vnode.state.offset = offset; vnode.state.load(); }}
    207       />,
    208     ];
    209   },
    210 };