[ARVADOS] updated: d9d02fc279ceca167a3a21e73a62774059409f30

Git user git at public.curoverse.com
Sat Oct 1 09:16:29 EDT 2016


Summary of changes:
 apps/wb2/app.js     | 250 ++++++++++++++++++++++++++++++++++++++++++----------
 apps/wb2/arvados.js |  44 ++++++---
 apps/wb2/index.html |   8 +-
 apps/wb2/local.js   |  49 ++++++++++
 4 files changed, 288 insertions(+), 63 deletions(-)
 create mode 100644 apps/wb2/local.js

       via  d9d02fc279ceca167a3a21e73a62774059409f30 (commit)
       via  06464fe7eb7fe84a461edd42a4481cca8feffd89 (commit)
       via  3b0869d8c54d5d84efc800c0cc3c4cda90cae0b7 (commit)
       via  527d04c63a42cbec848f05a43b0471fe6a4d4d48 (commit)
       via  7499e623ef08d62b169a42073f3ebcf16e058d23 (commit)
       via  58fff083de7256ddb67c292c07cf4f74d69d6f04 (commit)
       via  4779eede6b892831a7a49eb38cb17977dfa34c45 (commit)
      from  830f90c04ab99c2749013bc83a5c679f952ab677 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit d9d02fc279ceca167a3a21e73a62774059409f30
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Oct 1 02:18:49 2016 -0400

    9668: site->type nav

diff --git a/apps/wb2/app.js b/apps/wb2/app.js
index e3b49b1..1b3e062 100644
--- a/apps/wb2/app.js
+++ b/apps/wb2/app.js
@@ -4,6 +4,22 @@ var local = require('./local');
 
 var savedTokens = new local.Dict('tokens');
 var _sessions = {};
+var resources = [
+    'api_clients',
+    'authorized_keys',
+    'collections',
+    'container_requests',
+    'containers',
+    'groups',
+    'humans',
+    'jobs',
+    'job_tasks',
+    'nodes',
+    'repositories',
+    'specimens',
+    'users',
+    'virtual_machines',
+];
 
 // getSession returns a new or existing session for the API endpoint
 // specified by siteID.
@@ -205,17 +221,30 @@ var bsDropdown = {
 var TopNav = {
     view: function(vnode) {
         return m('nav.navbar.navbar-light[style=background-color:#e3f2fd]',
-                 m('.pull-xs-right',
-                   m(bsDropdown, {
-                       label: 'Log in...',
-                       align: 'right',
-                       items: Object.keys(savedTokens.Load()).map(function(siteID) {
-                           return m('a', {
-                               key: siteID,
-                               href: getSession(siteID).client.LoginURL(location.href.replace(/([^\/]*\/+[^\/]+[#!?\/]*)/, '$1loginCallback/'+siteID+'/XYZZY/')),
-                           }, siteID);
-                       }),
-                   })));
+                 m('ul.nav.navbar-nav',
+                   Object.keys(savedTokens.Load()).map(function(siteID) {
+                       return m('li.nav-item', m(bsDropdown, {
+                           label: siteID,
+                           items: resources.map(function(resource) {
+                               return m('a', {
+                                   oncreate: m.route.link,
+                                   href: '/site/'+siteID+'/'+resource,
+                                   key: resource,
+                               }, resource.replace(/_/g, ' '));
+                           }),
+                       }));
+                   }),
+                   m('li.nav-item.pull-xs-right',
+                     m(bsDropdown, {
+                         label: 'Log in...',
+                         align: 'right',
+                         items: Object.keys(savedTokens.Load()).map(function(siteID) {
+                             return m('a', {
+                                 key: siteID,
+                                 href: getSession(siteID).client.LoginURL(location.href.replace(/([^\/]*\/+[^\/]+[#!?\/]*)/, '$1loginCallback/'+siteID+'/XYZZY/')),
+                             }, siteID);
+                         }),
+                     }))));
     },
 };
 
@@ -276,22 +305,7 @@ function RouteResolver(layout, component, attrs) {
         '/site/:siteID/discovery': RR(Layout, DiscoveryDoc),
         '/loginCallback/:siteID/:token/:next...': TryLogin,
     };
-    [
-        'api_clients',
-        'authorized_keys',
-        'collections',
-        'container_requests',
-        'containers',
-        'groups',
-        'humans',
-        'jobs',
-        'job_tasks',
-        'nodes',
-        'repositories',
-        'specimens',
-        'users',
-        'virtual_machines',
-    ].map(function(table) {
+    resources.map(function(table) {
         routes['/site/:siteID/'+table+'/:uuid'] = RR(Layout, Show);
         routes['/site/:siteID/'+table] = RR(Layout, GenericResourceList(table), {filters: []});
     });

commit 06464fe7eb7fe84a461edd42a4481cca8feffd89
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Oct 1 01:55:43 2016 -0400

    9668: check schema for columns

diff --git a/apps/wb2/app.js b/apps/wb2/app.js
index 64648f7..e3b49b1 100644
--- a/apps/wb2/app.js
+++ b/apps/wb2/app.js
@@ -116,6 +116,20 @@ var Show = {
     },
 };
 
+var GenericCell = {
+    view: function(vnode) {
+        if (vnode.attrs.field == 'modified_by_user_uuid') {
+            var u = (vnode.attrs.session.client.Get('users/'+vnode.attrs.value)() || {})
+            return [u.full_name];
+        } else if (vnode.attrs.field == 'output') {
+            var c = (vnode.attrs.session.client.Get('collections/'+vnode.attrs.item[vnode.attrs.field])() || {})
+            return [c.name || c.portable_data_hash];
+        } else {
+            return [vnode.attrs.value];
+        }
+    },
+};
+
 // m(GenericResourceList, attrs, children...)
 //
 // resource: arvados resource path, e.g., 'collections'
@@ -129,17 +143,39 @@ var GenericResourceList = function(resource) { return {
                 filters: (vnode.attrs.filters instanceof Array) ? vnode.attrs.filters : vnode.attrs.filters(),
             }).
             catch(function() { return {items: []} });
+        vnode.state.coldefs = [];
+        vnode.state.session.dd.run(vnode.state.updateColdefs.bind(this, vnode));
+    },
+    updateColdefs: function(vnode, dd) {
+        var schema = dd.resources[vnode.state.resource].methods.get.response['$ref'];
+        var props = dd.schemas[schema].properties;
+        vnode.state.coldefs =
+            ['uuid', 'full_name', 'name', 'script', 'output', 'modified_by_user_uuid'].
+            reduce(function(cols, field) {
+                if (field in props)
+                    cols.push(field);
+                return cols;
+            }, []);
     },
     view: function(vnode) {
-        return !vnode.state.req() ? m(Loading) : m('table.table.table-hover.table-sm',
+        if (!vnode.state.req()) return m(Loading);
+        return m('table.table.table-hover.table-sm',
+                 m('thead',
+                   m('tr',
+                     vnode.state.coldefs.map(function(field) {
+                         return m('th', field);
+                     }))),
                  m('tbody',
                    vnode.state.req().items.map(function(item) {
-                       var user = vnode.state.session.client.Get('users/'+item.modified_by_user_uuid)() || {};
-                       return m('tr',
-                                m('td', item.full_name || item.name || item.hostname || item.script),
-                                m('td', item.email || item.script_version),
-                                m('td', user.full_name),
-                                m('td', item.uuid));
+                       return m('tr', {key: item.uuid},
+                                vnode.state.coldefs.map(function(field) {
+                                    return m('td', m(GenericCell, {
+                                        session: vnode.state.session,
+                                        item: item,
+                                        field: field,
+                                        value: item[field],
+                                    }));
+                                }));
                    })));
     },
 }};

commit 3b0869d8c54d5d84efc800c0cc3c4cda90cae0b7
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Oct 1 01:13:52 2016 -0400

    9668: generic lists

diff --git a/apps/wb2/app.js b/apps/wb2/app.js
index 0aeb740..64648f7 100644
--- a/apps/wb2/app.js
+++ b/apps/wb2/app.js
@@ -10,14 +10,11 @@ var _sessions = {};
 function getSession(siteID) {
     var session = _sessions[siteID];
     if (!session) {
-        var client = new arvados.Client(siteID);
         var token = savedTokens.Get(siteID) || savedTokens.Put(siteID, '');
+        var client = new arvados.Client(siteID, token, requestFunc);
         session = _sessions[siteID] = {
             client: client,
-            dd: m.request({
-                method: 'GET',
-                url: client.DiscoveryURL(),
-            }),
+            dd: client.DiscoveryDoc(),
             websocket: m.prop(),
             token: token,
         };
@@ -25,6 +22,21 @@ function getSession(siteID) {
     return session;
 }
 
+function requestFunc(options) {
+    if ('headers' in options) {
+        var headers = options.headers;
+        options.config = function(xhr) {
+            headers.map(function(hdr) {
+                xhr.setRequestHeader(hdr[0], hdr[1]);
+            });
+            return xhr;
+        }
+        delete options.headers;
+    }
+    return m.request(options);
+}
+
+// remove me
 function ArvadosRequest(session, method, url) {
     return session.dd.run(function() {
         return m.request({
@@ -104,6 +116,34 @@ var Show = {
     },
 };
 
+// m(GenericResourceList, attrs, children...)
+//
+// resource: arvados resource path, e.g., 'collections'
+// attrs.filters: an array of arvados filters, or a stream that returns one
+var GenericResourceList = function(resource) { return {
+    oninit: function(vnode) {
+        vnode.state.resource = resource;
+        vnode.state.session = getSession(vnode.attrs.siteID);
+        vnode.state.req =
+            vnode.state.session.client.Get(vnode.state.resource, {
+                filters: (vnode.attrs.filters instanceof Array) ? vnode.attrs.filters : vnode.attrs.filters(),
+            }).
+            catch(function() { return {items: []} });
+    },
+    view: function(vnode) {
+        return !vnode.state.req() ? m(Loading) : m('table.table.table-hover.table-sm',
+                 m('tbody',
+                   vnode.state.req().items.map(function(item) {
+                       var user = vnode.state.session.client.Get('users/'+item.modified_by_user_uuid)() || {};
+                       return m('tr',
+                                m('td', item.full_name || item.name || item.hostname || item.script),
+                                m('td', item.email || item.script_version),
+                                m('td', user.full_name),
+                                m('td', item.uuid));
+                   })));
+    },
+}};
+
 var bsDropdown = {
     oninit: function(vnode) {
         vnode.state.toggle = function(e) {
@@ -163,7 +203,7 @@ var Layout = {
     view: function(vnode) {
         return [
             m(TopNav),
-            m('.container-fluid', vnode.children),
+            m('.container-fluid', vnode.attrs, vnode.children),
         ];
     },
 };
@@ -182,13 +222,13 @@ var TryLogin = {
     },
 };
 
-function RouteResolver(layout, component, withKey) {
+function RouteResolver(layout, component, attrs) {
     return {
         render: function(vnode) {
             return m(layout, m(component,
                                Object.assign({
-                                   key: withKey + ':' + vnode.attrs[withKey],
-                               }, vnode.attrs)));
+                                   key: m.route.get(),
+                               }, attrs || {}, vnode.attrs)));
         },
     };
 }
@@ -197,12 +237,30 @@ function RouteResolver(layout, component, withKey) {
     var RR = RouteResolver;
     var routes = {
         '/': TryLogin,
-        '/site/:siteID/discovery': RR(Layout, DiscoveryDoc, 'siteID'),
+        '/site/:siteID/discovery': RR(Layout, DiscoveryDoc),
         '/loginCallback/:siteID/:token/:next...': TryLogin,
     };
-    ['collections', 'containers'].map(function(table) {
-        routes['/site/:siteID/'+table+'/:uuid'] = RR(Layout, Show, 'uuid');
+    [
+        'api_clients',
+        'authorized_keys',
+        'collections',
+        'container_requests',
+        'containers',
+        'groups',
+        'humans',
+        'jobs',
+        'job_tasks',
+        'nodes',
+        'repositories',
+        'specimens',
+        'users',
+        'virtual_machines',
+    ].map(function(table) {
+        routes['/site/:siteID/'+table+'/:uuid'] = RR(Layout, Show);
+        routes['/site/:siteID/'+table] = RR(Layout, GenericResourceList(table), {filters: []});
     });
     m.route(document.body, '/', routes);
     m.mount(document.head, Head);
 })();
+
+window.m = m;
diff --git a/apps/wb2/arvados.js b/apps/wb2/arvados.js
index acfe14c..4854a0b 100644
--- a/apps/wb2/arvados.js
+++ b/apps/wb2/arvados.js
@@ -1,13 +1,35 @@
 module.exports.Client = Client;
-function Client(UUIDPrefix) {
-    this.UUIDPrefix = UUIDPrefix;
-}
-
-Client.prototype.DiscoveryURL = function() {
-    return 'https://' + this.UUIDPrefix + '.arvadosapi.com/discovery/v1/apis/arvados/v1/rest';
-}
-
-Client.prototype.LoginURL = function(callbackURL) {
-    return 'https://' + this.UUIDPrefix + '.arvadosapi.com/login?return_to=' +
-        encodeURIComponent(callbackURL);
+function Client(UUIDPrefix, token, requestFunc) {
+    var cached = {};
+    var dd;
+    var client = {
+        DiscoveryDoc: function() {
+            if (dd === undefined)
+                dd = requestFunc({
+                    method: 'GET',
+                    url: client.DiscoveryURL(),
+                });
+            return dd;
+        },
+        DiscoveryURL: function() {
+            return 'https://' + UUIDPrefix + '.arvadosapi.com/discovery/v1/apis/arvados/v1/rest';
+        },
+        LoginURL: function(callbackURL) {
+            return 'https://' + UUIDPrefix + '.arvadosapi.com/login?return_to=' +
+                encodeURIComponent(callbackURL);
+        },
+        Get: function(path, params) {
+            if (path in cached)
+                return cached[path];
+            var req = requestFunc({
+                method: 'GET',
+                url: 'https://' + UUIDPrefix + '.arvadosapi.com/arvados/v1/' + path,
+                headers: [['Authorization', 'OAuth2 '+token]],
+            });
+            if (arguments.length == 1)
+                cached[path] = req;
+            return req;
+        },
+    };
+    return client;
 }

commit 527d04c63a42cbec848f05a43b0471fe6a4d4d48
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Sep 28 15:58:50 2016 -0400

    9668: refactor dropdown

diff --git a/apps/wb2/app.js b/apps/wb2/app.js
index 719c9eb..0aeb740 100644
--- a/apps/wb2/app.js
+++ b/apps/wb2/app.js
@@ -105,21 +105,23 @@ var Show = {
 };
 
 var bsDropdown = {
-    toggleOpen: function() {
-        this.openClass = this.openClass ? '' : 'open';
-        return false;
-    },
     oninit: function(vnode) {
-        this.openClass = '';
+        vnode.state.toggle = function(e) {
+            vnode.state.open = !vnode.state.open;
+            return false;
+        };
     },
     view: function(vnode) {
-        return m('.dropdown', {class: vnode.state.openClass}, [
+        return m('.dropdown', {className: vnode.state.open ? 'open' : ''}, [
             m('a.btn.btn-secondary.dropdown-toggle', {
-                onclick: vnode.state.toggleOpen.bind(vnode.state),
+                onclick: vnode.state.toggle,
             }, vnode.attrs.label),
             m('.dropdown-menu',
-              vnode.attrs.menuAttrs || {},
-              vnode.children),
+              {className: vnode.attrs.align == 'right' ? 'dropdown-menu-right' : ''},
+              vnode.attrs.items.map(function(item) {
+                  item.attrs.className = 'dropdown-item '+item.attrs.className;
+                  return item;
+              })),
         ]);
     },
 };
@@ -130,13 +132,14 @@ var TopNav = {
                  m('.pull-xs-right',
                    m(bsDropdown, {
                        label: 'Log in...',
-                       menuAttrs: {class: 'dropdown-menu-right'},
-                   }, Object.keys(savedTokens.Load()).map(function(siteID) {
-                       return m('a.dropdown-item', {
-                           key: siteID,
-                           href: getSession(siteID).client.LoginURL(location.href.replace(/([^\/]*\/+[^\/]+[#!?\/]*)/, '$1loginCallback/'+siteID+'/XYZZY/')),
-                       }, [siteID]);
-                   }))));
+                       align: 'right',
+                       items: Object.keys(savedTokens.Load()).map(function(siteID) {
+                           return m('a', {
+                               key: siteID,
+                               href: getSession(siteID).client.LoginURL(location.href.replace(/([^\/]*\/+[^\/]+[#!?\/]*)/, '$1loginCallback/'+siteID+'/XYZZY/')),
+                           }, siteID);
+                       }),
+                   })));
     },
 };
 

commit 7499e623ef08d62b169a42073f3ebcf16e058d23
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Sep 27 22:59:08 2016 -0400

    9668: tweak handler

diff --git a/apps/wb2/app.js b/apps/wb2/app.js
index 456a92a..719c9eb 100644
--- a/apps/wb2/app.js
+++ b/apps/wb2/app.js
@@ -114,8 +114,8 @@ var bsDropdown = {
     },
     view: function(vnode) {
         return m('.dropdown', {class: vnode.state.openClass}, [
-            m('a.btn.btn-secondary.dropdown-toggle[href=#]', {
-                onclick: m.withAttr('href', vnode.state.toggleOpen, vnode.state),
+            m('a.btn.btn-secondary.dropdown-toggle', {
+                onclick: vnode.state.toggleOpen.bind(vnode.state),
             }, vnode.attrs.label),
             m('.dropdown-menu',
               vnode.attrs.menuAttrs || {},

commit 58fff083de7256ddb67c292c07cf4f74d69d6f04
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Sep 27 22:03:15 2016 -0400

    9668: local storage

diff --git a/apps/wb2/app.js b/apps/wb2/app.js
index 691b735..456a92a 100644
--- a/apps/wb2/app.js
+++ b/apps/wb2/app.js
@@ -1,6 +1,8 @@
 var m = require('mithril');
 var arvados = require('./arvados');
+var local = require('./local');
 
+var savedTokens = new local.Dict('tokens');
 var _sessions = {};
 
 // getSession returns a new or existing session for the API endpoint
@@ -9,6 +11,7 @@ function getSession(siteID) {
     var session = _sessions[siteID];
     if (!session) {
         var client = new arvados.Client(siteID);
+        var token = savedTokens.Get(siteID) || savedTokens.Put(siteID, '');
         session = _sessions[siteID] = {
             client: client,
             dd: m.request({
@@ -16,7 +19,7 @@ function getSession(siteID) {
                 url: client.DiscoveryURL(),
             }),
             websocket: m.prop(),
-            token: loadToken(siteID),
+            token: token,
         };
     }
     return session;
@@ -35,24 +38,6 @@ function ArvadosRequest(session, method, url) {
     });
 }
 
-function saveToken(siteID, token) {
-    var tokens = {};
-    try {
-        tokens = JSON.parse(window.localStorage.tokens);
-    } catch(e) {}
-    tokens[siteID] = token;
-    window.localStorage.tokens = JSON.stringify(tokens);
-    getSession(siteID).token = token;
-}
-
-function loadToken(siteID) {
-    try {
-        return JSON.parse(window.localStorage.tokens)[siteID];
-    } catch(e) {
-        return undefined;
-    }
-}
-
 // getDiscoveryDoc returns a stream resolving to the discovery
 // document for the Arvados API endpoint specified by siteID.
 function getDiscoveryDoc(siteID) {
@@ -119,15 +104,39 @@ var Show = {
     },
 };
 
+var bsDropdown = {
+    toggleOpen: function() {
+        this.openClass = this.openClass ? '' : 'open';
+        return false;
+    },
+    oninit: function(vnode) {
+        this.openClass = '';
+    },
+    view: function(vnode) {
+        return m('.dropdown', {class: vnode.state.openClass}, [
+            m('a.btn.btn-secondary.dropdown-toggle[href=#]', {
+                onclick: m.withAttr('href', vnode.state.toggleOpen, vnode.state),
+            }, vnode.attrs.label),
+            m('.dropdown-menu',
+              vnode.attrs.menuAttrs || {},
+              vnode.children),
+        ]);
+    },
+};
+
 var TopNav = {
     view: function(vnode) {
         return m('nav.navbar.navbar-light[style=background-color:#e3f2fd]',
                  m('.pull-xs-right',
-                   Object.keys(_sessions).map(function(siteID) {
-                       return [m('a.btn.btn-secondary.btn-sm', {
+                   m(bsDropdown, {
+                       label: 'Log in...',
+                       menuAttrs: {class: 'dropdown-menu-right'},
+                   }, Object.keys(savedTokens.Load()).map(function(siteID) {
+                       return m('a.dropdown-item', {
+                           key: siteID,
                            href: getSession(siteID).client.LoginURL(location.href.replace(/([^\/]*\/+[^\/]+[#!?\/]*)/, '$1loginCallback/'+siteID+'/XYZZY/')),
-                       }, 'Login:', siteID)];
-                   })));
+                       }, [siteID]);
+                   }))));
     },
 };
 
@@ -162,7 +171,7 @@ var TryLogin = {
         if (token = location.href.match(/(\?api_token=([^\?&]+))/)) {
             location = location.href.replace(token[1], '').replace('XYZZY', token[2]);
         } else if (token = vnode.attrs.token) {
-            saveToken(vnode.attrs.siteID, token);
+            savedTokens.Put(vnode.attrs.siteID, token);
             m.route.set('/'+vnode.attrs.next);
         } else {
             m.route.set('/site/4xphq/discovery');
diff --git a/apps/wb2/local.js b/apps/wb2/local.js
new file mode 100644
index 0000000..48ccbe6
--- /dev/null
+++ b/apps/wb2/local.js
@@ -0,0 +1,49 @@
+// Usage:
+//
+// d = new Dict('examples');
+// d.Put('one', [1,2,3])
+// arr = d.Get('one');
+module.exports.Dict = Dict;
+
+function Dict(name) {
+    this.name = name;
+}
+
+Dict.prototype.List = function() {
+    return Object.keys(LoadDict(this.name));
+}
+
+Dict.prototype.Load = function() {
+    return LoadDict(this.name);
+}
+
+Dict.prototype.Get = function(k) {
+    return LoadDict(this.name)[k];
+}
+
+Dict.prototype.Put = function(k, v) {
+    var data = LoadDict(this.name);
+    data[k] = v;
+    SaveDict(this.name, data);
+    return v;
+}
+
+Dict.prototype.Delete = function(k) {
+    var data = LoadDict(this.name);
+    var v = data[k];
+    delete(data[k])
+    SaveDict(this.name, data);
+    return v;
+}
+
+function LoadDict(key) {
+    try {
+        return JSON.parse(window.localStorage[key]);
+    } catch(e) {
+        return {};
+    }
+}
+
+function SaveDict(key, data) {
+    window.localStorage[key] = JSON.stringify(data);
+}

commit 4779eede6b892831a7a49eb38cb17977dfa34c45
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Sep 27 00:29:27 2016 -0400

    9668: add bootstrap4

diff --git a/apps/wb2/app.js b/apps/wb2/app.js
index ca5f0a1..691b735 100644
--- a/apps/wb2/app.js
+++ b/apps/wb2/app.js
@@ -71,6 +71,15 @@ var ErrorTODO = {
     },
 };
 
+var FormRow = {
+    view: function(vnode) {
+        return m('.form-group.row',
+                 m('label.col-sm-3.col-form-label', vnode.attrs.label),
+                 m('.col-sm-9',
+                   m('p.form-control-static', vnode.attrs.value)));
+    },
+};
+
 var DiscoveryDoc = {
     oninit: function(vnode) {
         vnode.state.dd = getDiscoveryDoc(vnode.attrs.siteID);
@@ -84,18 +93,19 @@ var DiscoveryDoc = {
         var dd = vnode.state.dd;
         if (dd.error()) return m(ErrorTODO, dd.error());
         else if (!dd()) return m(Loading);
-        return m('.dd', [
-            m('.row', ['site ID: ', vnode.attrs.siteID]),
-            m('.row', ['version: ', dd().source_version]),
-            m('.row', ['websocketUrl: ', dd().websocketUrl]),
-            m('.row', ['defaultCollectionReplication: ', dd().defaultCollectionReplication]),
-            vnode.state.current_user ? m('.row', [
-                'current user: ',
-                vnode.state.current_user().full_name,
-                ' (', vnode.state.current_user().username,
-                ', ', vnode.state.current_user().email,
-                ')',
-            ]) : [],
+        return m('form', [
+            m(FormRow, {type: 'static', label: 'site ID', value: vnode.attrs.siteID}),
+            m(FormRow, {type: 'static', label: 'version', value: dd().source_version}),
+            m(FormRow, {type: 'static', label: 'websocketUrl', value: dd().websocketUrl}),
+            m(FormRow, {type: 'static', label: 'defaultCollectionReplication', value: dd().defaultCollectionReplication}),
+            vnode.state.current_user() ? m(FormRow, {
+                type: 'static',
+                label: 'current user',
+                value: [vnode.state.current_user().full_name,
+                        ' (', vnode.state.current_user().username,
+                        ', ', vnode.state.current_user().email,
+                        ')'],
+            }) : [],
         ]);
     },
 };
@@ -111,17 +121,38 @@ var Show = {
 
 var TopNav = {
     view: function(vnode) {
-        return Object.keys(_sessions).map(function(siteID) {
-            return [m('a', {
-                href: getSession(siteID).client.LoginURL(location.href.replace(/([^\/]*\/+[^\/]+[#!?\/]*)/, '$1loginCallback/'+siteID+'/XYZZY/')),
-            }, 'Login:', siteID), m.trust(' • ')];
-        });
+        return m('nav.navbar.navbar-light[style=background-color:#e3f2fd]',
+                 m('.pull-xs-right',
+                   Object.keys(_sessions).map(function(siteID) {
+                       return [m('a.btn.btn-secondary.btn-sm', {
+                           href: getSession(siteID).client.LoginURL(location.href.replace(/([^\/]*\/+[^\/]+[#!?\/]*)/, '$1loginCallback/'+siteID+'/XYZZY/')),
+                       }, 'Login:', siteID)];
+                   })));
+    },
+};
+
+var Head = {
+    view: function() {
+        return [
+            m('link[rel=stylesheet][href=https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css][integrity=sha384-2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj][crossorigin=anonymous]'),
+            m('meta[charset=utf-8]'),
+            m('meta[name=viewport][content=width=device-width, initial-scale=1, shrink-to-fit=no]'),
+            m('meta[http-equiv=x-ua-compatible][content=ie=edge]'),
+        ];
     },
 };
 
 var Layout = {
+    oninit: function(vnode) {
+        // TODO: (here, or in a separate page wrapper?) build map of
+        // known/logged-in sites, start getting discovery docs if
+        // needed
+    },
     view: function(vnode) {
-        return m('.layout', m(TopNav), vnode.children);
+        return [
+            m(TopNav),
+            m('.container-fluid', vnode.children),
+        ];
     },
 };
 
@@ -161,4 +192,5 @@ function RouteResolver(layout, component, withKey) {
         routes['/site/:siteID/'+table+'/:uuid'] = RR(Layout, Show, 'uuid');
     });
     m.route(document.body, '/', routes);
+    m.mount(document.head, Head);
 })();
diff --git a/apps/wb2/index.html b/apps/wb2/index.html
index ba0b7a3..7bd73a0 100644
--- a/apps/wb2/index.html
+++ b/apps/wb2/index.html
@@ -1,4 +1,6 @@
 <!doctype html>
-<body>
-  <script src="dist/app.js"></script>
-</body>
+<html lang="en">
+  <head></head>
+  <body></body>
+</html>
+<script src="dist/app.js"></script>

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list