[ARVADOS] created: 7252307e0e529e31dea36c5f94efd58c55fee7fd
git at public.curoverse.com
git at public.curoverse.com
Tue Nov 3 19:26:59 EST 2015
at 7252307e0e529e31dea36c5f94efd58c55fee7fd (commit)
commit 7252307e0e529e31dea36c5f94efd58c55fee7fd
Author: Tom Clegg <tom at curoverse.com>
Date: Tue Nov 3 19:26:48 2015 -0500
4831: Add a885m c97qk wx7k5
diff --git a/apps/backstage/app/backstage-routes.js b/apps/backstage/app/backstage-routes.js
index 1d4961f..11ecfb4 100644
--- a/apps/backstage/app/backstage-routes.js
+++ b/apps/backstage/app/backstage-routes.js
@@ -12,7 +12,7 @@ var m = require('mithril')
window.jQuery = require('jquery');
require('bootstrap');
-var connections = m.prop('4xphq qr1hi 9tee4 su92l tb05z'.split(' ').map(
+var connections = m.prop('4xphq a885m c97qk qr1hi 9tee4 su92l tb05z wx7k5'.split(' ').map(
function(site) {
return ArvadosConnection.make(site);
}));
commit ef3c5eadfcccf03cf498e1c6e58c0445f281905e
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 22:02:24 2015 -0500
4831: Rearrange classes more.
diff --git a/apps/backstage/app/backstage-layout.js b/apps/backstage/app/backstage-layout.js
index 746daf8..818daa8 100644
--- a/apps/backstage/app/backstage-layout.js
+++ b/apps/backstage/app/backstage-layout.js
@@ -4,15 +4,15 @@ var m = require('mithril');
var _ = require('lodash');
var Layout = require('./base-layout');
-function BackstageLayout(innerModules) {
- return _.extend(this, {
- controller: BackstageLayout.controller.bind(this, innerModules),
- view: BackstageLayout.view,
+function BackstageLayout(opts) {
+ _.extend(this, {
+ controller: this.controller.bind(this, opts),
});
}
-BackstageLayout.prototype = new Layout();
-BackstageLayout.controller = Layout.controller;
-BackstageLayout.view = function view(ctrl) {
+_.extend(BackstageLayout.prototype, Layout.prototype, {
+ view: view
+});
+function view(ctrl) {
return [
m('.navbar.navbar-default', {role: 'navigation'}, [
m('.container-fluid', [
diff --git a/apps/backstage/app/backstage-routes.js b/apps/backstage/app/backstage-routes.js
index 4f66765..1d4961f 100644
--- a/apps/backstage/app/backstage-routes.js
+++ b/apps/backstage/app/backstage-routes.js
@@ -20,12 +20,18 @@ var connections = m.prop('4xphq qr1hi 9tee4 su92l tb05z'.split(' ').map(
m.route(document.body, '/', {
'/login-callback': new BackstageLoginComponent(),
'/': new BackstageLayout({
- content: new ArvApiDirectoryComponent(connections)
+ modules: {
+ content: new ArvApiDirectoryComponent({connections: connections}),
+ },
}),
'/list/:connection/:modelName': new BackstageLayout({
- content: ArvIndexComponent
+ modules: {
+ content: ArvIndexComponent
+ },
}),
'/show/:uuid': new BackstageLayout({
- content: ArvShowComponent
+ modules: {
+ content: ArvShowComponent
+ },
}),
});
diff --git a/apps/backstage/app/base-layout.js b/apps/backstage/app/base-layout.js
index cba62c9..acab495 100644
--- a/apps/backstage/app/base-layout.js
+++ b/apps/backstage/app/base-layout.js
@@ -1,37 +1,50 @@
-// Layout class. Instances are suitable for passing to m.route().
+// Layout class. Instances of subclasses are suitable for passing to
+// m.route().
//
// Usage:
-// new Layout(viewFunction, {main: FooModuleClass, nav: NavComponent})
+// new LayoutSubclass({modules: {main: FooModuleClass, nav: NavComponent}})
//
-// viewFunction is expected to include this.views.main() and
-// this.views.nav() somewhere in its return value.
-//
-// Content can be given as a class (in which case instances are made
-// with new FooModuleClass()) or as a component instance (i.e., an
-// object with a function named 'controller').
+// Content can be given as:
+// * a class -- instances are made with `new class()`
+// * an array of [cls, arg] -- instances are made with `new cls.controller(arg)`
+// * a component instance (i.e., an object with a function named 'controller')
//
// The layout is responsible for creating and unloading controllers.
module.exports = Layout;
-var BaseController = require('app/base-ctrl');
+var BaseController = require('./base-ctrl');
var _ = require('lodash');
-function Layout(innerModules) {
+function Layout(opts) {
return _.extend(this, {
- controller: Layout.controller.bind(this, innerModules),
+ controller: this.controller.bind(this, opts),
});
}
-Layout.controller = function controller(innerModules) {
+_.extend(Layout.prototype, {
+ controller: controller,
+});
+controller.prototype = new BaseController();
+function controller(opts) {
+ _.extend(this, {modules: {}}, opts);
this.views = {};
this.controllers = [];
- Object.keys(innerModules).map(function(key) {
- var module = innerModules[key];
- var component = (module.controller instanceof Function) ? module : new module();
- var ctrl = new component.controller();
+ Object.keys(this.modules).map(function(key) {
+ var module = this.modules[key];
+ var component;
+ var ctrl;
+ if (module instanceof Array) {
+ component = module[0];
+ ctrl = new component.controller(module[1]);
+ } else if (module.controller instanceof Function) {
+ component = module;
+ ctrl = new component.controller();
+ } else {
+ component = new module();
+ ctrl = new component.controller();
+ }
var view = component.view.bind(component.view, ctrl);
this.controllers.push(ctrl);
this.views[key] = view;
}, this);
-};
-Layout.controller.prototype = new BaseController();
+}
diff --git a/apps/backstage/app/component.arv-api-directory.js b/apps/backstage/app/component.arv-api-directory.js
index 9d1360c..b79765b 100644
--- a/apps/backstage/app/component.arv-api-directory.js
+++ b/apps/backstage/app/component.arv-api-directory.js
@@ -1,43 +1,50 @@
module.exports = ArvApiDirectoryComponent;
-var m = require('mithril')
-, BaseController = require('app/base-ctrl')
-, ArvApiStatusComponent = require('app/component.arv-api-status');
+var m = require('mithril');
+var _ = require('lodash');
+var BaseController = require('./base-ctrl');
+var ArvApiStatusComponent = require('./component.arv-api-status');
-function ArvApiDirectoryComponent(connections) {
- this.controller = Controller;
- Controller.prototype = new BaseController();
- function Controller(vm) {
- this.vm = vm || {};
- this.vm.widgets = connections().map(function(conn) {
- var component = new ArvApiStatusComponent(conn);
- return {
- view: component.view,
- controller: new component.controller(),
- };
- });
- // Give BaseController a list of components to unload.
- this.controllers = function() {
- return this.vm.widgets.map(function(widget) {
- return widget.controller;
- });
- }.bind(this);
-
- this.redrawTimer = setInterval(function() {
- // If redraw is really really cheap, we can do this to make
- // "#seconds old" timers count in real time.
- m.redraw();
- }, 1000);
- this.onunload = function() {
- clearTimeout(this.redrawTimer);
+function ArvApiDirectoryComponent(opts) {
+ _.extend(this, {
+ controller: this.controller.bind(this, opts),
+ });
+}
+_.extend(ArvApiDirectoryComponent.prototype, {
+ controller: controller,
+ view: view,
+});
+function controller(opts) {
+ _.extend(this, {connections: m.prop([])}, opts);
+ this.redrawTimer = setInterval(function() {
+ // If redraw is really really cheap, we can do this to make
+ // "#seconds old" timers count in real time.
+ m.redraw();
+ }, 1000);
+ this.widgets = this.connections().map(function(conn) {
+ var component = new ArvApiStatusComponent(conn);
+ return {
+ view: component.view,
+ controller: new component.controller(),
};
- };
- this.view = View;
- function View(ctrl) {
- return m('div', [
- ctrl.vm.widgets.map(function(widget) {
- return widget.view(widget.controller);
- })
- ]);
- };
+ });
+}
+_.extend(controller.prototype, BaseController.prototype, {
+ // Give BaseController a list of components to unload.
+ controllers: function controllers() {
+ return this.widgets.map(function(widget) {
+ return widget.controller;
+ });
+ },
+ onunload: function onunload() {
+ clearTimeout(this.redrawTimer);
+ BaseController.prototype.onunload.call(this);
+ },
+});
+function view(ctrl) {
+ return m('div', [
+ ctrl.widgets.map(function(widget) {
+ return widget.view(widget.controller);
+ })
+ ]);
}
diff --git a/apps/backstage/app/component.arv-index.js b/apps/backstage/app/component.arv-index.js
index 8b79f2e..1eded1a 100644
--- a/apps/backstage/app/component.arv-index.js
+++ b/apps/backstage/app/component.arv-index.js
@@ -1,13 +1,18 @@
module.exports = ArvIndexComponent;
-var m = require('mithril')
-, BaseController = require('app/base-ctrl')
-, ArvListComponent = require('app/component.arv-list')
-, ArvObjectRowComponent = require('app/component.arv-object-row')
-, InfiniteScroll = require('app/infinitescroll');
+var m = require('mithril');
+var _ = require('lodash');
+var BaseController = require('./base-ctrl');
+var ArvListComponent = require('./component.arv-list')
+var ArvObjectRowComponent = require('./component.arv-object-row')
+var InfiniteScroll = require('./infinitescroll');
function ArvIndexComponent() {}
-ArvIndexComponent.controller = function controller() {
+_.extend(ArvIndexComponent.prototype, {
+ controller: controller,
+ view: view,
+});
+function controller() {
this.list =
new ArvListComponent(null, null, ArvObjectRowComponent);
this.listCtrl =
@@ -16,10 +21,12 @@ ArvIndexComponent.controller = function controller() {
new InfiniteScroll(this.listCtrl, this.list.view, {pxThreshold: 200});
this.scrollerCtrl =
new this.scroller.controller();
-};
-ArvIndexComponent.controller.prototype.controllers = function controllers() {
- return [this.listCtrl, this.scrollerCtrl];
-};
-ArvIndexComponent.view = function view(ctrl) {
+}
+_.extend(controller.prototype, BaseController.prototype, {
+ controllers: function controllers() {
+ return [this.listCtrl, this.scrollerCtrl];
+ },
+});
+function view(ctrl) {
return ctrl.scroller.view(ctrl.scrollerCtrl);
-};
+}
commit 3b62145615a5b40be68b44ffbfda1e3ddb5a6699
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 18:54:38 2015 -0500
4831: Remove old reference to util namespace.
diff --git a/apps/backstage/app/util.js b/apps/backstage/app/util.js
index 277124f..a856da2 100644
--- a/apps/backstage/app/util.js
+++ b/apps/backstage/app/util.js
@@ -3,8 +3,8 @@ module.exports = {
debounce: debounce,
};
-// util.choose('a', {a: 'A', b: 'B'}) --> return 'A'
-// util.choose('a', {a: [console.log, 'foo']}) --> return console.log('foo')
+// choose('a', {a: 'A', b: 'B'}) --> return 'A'
+// choose('a', {a: [console.log, 'foo']}) --> return console.log('foo')
function choose(key, options) {
var option = options[key];
if (option instanceof Array && option[0] instanceof Function)
@@ -13,13 +13,13 @@ function choose(key, options) {
return option;
}
-// util.debounce(250, key) --> Return a promise. If someone else
+// debounce(250, key) --> Return a promise. If someone else
// calls debounce with the same key, reject the promise. If nobody
// else has done so after 250ms, resolve the promise.
function debounce(ms, key) {
var newpending;
- util.debounce.pending = util.debounce.pending || [];
- util.debounce.pending.map(function(found) {
+ debounce.pending = debounce.pending || [];
+ debounce.pending.map(function(found) {
if (!newpending && found.key === key) {
// Promise already pending with this key. Reject the old
// one, reuse its slot for the new one.
@@ -32,15 +32,15 @@ function debounce(ms, key) {
if (!newpending) {
// No pending promise with this key.
newpending = {key: key}
- util.debounce.pending.push(newpending);
+ debounce.pending.push(newpending);
}
newpending.deferred = m.deferred();
m.startComputation();
newpending.timer = window.setTimeout(function() {
// Success, no more bouncing. Remove from pending list.
- util.debounce.pending.map(function(found, i) {
+ debounce.pending.map(function(found, i) {
if (found === newpending) {
- util.debounce.pending.splice(i, 1);
+ debounce.pending.splice(i, 1);
found.deferred.resolve();
m.endComputation();
}
commit e7c80567f45a6983f599cf01e5bcc8e32d8853a8
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 18:17:08 2015 -0500
4831: Unclassize some modules.
diff --git a/apps/backstage/app/backstage-layout.js b/apps/backstage/app/backstage-layout.js
index 5229669..746daf8 100644
--- a/apps/backstage/app/backstage-layout.js
+++ b/apps/backstage/app/backstage-layout.js
@@ -1,8 +1,18 @@
-module.exports = BackstageLayoutView;
+module.exports = BackstageLayout;
var m = require('mithril');
+var _ = require('lodash');
+var Layout = require('./base-layout');
-function BackstageLayoutView() {
+function BackstageLayout(innerModules) {
+ return _.extend(this, {
+ controller: BackstageLayout.controller.bind(this, innerModules),
+ view: BackstageLayout.view,
+ });
+}
+BackstageLayout.prototype = new Layout();
+BackstageLayout.controller = Layout.controller;
+BackstageLayout.view = function view(ctrl) {
return [
m('.navbar.navbar-default', {role: 'navigation'}, [
m('.container-fluid', [
@@ -26,7 +36,7 @@ function BackstageLayoutView() {
]),
]),
]),
- m('.container-fluid', this.views.content()),
+ m('.container-fluid', ctrl.views.content()),
];
function siteBreadcrumb() {
var txt;
diff --git a/apps/backstage/app/backstage-routes.js b/apps/backstage/app/backstage-routes.js
index defe480..4f66765 100644
--- a/apps/backstage/app/backstage-routes.js
+++ b/apps/backstage/app/backstage-routes.js
@@ -2,12 +2,12 @@ module.exports = true;
var m = require('mithril')
, ArvadosConnection = require('arvados/client')
-, Layout = require('app/base-layout')
-, BackstageLayoutView = require('app/backstage-layout')
-, BackstageLoginComponent = require('app/backstage-login')
-, ArvApiDirectoryComponent = require('app/component.arv-api-directory')
-, ArvIndexComponent = require('app/component.arv-index')
-, ArvShowComponent = require('app/component.arv-show');
+, Layout = require('./base-layout')
+, BackstageLayout = require('./backstage-layout')
+, BackstageLoginComponent = require('./backstage-login')
+, ArvApiDirectoryComponent = require('./component.arv-api-directory')
+, ArvIndexComponent = require('./component.arv-index')
+, ArvShowComponent = require('./component.arv-show');
window.jQuery = require('jquery');
require('bootstrap');
@@ -19,13 +19,13 @@ var connections = m.prop('4xphq qr1hi 9tee4 su92l tb05z'.split(' ').map(
m.route(document.body, '/', {
'/login-callback': new BackstageLoginComponent(),
- '/': new Layout(BackstageLayoutView, {
+ '/': new BackstageLayout({
content: new ArvApiDirectoryComponent(connections)
}),
- '/list/:connection/:modelName': new Layout(BackstageLayoutView, {
+ '/list/:connection/:modelName': new BackstageLayout({
content: ArvIndexComponent
}),
- '/show/:uuid': new Layout(BackstageLayoutView, {
+ '/show/:uuid': new BackstageLayout({
content: ArvShowComponent
}),
});
diff --git a/apps/backstage/app/base-component.js b/apps/backstage/app/base-component.js
deleted file mode 100644
index 9aab6f4..0000000
--- a/apps/backstage/app/base-component.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = BaseComponent;
-
-var BaseController = require('app/base-ctrl');
-
-BaseComponent.prototype.controller = BaseController;
-function BaseComponent() {}
diff --git a/apps/backstage/app/base-ctrl.js b/apps/backstage/app/base-ctrl.js
index 68f9b8f..feb33a2 100644
--- a/apps/backstage/app/base-ctrl.js
+++ b/apps/backstage/app/base-ctrl.js
@@ -2,6 +2,7 @@ module.exports = BaseController;
var m = require('mithril');
+function BaseController() {}
BaseController.prototype.selectUuid =
function selectUuid(uuid) {
m.route('/show/' + uuid);
@@ -19,6 +20,3 @@ BaseController.prototype.onunload =
ctrl.onunload();
});
}
-function BaseController(vm) {
- this.vm = vm;
-}
diff --git a/apps/backstage/app/base-layout.js b/apps/backstage/app/base-layout.js
index 29adbc0..cba62c9 100644
--- a/apps/backstage/app/base-layout.js
+++ b/apps/backstage/app/base-layout.js
@@ -14,24 +14,24 @@
module.exports = Layout;
-var BaseComponent = require('app/base-component')
-, BaseController = require('app/base-ctrl');
+var BaseController = require('app/base-ctrl');
+var _ = require('lodash');
-Layout.prototype = BaseComponent;
-function Layout(layoutView, innerModules) {
- var layout = this;
- this.views = {};
- this.controller = function controller() {
- this.controllers = [];
- Object.keys(innerModules).map(function(key) {
- var module = innerModules[key];
- var component = (module.controller instanceof Function) ? module : new module();
- var ctrl = new component.controller();
- var view = component.view.bind(component.view, ctrl);
- this.controllers.push(ctrl);
- layout.views[key] = view;
- }, this);
- };
- this.controller.prototype = new BaseController();
- this.view = layoutView.bind(this, this.controller);
+function Layout(innerModules) {
+ return _.extend(this, {
+ controller: Layout.controller.bind(this, innerModules),
+ });
}
+Layout.controller = function controller(innerModules) {
+ this.views = {};
+ this.controllers = [];
+ Object.keys(innerModules).map(function(key) {
+ var module = innerModules[key];
+ var component = (module.controller instanceof Function) ? module : new module();
+ var ctrl = new component.controller();
+ var view = component.view.bind(component.view, ctrl);
+ this.controllers.push(ctrl);
+ this.views[key] = view;
+ }, this);
+};
+Layout.controller.prototype = new BaseController();
diff --git a/apps/backstage/app/component.arv-api-directory.js b/apps/backstage/app/component.arv-api-directory.js
index 1370ede..9d1360c 100644
--- a/apps/backstage/app/component.arv-api-directory.js
+++ b/apps/backstage/app/component.arv-api-directory.js
@@ -1,11 +1,9 @@
module.exports = ArvApiDirectoryComponent;
var m = require('mithril')
-, BaseComponent = require('app/base-component')
, BaseController = require('app/base-ctrl')
, ArvApiStatusComponent = require('app/component.arv-api-status');
-ArvApiDirectoryComponent.prototype = new BaseComponent();
function ArvApiDirectoryComponent(connections) {
this.controller = Controller;
Controller.prototype = new BaseController();
@@ -18,7 +16,7 @@ function ArvApiDirectoryComponent(connections) {
controller: new component.controller(),
};
});
- // Give BaseComponent a list of components to unload.
+ // Give BaseController a list of components to unload.
this.controllers = function() {
return this.vm.widgets.map(function(widget) {
return widget.controller;
diff --git a/apps/backstage/app/component.arv-index.js b/apps/backstage/app/component.arv-index.js
index 10a0c93..8b79f2e 100644
--- a/apps/backstage/app/component.arv-index.js
+++ b/apps/backstage/app/component.arv-index.js
@@ -6,28 +6,20 @@ var m = require('mithril')
, ArvObjectRowComponent = require('app/component.arv-object-row')
, InfiniteScroll = require('app/infinitescroll');
-function ArvIndexComponent() {
- this.controller = Controller;
- this.view = function view(ctrl) {
- return ctrl.vm.scroller.view(ctrl.vm.scrollerCtrl);
- };
- function ViewModel() {
- this.list =
- new ArvListComponent(null, null, new ArvObjectRowComponent());
- this.listCtrl =
- new this.list.controller();
- this.scroller =
- new InfiniteScroll(this.listCtrl, this.list.view,
- {pxThreshold: 200});
- this.scrollerCtrl =
- new this.scroller.controller();
- }
- function Controller() {
- this.vm = new ViewModel();
- }
- Controller.prototype = new BaseController();
- Controller.prototype.controllers =
- function controllers() {
- return [this.vm.listCtrl, this.vm.scrollerCtrl];
- }
-}
+function ArvIndexComponent() {}
+ArvIndexComponent.controller = function controller() {
+ this.list =
+ new ArvListComponent(null, null, ArvObjectRowComponent);
+ this.listCtrl =
+ new this.list.controller();
+ this.scroller =
+ new InfiniteScroll(this.listCtrl, this.list.view, {pxThreshold: 200});
+ this.scrollerCtrl =
+ new this.scroller.controller();
+};
+ArvIndexComponent.controller.prototype.controllers = function controllers() {
+ return [this.listCtrl, this.scrollerCtrl];
+};
+ArvIndexComponent.view = function view(ctrl) {
+ return ctrl.scroller.view(ctrl.scrollerCtrl);
+};
diff --git a/apps/backstage/app/component.arv-object-row.js b/apps/backstage/app/component.arv-object-row.js
index 73504d6..df45024 100644
--- a/apps/backstage/app/component.arv-object-row.js
+++ b/apps/backstage/app/component.arv-object-row.js
@@ -7,29 +7,29 @@
// mod.view(ctrl)
module.exports = ArvObjectRowComponent;
-var m = require('mithril')
-, BaseComponent = require('app/base-component');
+var m = require('mithril');
+var BaseController = require('./base-ctrl');
-ArvObjectRowComponent.prototype = new BaseComponent();
-function ArvObjectRowComponent() {
- this.view = function(ctrl) {
- var _item = ctrl.vm.item;
- var _owner = _item.owner_uuid ? _item._conn.find(_item.owner_uuid)() : '';
- return m('.row', [
- m('.col-sm-3', [
- m('.btn.btn-xs',
- {onclick: ctrl.selectUuid.bind(ctrl, _item.uuid)}, [
- m('span.glyphicon.glyphicon-link'),
- ]),
- _item.uuid,
+function ArvObjectRowComponent() {}
+ArvObjectRowComponent.controller = function(props) { this.props = props }
+ArvObjectRowComponent.controller.prototype = new BaseController();
+ArvObjectRowComponent.view = function(ctrl) {
+ var _item = ctrl.props.item;
+ var _owner = _item.owner_uuid ? _item._conn.find(_item.owner_uuid)() : '';
+ return m('.row', [
+ m('.col-sm-3', [
+ m('.btn.btn-xs',
+ {onclick: ctrl.selectUuid.bind(ctrl, _item.uuid)}, [
+ m('span.glyphicon.glyphicon-link'),
+ ]),
+ _item.uuid,
+ ]),
+ m('.col-sm-4', _item.name),
+ m('.col-sm-2', [
+ m('a[href="/show/'+_item.owner_uuid+'"]', {config:m.route}, [
+ _owner && (_owner.full_name || _owner.name)
]),
- m('.col-sm-4', _item.name),
- m('.col-sm-2', [
- m('a[href="/show/'+_item.owner_uuid+'"]', {config:m.route}, [
- _owner && (_owner.full_name || _owner.name)
- ]),
- ]),
- m('.col-sm-2', new Date(_item.created_at).toLocaleString()),
- ]);
- };
-}
+ ]),
+ m('.col-sm-2', new Date(_item.created_at).toLocaleString()),
+ ]);
+};
commit d3be209f62d66580878ea1f49b0ae9ce2d01d4a2
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 16:19:43 2015 -0500
4831: Show logged-in email address on dashboard summary. Remove a level of prop() redirection.
diff --git a/apps/backstage/app/component.arv-api-status.js b/apps/backstage/app/component.arv-api-status.js
index 230700e..22d23b2 100644
--- a/apps/backstage/app/component.arv-api-status.js
+++ b/apps/backstage/app/component.arv-api-status.js
@@ -9,6 +9,7 @@ function ArvApiStatusComponent(connection) {
apistatus.vm = (function() {
var vm = {};
vm.connection = connection;
+ vm.currentUser = m.prop({});
vm.dd = connection.discoveryDoc;
vm.dirty = true;
vm.init = function() {
@@ -16,13 +17,15 @@ function ArvApiStatusComponent(connection) {
vm.refresh();
vm.dirty = false;
};
- vm.keepServices = m.prop();
- vm.nodes = m.prop();
+ vm.keepServices = m.prop([]);
+ vm.nodes = m.prop([]);
vm.refresh = function() {
vm.connection.api(
- 'KeepService', 'list', {}).then(vm.keepServices);
+ 'KeepService', 'list', {}).then(vm.keepServices).then(m.redraw);
vm.connection.api(
- 'Node', 'list', {}).then(vm.nodes);
+ 'Node', 'list', {}).then(vm.nodes).then(m.redraw);
+ vm.connection.api(
+ 'User', 'current', {}).then(vm.currentUser).then(m.redraw);
};
vm.logout = function() {
vm.connection.token(undefined);
@@ -61,8 +64,10 @@ function ArvApiStatusComponent(connection) {
!vm.dd() ? '' : m('.pull-right', [
util.choose(!!vm.connection.token(), {
true: [function() {
- return m('a.btn.btn-xs.btn-default',
- {onclick: vm.logout}, 'Log out');
+ return [vm.currentUser().email,
+ " ",
+ m('a.btn.btn-xs.btn-default',
+ {onclick: vm.logout}, 'Log out')];
}],
false: [function() {
return m('a.btn.btn-xs.btn-primary',
@@ -81,41 +86,41 @@ function ArvApiStatusComponent(connection) {
]);
})),
m('.col-md-4', [
- !vm.keepServices() ? '' : m('ul', [
+ m('ul', [
'' + vm.keepServices().length + ' Keep services',
vm.keepServices().map(function(keepService) {
return m('li', [
m('span.label.label-default',
- keepService().service_type),
+ keepService.service_type),
' ',
m('a',
- {href: '/show/'+keepService().uuid,
+ {href: '/show/'+keepService.uuid,
config: m.route}, [
- keepService().service_host,
+ keepService.service_host,
':',
- keepService().service_port,
+ keepService.service_port,
]),
]);
}),
]),
]),
m('.col-md-4', [
- !vm.nodes() ? '' : m('ul', [
+ m('ul', [
'' + vm.nodes().length + ' worker nodes',
vm.nodes().filter(function(node) {
- return node().crunch_worker_state != 'down';
+ return node.crunch_worker_state != 'down';
}).map(function(node) {
return m('li', [
m('span.label.label-default', [
- node().crunch_worker_state,
+ node.crunch_worker_state,
]),
' ',
- m('a', {href: '/show/'+node().uuid,
+ m('a', {href: '/show/'+node.uuid,
config: m.route},
- node().hostname),
+ node.hostname),
' ',
m('span.label.label-info', {title: 'time since last ping'}, [
- ((new Date() - Date.parse(node().last_ping_at))/1000).toFixed(),
+ ((new Date() - Date.parse(node.last_ping_at))/1000).toFixed(),
's'
]),
]);
diff --git a/apps/backstage/app/component.arv-object-row.js b/apps/backstage/app/component.arv-object-row.js
index 9222408..73504d6 100644
--- a/apps/backstage/app/component.arv-object-row.js
+++ b/apps/backstage/app/component.arv-object-row.js
@@ -13,7 +13,7 @@ var m = require('mithril')
ArvObjectRowComponent.prototype = new BaseComponent();
function ArvObjectRowComponent() {
this.view = function(ctrl) {
- var _item = ctrl.vm.item();
+ var _item = ctrl.vm.item;
var _owner = _item.owner_uuid ? _item._conn.find(_item.owner_uuid)() : '';
return m('.row', [
m('.col-sm-3', [
diff --git a/apps/backstage/app/component.dmgraph.js b/apps/backstage/app/component.dmgraph.js
index dcf9894..67ca595 100644
--- a/apps/backstage/app/component.dmgraph.js
+++ b/apps/backstage/app/component.dmgraph.js
@@ -14,12 +14,12 @@ DataManagerGraph.controller = function(opts) {
var seriesLabel = {};
var data = _.compact(this.logs().map(function(item){
try {
- var p = item().properties;
+ var p = item.properties;
var runId = '' + p.run_info.pid + '/' + p.run_info.start_time;
var pt = {
seriesLabel: '' + p.run_info.pid,
collectionsRead: p.collection_info.collections_read,
- logCreatedAt: toDate(item().created_at),
+ logCreatedAt: toDate(item.created_at),
runStartTime: toDate(p.run_info.start_time),
};
pt[runId] = pt.collectionsRead;
diff --git a/apps/backstage/arvados/client.js b/apps/backstage/arvados/client.js
index 935cd3e..6a3a977 100644
--- a/apps/backstage/arvados/client.js
+++ b/apps/backstage/arvados/client.js
@@ -181,7 +181,7 @@ function ArvadosConnection(apiPrefix) {
store[response.uuid](response);
store[response.uuid]()._cacheTime = new Date();
store[response.uuid]()._conn = connection;
- return store[response.uuid];
+ return store[response.uuid]();
} else {
return response;
}
commit a3a78746fc25d0e77b825c448d61662223197f38
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 12:20:19 2015 -0500
4831: Add more error checking to mithril deferred.onerror.
diff --git a/apps/backstage/app/util.js b/apps/backstage/app/util.js
index f8b4d8e..277124f 100644
--- a/apps/backstage/app/util.js
+++ b/apps/backstage/app/util.js
@@ -48,3 +48,13 @@ function debounce(ms, key) {
}, ms);
return newpending.deferred.promise;
}
+
+// Override mithril's default deferred.onerror, with more error checking
+var m = require('mithril');
+m.deferred.onerror = function(e) {
+ if ({}.toString.call(e) === "[object Error]" &&
+ !(e.constructor &&
+ e.constructor.toString() &&
+ e.constructor.toString().match(/ Error/)))
+ throw e;
+};
commit c4a18f7a6084e559fdb0182da2d52ee04a5dacef
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 04:39:57 2015 -0500
4831: Add morris graph of data manager log entries.
diff --git a/apps/backstage/Makefile b/apps/backstage/Makefile
index 035cffd..9273ce8 100644
--- a/apps/backstage/Makefile
+++ b/apps/backstage/Makefile
@@ -6,7 +6,7 @@ clean:
rm -rvf dist
npmdeps:
- ulimit -n 4000; for x in bootstrap bower browserify chai chai-jquery jquery jsdom mithril mithril-query mocha mocha-phantomjs sinon; do [ -d "$(NPMBIN)/../$$x" ] || npm install $$x --save-dev; done
+ ulimit -n 4000; for x in bootstrap bower browserify chai chai-jquery jquery jsdom lodash mithril mithril-query mocha mocha-phantomjs sinon; do [ -d "$(NPMBIN)/../$$x" ] || npm install $$x --save-dev; done
# This uses --debug to enable source maps.
build: build-app build-test build-assets
diff --git a/apps/backstage/app/component.arv-api-status.js b/apps/backstage/app/component.arv-api-status.js
index 0dff644..230700e 100644
--- a/apps/backstage/app/component.arv-api-status.js
+++ b/apps/backstage/app/component.arv-api-status.js
@@ -1,7 +1,8 @@
module.exports = ArvApiStatusComponent;
var m = require('mithril')
-, util = require('app/util');
+, util = require('app/util')
+, DataManagerGraph = require('./component.dmgraph');
function ArvApiStatusComponent(connection) {
var apistatus = {};
@@ -43,6 +44,9 @@ function ArvApiStatusComponent(connection) {
vm.dd().websocketUrl)}, ['none'])
}
};
+ vm.dmGraphCtrl = new DataManagerGraph.controller({
+ connection: connection,
+ });
return vm;
})();
apistatus.controller = function() {
@@ -128,6 +132,11 @@ function ArvApiStatusComponent(connection) {
}, arvModelName+'s'),
]);
})),
+ m('.row', [
+ m('.col-sm-12', [
+ DataManagerGraph.view(vm.dmGraphCtrl)
+ ]),
+ ]),
]),
]);
};
diff --git a/apps/backstage/app/component.dmgraph.js b/apps/backstage/app/component.dmgraph.js
new file mode 100644
index 0000000..dcf9894
--- /dev/null
+++ b/apps/backstage/app/component.dmgraph.js
@@ -0,0 +1,63 @@
+module.exports = DataManagerGraph;
+
+var _ = require('lodash');
+var m = require('mithril');
+// var Morris = require('morrisjs'); /* It's currently a global. */
+
+function DataManagerGraph() {};
+DataManagerGraph.controller = function(opts) {
+ opts.connection.api('Log', 'list', {
+ filters: [['event_type', '=', 'experimental-data-manager-report']],
+ order: ['created_at DESC'],
+ }).then(this.logs = m.prop([])).then(m.redraw);
+ this.configGraph = function configGraph(element, isInitialized, context) {
+ var seriesLabel = {};
+ var data = _.compact(this.logs().map(function(item){
+ try {
+ var p = item().properties;
+ var runId = '' + p.run_info.pid + '/' + p.run_info.start_time;
+ var pt = {
+ seriesLabel: '' + p.run_info.pid,
+ collectionsRead: p.collection_info.collections_read,
+ logCreatedAt: toDate(item().created_at),
+ runStartTime: toDate(p.run_info.start_time),
+ };
+ pt[runId] = pt.collectionsRead;
+ seriesLabel[runId] = p.run_info.pid;
+ return pt;
+ } catch(e) {
+ return null;
+ }
+ }));
+ var chartopts = {
+ element: element,
+ hoverCallback: morrisHoverCallback,
+ xkey: 'logCreatedAt',
+ ykeys: _.keys(seriesLabel),
+ labels: _.values(seriesLabel),
+ resize: true,
+ };
+ if (!isInitialized || !_.isEqual(chartopts, context.chartopts)) {
+ context.chartopts = chartopts;
+ context.chart = new Morris.Line(_.merge({data:data}, chartopts));
+ } else if (!context.chart) {
+ // Initialization crashed, no chart?
+ } else if (!_.isEqual(data, context.data)) {
+ context.chart.setData(data);
+ }
+ context.data = data;
+ }.bind(this);
+ function toDate(timestamp) {
+ // 2015-01-13T01:13:52.281556508Z -> 2015-01-13 01:13:52.281
+ return timestamp.match(/([-\d]{10})T([:\.\d]{8,12})/).slice(1).join(' ');
+ }
+ function morrisHoverCallback(index, options, content, row) {
+ return ''+row.seriesLabel+': '+row.collectionsRead+' collections read @ '+row.logCreatedAt;
+ }
+};
+DataManagerGraph.view = function(ctrl) {
+ return ctrl.logs().length==0 ? [] : m('div', {
+ config: ctrl.configGraph,
+ style: { width: '100%', height: 200 },
+ });
+};
diff --git a/apps/backstage/index.html b/apps/backstage/index.html
index 691d347..a4b6312 100644
--- a/apps/backstage/index.html
+++ b/apps/backstage/index.html
@@ -7,9 +7,13 @@
<title>arvados backstage</title>
<link href="dist/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="dist/bootstrap/css/bootstrap-theme.min.css" rel="stylesheet">
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css">
<link href="app/backstage.css" rel="stylesheet">
</head>
<body>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script src="dist/app.js"></script>
</body>
</html>
commit 3b8e03d5fcd6726c68b5cbaf1cc4df69428c4468
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 03:54:23 2015 -0500
4831: Rearrange some files.
diff --git a/apps/backstage/.gitignore b/apps/backstage/.gitignore
index 7c8727f..e1497d4 100644
--- a/apps/backstage/.gitignore
+++ b/apps/backstage/.gitignore
@@ -1,6 +1,7 @@
dist
-node_modules
+node_modules/*
!node_modules/app
!node_modules/arvados
!node_modules/mithril-jquery
!node_modules/test
+npm-debug.log
diff --git a/apps/backstage/node_modules/arvados/client.js b/apps/backstage/arvados/client.js
similarity index 100%
rename from apps/backstage/node_modules/arvados/client.js
rename to apps/backstage/arvados/client.js
diff --git a/apps/backstage/node_modules/mithril-jquery/index.js b/apps/backstage/mithril-jquery/index.js
similarity index 100%
rename from apps/backstage/node_modules/mithril-jquery/index.js
rename to apps/backstage/mithril-jquery/index.js
diff --git a/apps/backstage/node_modules/arvados b/apps/backstage/node_modules/arvados
new file mode 120000
index 0000000..4f53985
--- /dev/null
+++ b/apps/backstage/node_modules/arvados
@@ -0,0 +1 @@
+../arvados
\ No newline at end of file
diff --git a/apps/backstage/node_modules/mithril-jquery b/apps/backstage/node_modules/mithril-jquery
new file mode 120000
index 0000000..d57928e
--- /dev/null
+++ b/apps/backstage/node_modules/mithril-jquery
@@ -0,0 +1 @@
+../mithril-jquery
\ No newline at end of file
commit 46b7efb1c6013fe695dd9c65ba892adb92e3c7d0
Author: Tom Clegg <tom at curoverse.com>
Date: Mon Jan 19 03:50:56 2015 -0500
4831: Simplify promise use.
diff --git a/apps/backstage/app/component.arv-api-status.js b/apps/backstage/app/component.arv-api-status.js
index 64e8d64..0dff644 100644
--- a/apps/backstage/app/component.arv-api-status.js
+++ b/apps/backstage/app/component.arv-api-status.js
@@ -15,11 +15,13 @@ function ArvApiStatusComponent(connection) {
vm.refresh();
vm.dirty = false;
};
+ vm.keepServices = m.prop();
+ vm.nodes = m.prop();
vm.refresh = function() {
- vm.nodes = vm.connection.api(
- 'Node', 'list', {});
- vm.keepServices = vm.connection.api(
- 'KeepService', 'list', {});
+ vm.connection.api(
+ 'KeepService', 'list', {}).then(vm.keepServices);
+ vm.connection.api(
+ 'Node', 'list', {}).then(vm.nodes);
};
vm.logout = function() {
vm.connection.token(undefined);
diff --git a/apps/backstage/node_modules/arvados/client.js b/apps/backstage/node_modules/arvados/client.js
index 380384d..935cd3e 100644
--- a/apps/backstage/node_modules/arvados/client.js
+++ b/apps/backstage/node_modules/arvados/client.js
@@ -93,17 +93,10 @@ function ArvadosConnection(apiPrefix) {
// modelClass: 'Collection', 'Node', etc.
// action: 'get', 'list', 'update', etc.
// params: {uuid:'foo',filters:[],...}
- // deferred (optional): deferred object for response. If not
- // supplied, a new one is created.
- function api(modelClass, action, params, deferred) {
- deferred = deferred || m.deferred();
- connection.ready.then(function() {
- connection[modelClass][action](params).
- then(updateStore).
- then(deferred.resolve, deferred.reject).
- then(m.redraw);
- }, deferred.reject);
- return deferred.promise;
+ function api(modelClass, action, params) {
+ return connection.ready.then(function() {
+ return connection[modelClass][action](params);
+ }).then(updateStore);
}
// Private instance variables
commit e3829a29b1d8b5a7bdfce82c784812996fec7c47
Author: Tom Clegg <tom at curoverse.com>
Date: Sun Jan 4 03:59:12 2015 -0500
4831: Add tb05z
diff --git a/apps/backstage/app/backstage-routes.js b/apps/backstage/app/backstage-routes.js
index 0575620..defe480 100644
--- a/apps/backstage/app/backstage-routes.js
+++ b/apps/backstage/app/backstage-routes.js
@@ -12,7 +12,7 @@ var m = require('mithril')
window.jQuery = require('jquery');
require('bootstrap');
-var connections = m.prop('4xphq qr1hi 9tee4 su92l bogus'.split(' ').map(
+var connections = m.prop('4xphq qr1hi 9tee4 su92l tb05z'.split(' ').map(
function(site) {
return ArvadosConnection.make(site);
}));
commit 0732b731453ca16312cb6a59aa4af2a6bdbab7f7
Author: Tom Clegg <tom at curoverse.com>
Date: Wed Dec 31 16:21:02 2014 -0500
4831: Add eof() controller method.
diff --git a/apps/backstage/app/component.arv-list.js b/apps/backstage/app/component.arv-list.js
index d2dbf13..1414db0 100644
--- a/apps/backstage/app/component.arv-list.js
+++ b/apps/backstage/app/component.arv-list.js
@@ -21,6 +21,10 @@ function ArvListComponent(connection, arvModelName, contentModule) {
function getMoreItems() {
return this.vm.getMoreItems.apply(this.vm, arguments);
}
+ Controller.prototype.eof =
+ function eof() {
+ return this.vm.eof();
+ }
Controller.prototype.currentFilter =
function currentFilter(key, attr, operator, operand) {
if (arguments.length > 1) {
commit a27e3f6ad0077c8a84c06ffb2862aa716167cbb8
Author: Tom Clegg <tom at curoverse.com>
Date: Wed Dec 31 16:20:33 2014 -0500
4831: Change ObjectType dropdown label.
diff --git a/apps/backstage/app/filter.js b/apps/backstage/app/filter.js
index 19c95bc..7d2fdff 100644
--- a/apps/backstage/app/filter.js
+++ b/apps/backstage/app/filter.js
@@ -27,7 +27,7 @@ function FilterObjectType(opts) {
m('.input-group.input-group-sm', [
m('.input-group-btn', [
m('button.btn.btn-default.dropdown-toggle[type="button"][data-toggle="dropdown"]', [
- ctrl.currentFilter() ? ctrl.currentFilter()[2].replace(/^.*#/,'') : 'Type',
+ ctrl.currentFilter() ? ctrl.currentFilter()[2].replace(/^.*#/,'') : 'Any type',
' ',
m('span.caret'),
]),
diff --git a/apps/backstage/test/unit/filter.js b/apps/backstage/test/unit/filter.js
index 462162c..a99e64d 100644
--- a/apps/backstage/test/unit/filter.js
+++ b/apps/backstage/test/unit/filter.js
@@ -34,12 +34,12 @@ suite('Filter', function() {
suite('ObjectType', function() {
test("uses existing filter value as initial label", function() {
f = prep(Filter.ObjectType, ['fakeAttr','is_a','arvados#collection']);
- c.assert.lengthOf(m$('.dropdown-toggle:contains(Type)', f.vdom), 0);
+ c.assert.lengthOf(m$('.dropdown-toggle:contains(Any type)', f.vdom), 0);
c.assert.lengthOf(m$('.dropdown-toggle:contains(collection)', f.vdom), 1);
});
- test("uses 'Type' as initial label if no current filter", function() {
+ test("uses 'Any type' as initial label if no current filter", function() {
f = prep(Filter.ObjectType, undefined);
- c.assert.lengthOf(m$('.dropdown-toggle:contains(Type)', f.vdom), 1);
+ c.assert.lengthOf(m$('.dropdown-toggle:contains(Any type)', f.vdom), 1);
});
test("calls currentFilter when selection clicked", function() {
f = prep(Filter.ObjectType);
commit d3bcddaca100415eed3f5aa37994d2dd5a7e5d2a
Author: Tom Clegg <tom at curoverse.com>
Date: Wed Dec 31 16:20:00 2014 -0500
4831: Detect EOF using items_available/offset in API response
diff --git a/apps/backstage/app/component.arv-list.js b/apps/backstage/app/component.arv-list.js
index c859c12..d2dbf13 100644
--- a/apps/backstage/app/component.arv-list.js
+++ b/apps/backstage/app/component.arv-list.js
@@ -96,7 +96,9 @@ function ArvListComponent(connection, arvModelName, contentModule) {
// new one. Ignore.
return;
}
- vm.eof(newItems.length === 0);
+ vm.eof(newItems.length === 0 ||
+ (typeof newItems.offset === 'number' &&
+ newItems.items_available === newItems.offset + newItems.length));
vm.items(vm.items().concat(newItems));
}, vm.eof).then(vm.makeItemViews).then(function() {
// Give the new items a chance to render before
commit a8970774fdea988769845fa60901bf7d58369552
Author: Tom Clegg <tom at curoverse.com>
Date: Wed Dec 31 15:56:59 2014 -0500
4831: Add test methods.
diff --git a/apps/backstage/Makefile b/apps/backstage/Makefile
index 209a8ae..035cffd 100644
--- a/apps/backstage/Makefile
+++ b/apps/backstage/Makefile
@@ -25,5 +25,10 @@ server: build
[ -e $(NPMBIN)/harp ] || npm install harp
$(NPMBIN)/harp server --port 9000
-test: build-test build-assets
+test-phantomjs: build-test build-assets
$(NPMBIN)/mocha-phantomjs test.html
+test-watch:
+ $(NPMBIN)/mocha --watch
+.PHONY: test
+test:
+ $(NPMBIN)/mocha
diff --git a/apps/backstage/README.md b/apps/backstage/README.md
index d16a038..dd92262 100644
--- a/apps/backstage/README.md
+++ b/apps/backstage/README.md
@@ -1,20 +1,57 @@
-Prerequisites
-=============
+# Install
- sudo apt-get install nodejs
+## Prerequisites
-Install dependencies and build dist files
-=========================================
+```
+sudo apt-get install nodejs
+```
- make
+## Install dependencies and build dist files
-Update dist files and run a dev server
-======================================
+```
+make
+```
- make server
+# Develop
-Update dependencies
-===================
+## Update dist files and run a dev server
- npm update
- make
+```
+make server
+```
+
+## Update dependencies
+
+```
+npm update && make
+```
+
+# Test
+
+## Run test suite
+
+This uses mocha, node.js, and jsdom.
+
+```
+make test
+```
+
+Run mocha in "watch" mode to re-run tests whenever you change a source file.
+
+```
+make test-watch
+```
+
+## Run test suite using phantomjs
+
+```
+make test-phantomjs
+```
+
+## Run test suite using a real browser
+
+```
+make server
+```
+
+Point your browser at http://localhost:9000/test.html
commit b659792bfad5a62adeb9b959dc7d1b99c6f889a5
Author: Tom Clegg <tom at curoverse.com>
Date: Wed Dec 31 10:45:07 2014 -0500
4831: Rearrange mithril-dom as mithril-jquery
diff --git a/apps/backstage/.gitignore b/apps/backstage/.gitignore
index a2fa9ac..7c8727f 100644
--- a/apps/backstage/.gitignore
+++ b/apps/backstage/.gitignore
@@ -2,4 +2,5 @@ dist
node_modules
!node_modules/app
!node_modules/arvados
+!node_modules/mithril-jquery
!node_modules/test
diff --git a/apps/backstage/node_modules/mithril-jquery/index.js b/apps/backstage/node_modules/mithril-jquery/index.js
new file mode 100644
index 0000000..bd6e3bc
--- /dev/null
+++ b/apps/backstage/node_modules/mithril-jquery/index.js
@@ -0,0 +1,40 @@
+module.exports = mJquery;
+
+var m = require('mithril');
+var global = (function() { return this })();
+var usingWin = global;
+var ready;
+
+mJquery.ready = function() {
+ if (ready) {
+ // Already done, or underway.
+ } else if (typeof window !== 'undefined' && global === window) {
+ ready = m.deferred();
+ ready.resolve(require('jquery'));
+ } else {
+ ready = m.deferred();
+ require('jsdom').env({
+ html: '<!doctype html><html></html>',
+ scripts: [
+ '../jquery/dist/jquery.js',
+ '../mithril/mithril.js',
+ ],
+ done: function(err, win) {
+ if (err) {
+ console.log("jsdom setup failed: "+JSON.stringify(err));
+ ready.reject(err);
+ } else {
+ usingWin = win;
+ ready.resolve(win.jQuery);
+ }
+ }
+ });
+ }
+ return ready.promise;
+}
+
+function mJquery(selector, cell) {
+ var $div = ready.promise()('<div></div>');
+ (usingWin.m || m).render($div[0], cell);
+ return ready.promise()(selector, $div[0]);
+}
diff --git a/apps/backstage/test/mithril-dom.js b/apps/backstage/test/mithril-dom.js
deleted file mode 100644
index e1fd8d0..0000000
--- a/apps/backstage/test/mithril-dom.js
+++ /dev/null
@@ -1,33 +0,0 @@
-var $ = require('jquery')
-, jsdom = require('jsdom')
-, m = require('mithril');
-
-module.exports = md;
-
-var global = (function() { return this })();
-var jsdomWin = global;
-
-md.ready = function(cb) {
- if (typeof window !== 'undefined' && global === window) {
- cb($);
- return;
- }
- jsdom.env({
- html: '<html></html>',
- scripts: [
- '../node_modules/jquery/dist/jquery.js',
- '../node_modules/mithril/mithril.js',
- ],
- done: function(err, win) {
- jsdomWin = win;
- $ = win.jQuery;
- cb($);
- }
- });
-}
-
-function md(cell) {
- var div = $('<div></div>')[0];
- (jsdomWin.m || m).render(div, cell);
- return div.children;
-}
diff --git a/apps/backstage/test/mocha.opts b/apps/backstage/test/mocha.opts
new file mode 100644
index 0000000..5efaf24
--- /dev/null
+++ b/apps/backstage/test/mocha.opts
@@ -0,0 +1 @@
+--ui tdd
diff --git a/apps/backstage/test/unit/filter.js b/apps/backstage/test/unit/filter.js
index 427866f..462162c 100644
--- a/apps/backstage/test/unit/filter.js
+++ b/apps/backstage/test/unit/filter.js
@@ -1,7 +1,7 @@
var Filter = require('app/filter')
, chai = require('chai')
, m = require('mithril')
-, md = require('test/mithril-dom')
+, m$ = require('mithril-jquery')
, mq = require('mithril-query')
, sinon = require('sinon')
, $ = require('jquery')
@@ -9,53 +9,41 @@ var Filter = require('app/filter')
, s = sinon;
suite('Filter', function() {
- setup(function(done) {
- md.ready(function(jQuery) {
- $ = jQuery;
- done();
- });
- });
+ setup(m$.ready);
function prep(filterClass, initialFilter) {
var f = {};
f.tested = new filterClass({attr: 'fakeAttr'});
f.cfSpy = sinon.stub();
f.cfSpy.withArgs().returns(initialFilter);
f.ctrl = {currentFilter: f.cfSpy};
- f.rendered = f.tested.view(f.ctrl);
- f.domfrag = mq(f.rendered);
+ f.vdom = f.tested.view(f.ctrl);
return f;
}
suite('AnyText', function() {
- test("default is existing filter value", function() {
+ test("uses existing filter as initial input value", function() {
f = prep(Filter.AnyText, ['any','ilike','%quux%']);
- f.rendered = f.tested.view(f.ctrl);
- f.domfrag = mq(f.rendered);
- c.assert.equal(f.domfrag.first('input').attrs.value, "quux");
+ c.assert.equal(mq(f.vdom).first('input').attrs.value, "quux");
});
- test("fires currentFilter on input change", function() {
+ test("calls currentFilter when input changes", function() {
f = prep(Filter.AnyText);
- f.domfrag.setValue('input', 'qux');
+ mq(f.vdom).setValue('input', 'qux');
// Should call again to set new filter value
s.assert.calledWith(f.cfSpy, 'any', 'ilike', '%qux%');
});
});
suite('ObjectType', function() {
- test("default is existing filter value", function() {
+ test("uses existing filter value as initial label", function() {
f = prep(Filter.ObjectType, ['fakeAttr','is_a','arvados#collection']);
- f.rendered = f.tested.view(f.ctrl);
- f.md = md(f.rendered);
- c.assert.lengthOf($('.dropdown-toggle:contains(Type)', f.md), 0);
- c.assert.lengthOf($('.dropdown-toggle:contains(collection)', f.md), 1);
+ c.assert.lengthOf(m$('.dropdown-toggle:contains(Type)', f.vdom), 0);
+ c.assert.lengthOf(m$('.dropdown-toggle:contains(collection)', f.vdom), 1);
});
- test("show generic label if no existing filter value", function() {
+ test("uses 'Type' as initial label if no current filter", function() {
f = prep(Filter.ObjectType, undefined);
- f.rendered = f.tested.view(f.ctrl);
- f.md = md(f.rendered);
- c.assert.lengthOf($('.dropdown-toggle:contains(Type)', f.md), 1);
+ c.assert.lengthOf(m$('.dropdown-toggle:contains(Type)', f.vdom), 1);
});
- test("fires currentFilter on selection", function() {
+ test("calls currentFilter when selection clicked", function() {
f = prep(Filter.ObjectType);
- f.domfrag.click('li a[data-value="arvados#pipelineInstance"]');
+ mq(f.vdom).click('li a[data-value="arvados#pipelineInstance"]');
s.assert.calledOn(f.cfSpy, f.ctrl);
s.assert.calledWith(f.cfSpy, 'fakeAttr', 'is_a', 'arvados#pipelineInstance');
});
commit 791546592e753f2b59f563d2d0ddb82fb48b7b1a
Author: Tom Clegg <tom at curoverse.com>
Date: Wed Dec 31 03:40:14 2014 -0500
4831: Tests pass with "make test", "mocha --watch", and browser at /test.html
diff --git a/apps/backstage/Makefile b/apps/backstage/Makefile
index 305f19b..209a8ae 100644
--- a/apps/backstage/Makefile
+++ b/apps/backstage/Makefile
@@ -6,17 +6,18 @@ clean:
rm -rvf dist
npmdeps:
- for x in bootstrap bower browserify chai chai-jquery jquery mithril mithril-query mocha mocha-phantomjs sinon; do [ -d "$(NPMBIN)/../$$x" ] || npm install $$x --save-dev; done
+ ulimit -n 4000; for x in bootstrap bower browserify chai chai-jquery jquery jsdom mithril mithril-query mocha mocha-phantomjs sinon; do [ -d "$(NPMBIN)/../$$x" ] || npm install $$x --save-dev; done
# This uses --debug to enable source maps.
-build: build-app build-test build-css
+build: build-app build-test build-assets
build-app:
mkdir -p dist
- $(NPMBIN)/browserify --debug -o dist/app.js app/app.js
+ ulimit -n 4000; $(NPMBIN)/browserify --debug -o dist/app.js app/app.js
build-test:
mkdir -p dist
- $(NPMBIN)/browserify --debug -o dist/test.js test/runner.js
-build-css:
+ ulimit -n 4000; $(NPMBIN)/browserify --debug -o dist/test.js test/runner.js
+build-assets: dist/bootstrap
+dist/bootstrap:
mkdir -p dist
rsync -a $(NPMBIN)/../bootstrap/dist/ dist/bootstrap/
@@ -24,5 +25,5 @@ server: build
[ -e $(NPMBIN)/harp ] || npm install harp
$(NPMBIN)/harp server --port 9000
-test: build-test build-css
+test: build-test build-assets
$(NPMBIN)/mocha-phantomjs test.html
diff --git a/apps/backstage/test/mithril-dom.js b/apps/backstage/test/mithril-dom.js
index 6b92b49..e1fd8d0 100644
--- a/apps/backstage/test/mithril-dom.js
+++ b/apps/backstage/test/mithril-dom.js
@@ -1,10 +1,33 @@
var $ = require('jquery')
+, jsdom = require('jsdom')
, m = require('mithril');
module.exports = md;
+var global = (function() { return this })();
+var jsdomWin = global;
+
+md.ready = function(cb) {
+ if (typeof window !== 'undefined' && global === window) {
+ cb($);
+ return;
+ }
+ jsdom.env({
+ html: '<html></html>',
+ scripts: [
+ '../node_modules/jquery/dist/jquery.js',
+ '../node_modules/mithril/mithril.js',
+ ],
+ done: function(err, win) {
+ jsdomWin = win;
+ $ = win.jQuery;
+ cb($);
+ }
+ });
+}
+
function md(cell) {
var div = $('<div></div>')[0];
- m.render(div, cell);
+ (jsdomWin.m || m).render(div, cell);
return div.children;
}
diff --git a/apps/backstage/test/runner.js b/apps/backstage/test/runner.js
index 18270c4..ff4a670 100644
--- a/apps/backstage/test/runner.js
+++ b/apps/backstage/test/runner.js
@@ -1,9 +1,13 @@
var global = (function() { return this })();
-global.$ = global.jQuery = require('jquery');
-chaiJquery = require('chai-jquery');
-require('chai').use(chaiJquery);
-mocha.setup({ui: 'tdd'});
+if (global.mocha) {
+ // Running in browser.
+ global.$ = global.jQuery = require('jquery');
+ var cj = require('chai-jquery');
+ var c = require('chai');
+ c.use(cj);
+ global.mocha.setup({ui: 'tdd'});
+}
require('test/unit/filter.js');
require('test/unit/filterset.js');
diff --git a/apps/backstage/test/unit/filter.js b/apps/backstage/test/unit/filter.js
index d8ffd5e..427866f 100644
--- a/apps/backstage/test/unit/filter.js
+++ b/apps/backstage/test/unit/filter.js
@@ -9,7 +9,13 @@ var Filter = require('app/filter')
, s = sinon;
suite('Filter', function() {
- function setup(filterClass, initialFilter) {
+ setup(function(done) {
+ md.ready(function(jQuery) {
+ $ = jQuery;
+ done();
+ });
+ });
+ function prep(filterClass, initialFilter) {
var f = {};
f.tested = new filterClass({attr: 'fakeAttr'});
f.cfSpy = sinon.stub();
@@ -21,13 +27,13 @@ suite('Filter', function() {
}
suite('AnyText', function() {
test("default is existing filter value", function() {
- f = setup(Filter.AnyText, ['any','ilike','%quux%']);
+ f = prep(Filter.AnyText, ['any','ilike','%quux%']);
f.rendered = f.tested.view(f.ctrl);
f.domfrag = mq(f.rendered);
c.assert.equal(f.domfrag.first('input').attrs.value, "quux");
});
test("fires currentFilter on input change", function() {
- f = setup(Filter.AnyText);
+ f = prep(Filter.AnyText);
f.domfrag.setValue('input', 'qux');
// Should call again to set new filter value
s.assert.calledWith(f.cfSpy, 'any', 'ilike', '%qux%');
@@ -35,20 +41,20 @@ suite('Filter', function() {
});
suite('ObjectType', function() {
test("default is existing filter value", function() {
- f = setup(Filter.ObjectType, ['fakeAttr','is_a','arvados#collection']);
+ f = prep(Filter.ObjectType, ['fakeAttr','is_a','arvados#collection']);
f.rendered = f.tested.view(f.ctrl);
f.md = md(f.rendered);
c.assert.lengthOf($('.dropdown-toggle:contains(Type)', f.md), 0);
c.assert.lengthOf($('.dropdown-toggle:contains(collection)', f.md), 1);
});
test("show generic label if no existing filter value", function() {
- f = setup(Filter.ObjectType, undefined);
+ f = prep(Filter.ObjectType, undefined);
f.rendered = f.tested.view(f.ctrl);
f.md = md(f.rendered);
c.assert.lengthOf($('.dropdown-toggle:contains(Type)', f.md), 1);
});
test("fires currentFilter on selection", function() {
- f = setup(Filter.ObjectType);
+ f = prep(Filter.ObjectType);
f.domfrag.click('li a[data-value="arvados#pipelineInstance"]');
s.assert.calledOn(f.cfSpy, f.ctrl);
s.assert.calledWith(f.cfSpy, 'fakeAttr', 'is_a', 'arvados#pipelineInstance');
diff --git a/apps/backstage/test/webdriver-client.js b/apps/backstage/test/webdriver-client.js
index 752a797..54996dc 100644
--- a/apps/backstage/test/webdriver-client.js
+++ b/apps/backstage/test/webdriver-client.js
@@ -9,5 +9,6 @@ var client = webdriverjs.remote({
// However, if anything goes wrong, remove this to see more details
// logLevel: 'silent'
});
-client.init();
+// client.init();
+
module.exports = client;
commit 67422ce6e67a59d6c7d100b26f1e377e6eeaa870
Author: Tom Clegg <tom at curoverse.com>
Date: Wed Dec 31 01:56:10 2014 -0500
4831: Set initial values for filters.
diff --git a/apps/backstage/app/filter.js b/apps/backstage/app/filter.js
index 1da6159..19c95bc 100644
--- a/apps/backstage/app/filter.js
+++ b/apps/backstage/app/filter.js
@@ -9,8 +9,12 @@ function FilterAnyText() {
this.view = function(ctrl) {
return m('.input-group.input-group-sm', [
m('input.form-control[type="text"][placeholder="Search"]',
- {oninput: m.withAttr('value', setFilter)}),
+ {value: getFilter(),
+ oninput: m.withAttr('value', setFilter)}),
]);
+ function getFilter() {
+ return ctrl.currentFilter() ? ctrl.currentFilter()[2].replace(/^%(.*)%$/, '$1') : ''
+ }
function setFilter(value) {
ctrl.currentFilter('any', 'ilike', '%'+value+'%')
}
@@ -29,10 +33,10 @@ function FilterObjectType(opts) {
]),
m('ul.dropdown-menu[role="menu"]', [
m('li', [
- m('a[href="#"]', {onclick: ctrl.currentFilter.bind(ctrl, opts.attr, 'is_a', 'arvados#collection')}, 'Collection'),
+ m('a[href="#"][data-value="arvados#collection"]', {onclick: ctrl.currentFilter.bind(ctrl, opts.attr, 'is_a', 'arvados#collection')}, 'Collection'),
]),
m('li', [
- m('a[href="#"]', {onclick: ctrl.currentFilter.bind(ctrl, opts.attr, 'is_a', 'arvados#pipelineInstance')}, 'Pipeline instance'),
+ m('a[href="#"][data-value="arvados#pipelineInstance"]', {onclick: ctrl.currentFilter.bind(ctrl, opts.attr, 'is_a', 'arvados#pipelineInstance')}, 'Pipeline instance'),
]),
]),
]),
diff --git a/apps/backstage/test/mithril-dom.js b/apps/backstage/test/mithril-dom.js
new file mode 100644
index 0000000..6b92b49
--- /dev/null
+++ b/apps/backstage/test/mithril-dom.js
@@ -0,0 +1,10 @@
+var $ = require('jquery')
+, m = require('mithril');
+
+module.exports = md;
+
+function md(cell) {
+ var div = $('<div></div>')[0];
+ m.render(div, cell);
+ return div.children;
+}
diff --git a/apps/backstage/test/unit/filter.js b/apps/backstage/test/unit/filter.js
index 3756471..d8ffd5e 100644
--- a/apps/backstage/test/unit/filter.js
+++ b/apps/backstage/test/unit/filter.js
@@ -1,40 +1,57 @@
-var mq = require('mithril-query')
-, Filter = require('app/filter')
+var Filter = require('app/filter')
+, chai = require('chai')
+, m = require('mithril')
+, md = require('test/mithril-dom')
+, mq = require('mithril-query')
, sinon = require('sinon')
+, $ = require('jquery')
+, c = chai
, s = sinon;
suite('Filter', function() {
- function setup(filterClass) {
+ function setup(filterClass, initialFilter) {
var f = {};
f.tested = new filterClass({attr: 'fakeAttr'});
- f.cfSpy = sinon.spy();
+ f.cfSpy = sinon.stub();
+ f.cfSpy.withArgs().returns(initialFilter);
f.ctrl = {currentFilter: f.cfSpy};
f.rendered = f.tested.view(f.ctrl);
f.domfrag = mq(f.rendered);
return f;
}
suite('AnyText', function() {
- test.skip("default is existing filter value", function() {
- // TODO
+ test("default is existing filter value", function() {
+ f = setup(Filter.AnyText, ['any','ilike','%quux%']);
+ f.rendered = f.tested.view(f.ctrl);
+ f.domfrag = mq(f.rendered);
+ c.assert.equal(f.domfrag.first('input').attrs.value, "quux");
});
test("fires currentFilter on input change", function() {
f = setup(Filter.AnyText);
- // XXX: should call once to retrieve current filter value
- // s.assert.calledWith(f.cfSpy);
f.domfrag.setValue('input', 'qux');
// Should call again to set new filter value
s.assert.calledWith(f.cfSpy, 'any', 'ilike', '%qux%');
});
});
suite('ObjectType', function() {
- test.skip("default is existing filter value", function() {
- // TODO
+ test("default is existing filter value", function() {
+ f = setup(Filter.ObjectType, ['fakeAttr','is_a','arvados#collection']);
+ f.rendered = f.tested.view(f.ctrl);
+ f.md = md(f.rendered);
+ c.assert.lengthOf($('.dropdown-toggle:contains(Type)', f.md), 0);
+ c.assert.lengthOf($('.dropdown-toggle:contains(collection)', f.md), 1);
+ });
+ test("show generic label if no existing filter value", function() {
+ f = setup(Filter.ObjectType, undefined);
+ f.rendered = f.tested.view(f.ctrl);
+ f.md = md(f.rendered);
+ c.assert.lengthOf($('.dropdown-toggle:contains(Type)', f.md), 1);
});
test("fires currentFilter on selection", function() {
f = setup(Filter.ObjectType);
- f.domfrag.click('li a');
+ f.domfrag.click('li a[data-value="arvados#pipelineInstance"]');
s.assert.calledOn(f.cfSpy, f.ctrl);
- s.assert.calledWith(f.cfSpy, 'fakeAttr', 'is_a', 'arvados#collection');
+ s.assert.calledWith(f.cfSpy, 'fakeAttr', 'is_a', 'arvados#pipelineInstance');
});
});
});
commit 3c48f7bf7a14ff938c523a560a0ebc4cd629a090
Author: Tom Clegg <tom at curoverse.com>
Date: Tue Dec 30 23:18:08 2014 -0500
4831: More testing stuff.
diff --git a/apps/backstage/Makefile b/apps/backstage/Makefile
index 65f375d..305f19b 100644
--- a/apps/backstage/Makefile
+++ b/apps/backstage/Makefile
@@ -1,6 +1,6 @@
NPMBIN:=$(shell npm bin)
-all: npmdeps build-dist
+all: npmdeps build
clean:
rm -rvf dist
@@ -9,12 +9,20 @@ npmdeps:
for x in bootstrap bower browserify chai chai-jquery jquery mithril mithril-query mocha mocha-phantomjs sinon; do [ -d "$(NPMBIN)/../$$x" ] || npm install $$x --save-dev; done
# This uses --debug to enable source maps.
-build-dist:
+build: build-app build-test build-css
+build-app:
mkdir -p dist
$(NPMBIN)/browserify --debug -o dist/app.js app/app.js
+build-test:
+ mkdir -p dist
$(NPMBIN)/browserify --debug -o dist/test.js test/runner.js
+build-css:
+ mkdir -p dist
rsync -a $(NPMBIN)/../bootstrap/dist/ dist/bootstrap/
-server: build-dist
+server: build
[ -e $(NPMBIN)/harp ] || npm install harp
$(NPMBIN)/harp server --port 9000
+
+test: build-test build-css
+ $(NPMBIN)/mocha-phantomjs test.html
diff --git a/apps/backstage/test/functional/dashboard.js b/apps/backstage/test/functional/dashboard.js
index 95418c5..df95f64 100644
--- a/apps/backstage/test/functional/dashboard.js
+++ b/apps/backstage/test/functional/dashboard.js
@@ -1,11 +1,12 @@
-require(['chai', 'test/webdriver-client'], function(chai, c) {
- var assert = chai.assert;
- describe('Dashboard page', function() {
- before(function() {
- c.init().url('http://localhost:5555');
- });
- it('has a nav', function() {
- assert(c.isVisible('nav'));
- });
+var chai = require('chai')
+, wd = require('webdriver-client')
+, c = chai;
+
+suite('Dashboard page', function() {
+ setup(function() {
+ wd.url('http://localhost:5555');
+ });
+ test('has a nav', function() {
+ c.assert(wd.isVisible('nav'));
});
});
diff --git a/apps/backstage/test/unit/filter.js b/apps/backstage/test/unit/filter.js
index dc1b6be..3756471 100644
--- a/apps/backstage/test/unit/filter.js
+++ b/apps/backstage/test/unit/filter.js
@@ -4,15 +4,37 @@ var mq = require('mithril-query')
, s = sinon;
suite('Filter', function() {
- test("changing input fires currentFilter", function() {
- var tested = new Filter.AnyText();
- var cfSpy = sinon.spy();
- var ctrl = {currentFilter: cfSpy};
- var v = tested.view(ctrl);
- s.assert.notCalled(cfSpy);
- mq(v).setValue('input', 'qux');
- s.assert.calledOnce(cfSpy);
- s.assert.calledOn(cfSpy, ctrl);
- s.assert.calledWith(cfSpy, 'any', 'ilike', '%qux%');
+ function setup(filterClass) {
+ var f = {};
+ f.tested = new filterClass({attr: 'fakeAttr'});
+ f.cfSpy = sinon.spy();
+ f.ctrl = {currentFilter: f.cfSpy};
+ f.rendered = f.tested.view(f.ctrl);
+ f.domfrag = mq(f.rendered);
+ return f;
+ }
+ suite('AnyText', function() {
+ test.skip("default is existing filter value", function() {
+ // TODO
+ });
+ test("fires currentFilter on input change", function() {
+ f = setup(Filter.AnyText);
+ // XXX: should call once to retrieve current filter value
+ // s.assert.calledWith(f.cfSpy);
+ f.domfrag.setValue('input', 'qux');
+ // Should call again to set new filter value
+ s.assert.calledWith(f.cfSpy, 'any', 'ilike', '%qux%');
+ });
+ });
+ suite('ObjectType', function() {
+ test.skip("default is existing filter value", function() {
+ // TODO
+ });
+ test("fires currentFilter on selection", function() {
+ f = setup(Filter.ObjectType);
+ f.domfrag.click('li a');
+ s.assert.calledOn(f.cfSpy, f.ctrl);
+ s.assert.calledWith(f.cfSpy, 'fakeAttr', 'is_a', 'arvados#collection');
+ });
});
});
diff --git a/apps/backstage/test/webdriver-client.js b/apps/backstage/test/webdriver-client.js
index dc40522..752a797 100644
--- a/apps/backstage/test/webdriver-client.js
+++ b/apps/backstage/test/webdriver-client.js
@@ -1,13 +1,13 @@
-define(['webdriverjs'], function(webdriverjs) {
- var client = webdriverjs.remote({
- desiredCapabilities: {
- // http://code.google.com/p/selenium/wiki/DesiredCapabilities
- browserName: 'phantomjs'
- },
- // webdriverjs has a lot of output which is generally useless
- // However, if anything goes wrong, remove this to see more details
- // logLevel: 'silent'
- });
- client.init();
- return client;
+var webdriverjs = require('webdriverjs');
+
+var client = webdriverjs.remote({
+ desiredCapabilities: {
+ // http://code.google.com/p/selenium/wiki/DesiredCapabilities
+ browserName: 'phantomjs'
+ },
+ // webdriverjs has a lot of output which is generally useless
+ // However, if anything goes wrong, remove this to see more details
+ // logLevel: 'silent'
});
+client.init();
+module.exports = client;
commit 17114546f7bd6b2d442d0ac384111bb73f1accd8
Author: Tom Clegg <tom at curoverse.com>
Date: Tue Dec 30 18:56:52 2014 -0500
4831: "Backstage" browser client.
diff --git a/apps/backstage/.gitignore b/apps/backstage/.gitignore
new file mode 100644
index 0000000..a2fa9ac
--- /dev/null
+++ b/apps/backstage/.gitignore
@@ -0,0 +1,5 @@
+dist
+node_modules
+!node_modules/app
+!node_modules/arvados
+!node_modules/test
diff --git a/apps/backstage/Makefile b/apps/backstage/Makefile
new file mode 100644
index 0000000..65f375d
--- /dev/null
+++ b/apps/backstage/Makefile
@@ -0,0 +1,20 @@
+NPMBIN:=$(shell npm bin)
+
+all: npmdeps build-dist
+
+clean:
+ rm -rvf dist
+
+npmdeps:
+ for x in bootstrap bower browserify chai chai-jquery jquery mithril mithril-query mocha mocha-phantomjs sinon; do [ -d "$(NPMBIN)/../$$x" ] || npm install $$x --save-dev; done
+
+# This uses --debug to enable source maps.
+build-dist:
+ mkdir -p dist
+ $(NPMBIN)/browserify --debug -o dist/app.js app/app.js
+ $(NPMBIN)/browserify --debug -o dist/test.js test/runner.js
+ rsync -a $(NPMBIN)/../bootstrap/dist/ dist/bootstrap/
+
+server: build-dist
+ [ -e $(NPMBIN)/harp ] || npm install harp
+ $(NPMBIN)/harp server --port 9000
diff --git a/apps/backstage/README.md b/apps/backstage/README.md
new file mode 100644
index 0000000..d16a038
--- /dev/null
+++ b/apps/backstage/README.md
@@ -0,0 +1,20 @@
+Prerequisites
+=============
+
+ sudo apt-get install nodejs
+
+Install dependencies and build dist files
+=========================================
+
+ make
+
+Update dist files and run a dev server
+======================================
+
+ make server
+
+Update dependencies
+===================
+
+ npm update
+ make
diff --git a/apps/backstage/app/app.js b/apps/backstage/app/app.js
new file mode 100644
index 0000000..fc60fb1
--- /dev/null
+++ b/apps/backstage/app/app.js
@@ -0,0 +1 @@
+require('app/backstage-routes');
diff --git a/apps/backstage/app/backstage-layout.js b/apps/backstage/app/backstage-layout.js
new file mode 100644
index 0000000..5229669
--- /dev/null
+++ b/apps/backstage/app/backstage-layout.js
@@ -0,0 +1,39 @@
+module.exports = BackstageLayoutView;
+
+var m = require('mithril');
+
+function BackstageLayoutView() {
+ return [
+ m('.navbar.navbar-default', {role: 'navigation'}, [
+ m('.container-fluid', [
+ m('.navbar-header', [
+ m('button.navbar-toggle.collapsed',
+ {'data-toggle': 'collapse', 'data-target': '#navbar'},
+ [0,0,0].map(function() {
+ return m('span.icon-bar');
+ })),
+ m("a.navbar-brand[href='/']", {config:m.route},
+ 'Arvados::Backstage'),
+ ]),
+ m('#navbar.navbar-collapse.collapse', [
+ m('ul.nav.navbar-nav', [
+ m('li', [
+ m("a[href='/']", {config:m.route},
+ 'Dashboard'),
+ ]),
+ ]),
+ m('p.navbar-text', [siteBreadcrumb()]),
+ ]),
+ ]),
+ ]),
+ m('.container-fluid', this.views.content()),
+ ];
+ function siteBreadcrumb() {
+ var txt;
+ if (txt = m.route.param('connection'))
+ return txt;
+ if ((txt = m.route.param('uuid')) && txt.substr(5,1)=='-')
+ return txt.substr(0,5);
+ return '';
+ }
+}
diff --git a/apps/backstage/app/backstage-login.js b/apps/backstage/app/backstage-login.js
new file mode 100644
index 0000000..13551d7
--- /dev/null
+++ b/apps/backstage/app/backstage-login.js
@@ -0,0 +1,18 @@
+module.exports = BackstageLoginComponent;
+
+var m = require('mithril');
+
+function BackstageLoginComponent() {
+ var callback = {};
+ callback.controller = function() {
+ var tokens = {};
+ try {
+ tokens = JSON.parse(window.localStorage.tokens);
+ } catch(e) {}
+ tokens[m.route.param('apiPrefix')] = m.route.param('api_token');
+ window.localStorage.tokens = JSON.stringify(tokens);
+ m.route(m.route.param('return_to') || '/');
+ }
+ callback.view = function(ctrl) {}
+ return callback;
+}
diff --git a/apps/backstage/app/backstage-routes.js b/apps/backstage/app/backstage-routes.js
new file mode 100644
index 0000000..0575620
--- /dev/null
+++ b/apps/backstage/app/backstage-routes.js
@@ -0,0 +1,31 @@
+module.exports = true;
+
+var m = require('mithril')
+, ArvadosConnection = require('arvados/client')
+, Layout = require('app/base-layout')
+, BackstageLayoutView = require('app/backstage-layout')
+, BackstageLoginComponent = require('app/backstage-login')
+, ArvApiDirectoryComponent = require('app/component.arv-api-directory')
+, ArvIndexComponent = require('app/component.arv-index')
+, ArvShowComponent = require('app/component.arv-show');
+
+window.jQuery = require('jquery');
+require('bootstrap');
+
+var connections = m.prop('4xphq qr1hi 9tee4 su92l bogus'.split(' ').map(
+ function(site) {
+ return ArvadosConnection.make(site);
+ }));
+
+m.route(document.body, '/', {
+ '/login-callback': new BackstageLoginComponent(),
+ '/': new Layout(BackstageLayoutView, {
+ content: new ArvApiDirectoryComponent(connections)
+ }),
+ '/list/:connection/:modelName': new Layout(BackstageLayoutView, {
+ content: ArvIndexComponent
+ }),
+ '/show/:uuid': new Layout(BackstageLayoutView, {
+ content: ArvShowComponent
+ }),
+});
diff --git a/apps/backstage/app/backstage.css b/apps/backstage/app/backstage.css
new file mode 100644
index 0000000..55a9c0f
--- /dev/null
+++ b/apps/backstage/app/backstage.css
@@ -0,0 +1,3 @@
+.lighten {
+ opacity: 0.5;
+}
diff --git a/apps/backstage/app/base-component.js b/apps/backstage/app/base-component.js
new file mode 100644
index 0000000..9aab6f4
--- /dev/null
+++ b/apps/backstage/app/base-component.js
@@ -0,0 +1,6 @@
+module.exports = BaseComponent;
+
+var BaseController = require('app/base-ctrl');
+
+BaseComponent.prototype.controller = BaseController;
+function BaseComponent() {}
diff --git a/apps/backstage/app/base-ctrl.js b/apps/backstage/app/base-ctrl.js
new file mode 100644
index 0000000..68f9b8f
--- /dev/null
+++ b/apps/backstage/app/base-ctrl.js
@@ -0,0 +1,24 @@
+module.exports = BaseController;
+
+var m = require('mithril');
+
+BaseController.prototype.selectUuid =
+ function selectUuid(uuid) {
+ m.route('/show/' + uuid);
+ }
+BaseController.prototype.onunload =
+ function onunload() {
+ var todo = [];
+ if (this.controllers instanceof Function) {
+ todo = this.controllers();
+ } else if (this.controllers instanceof Array) {
+ todo = this.controllers;
+ }
+ todo.map(function(ctrl) {
+ if (ctrl.onunload instanceof Function)
+ ctrl.onunload();
+ });
+ }
+function BaseController(vm) {
+ this.vm = vm;
+}
diff --git a/apps/backstage/app/base-layout.js b/apps/backstage/app/base-layout.js
new file mode 100644
index 0000000..29adbc0
--- /dev/null
+++ b/apps/backstage/app/base-layout.js
@@ -0,0 +1,37 @@
+// Layout class. Instances are suitable for passing to m.route().
+//
+// Usage:
+// new Layout(viewFunction, {main: FooModuleClass, nav: NavComponent})
+//
+// viewFunction is expected to include this.views.main() and
+// this.views.nav() somewhere in its return value.
+//
+// Content can be given as a class (in which case instances are made
+// with new FooModuleClass()) or as a component instance (i.e., an
+// object with a function named 'controller').
+//
+// The layout is responsible for creating and unloading controllers.
+
+module.exports = Layout;
+
+var BaseComponent = require('app/base-component')
+, BaseController = require('app/base-ctrl');
+
+Layout.prototype = BaseComponent;
+function Layout(layoutView, innerModules) {
+ var layout = this;
+ this.views = {};
+ this.controller = function controller() {
+ this.controllers = [];
+ Object.keys(innerModules).map(function(key) {
+ var module = innerModules[key];
+ var component = (module.controller instanceof Function) ? module : new module();
+ var ctrl = new component.controller();
+ var view = component.view.bind(component.view, ctrl);
+ this.controllers.push(ctrl);
+ layout.views[key] = view;
+ }, this);
+ };
+ this.controller.prototype = new BaseController();
+ this.view = layoutView.bind(this, this.controller);
+}
diff --git a/apps/backstage/app/component.arv-api-directory.js b/apps/backstage/app/component.arv-api-directory.js
new file mode 100644
index 0000000..1370ede
--- /dev/null
+++ b/apps/backstage/app/component.arv-api-directory.js
@@ -0,0 +1,45 @@
+module.exports = ArvApiDirectoryComponent;
+
+var m = require('mithril')
+, BaseComponent = require('app/base-component')
+, BaseController = require('app/base-ctrl')
+, ArvApiStatusComponent = require('app/component.arv-api-status');
+
+ArvApiDirectoryComponent.prototype = new BaseComponent();
+function ArvApiDirectoryComponent(connections) {
+ this.controller = Controller;
+ Controller.prototype = new BaseController();
+ function Controller(vm) {
+ this.vm = vm || {};
+ this.vm.widgets = connections().map(function(conn) {
+ var component = new ArvApiStatusComponent(conn);
+ return {
+ view: component.view,
+ controller: new component.controller(),
+ };
+ });
+ // Give BaseComponent a list of components to unload.
+ this.controllers = function() {
+ return this.vm.widgets.map(function(widget) {
+ return widget.controller;
+ });
+ }.bind(this);
+
+ this.redrawTimer = setInterval(function() {
+ // If redraw is really really cheap, we can do this to make
+ // "#seconds old" timers count in real time.
+ m.redraw();
+ }, 1000);
+ this.onunload = function() {
+ clearTimeout(this.redrawTimer);
+ };
+ };
+ this.view = View;
+ function View(ctrl) {
+ return m('div', [
+ ctrl.vm.widgets.map(function(widget) {
+ return widget.view(widget.controller);
+ })
+ ]);
+ };
+}
diff --git a/apps/backstage/app/component.arv-api-status.js b/apps/backstage/app/component.arv-api-status.js
new file mode 100644
index 0000000..64e8d64
--- /dev/null
+++ b/apps/backstage/app/component.arv-api-status.js
@@ -0,0 +1,133 @@
+module.exports = ArvApiStatusComponent;
+
+var m = require('mithril')
+, util = require('app/util');
+
+function ArvApiStatusComponent(connection) {
+ var apistatus = {};
+ apistatus.vm = (function() {
+ var vm = {};
+ vm.connection = connection;
+ vm.dd = connection.discoveryDoc;
+ vm.dirty = true;
+ vm.init = function() {
+ if (vm.dirty)
+ vm.refresh();
+ vm.dirty = false;
+ };
+ vm.refresh = function() {
+ vm.nodes = vm.connection.api(
+ 'Node', 'list', {});
+ vm.keepServices = vm.connection.api(
+ 'KeepService', 'list', {});
+ };
+ vm.logout = function() {
+ vm.connection.token(undefined);
+ };
+ vm.ddSummary = function() {
+ return !vm.dd() ? {} : {
+ apiVersion: vm.dd().version + ' (' + vm.dd().revision + ')',
+ sourceVersion: m('a', {
+ href: 'https://arvados.org/projects/arvados/repository/changes?rev=' + vm.dd().source_version
+ }, vm.dd().source_version),
+ generatedAt: vm.dd().generatedAt,
+ websocket: util.choose(vm.connection.webSocket().readyState, {
+ 0: m('span.label.label-warning', ['connecting']),
+ 1: m('span.label.label-success', ['OK']),
+ 2: m('span.label.label-danger', ['closing']),
+ 3: m('span.label.label-danger', ['closed']),
+ }) || m('span.label.label-danger',
+ {title: ('advertised websocketUrl: ' +
+ vm.dd().websocketUrl)}, ['none'])
+ }
+ };
+ return vm;
+ })();
+ apistatus.controller = function() {
+ apistatus.vm.init();
+ };
+ apistatus.view = function() {
+ var vm = apistatus.vm;
+ var ddSummary = vm.ddSummary();
+ return m('.panel.panel-info.arv-bs-api-status', [
+ m('.panel-heading', [
+ vm.connection.apiPrefix(),
+ !vm.dd() ? '' : m('.pull-right', [
+ util.choose(!!vm.connection.token(), {
+ true: [function() {
+ return m('a.btn.btn-xs.btn-default',
+ {onclick: vm.logout}, 'Log out');
+ }],
+ false: [function() {
+ return m('a.btn.btn-xs.btn-primary',
+ {href: vm.connection.loginLink()}, 'Log in');
+ }]
+ }),
+ ]),
+ ]),
+ m('.panel-body', !vm.dd() ? [vm.connection.state()] : [
+ m('.row', [
+ m('.col-md-4',
+ Object.keys(ddSummary).map(function(key) {
+ return m('.row', [
+ m('.col-sm-4.lighten', key),
+ m('.col-sm-8', ddSummary[key]),
+ ]);
+ })),
+ m('.col-md-4', [
+ !vm.keepServices() ? '' : m('ul', [
+ '' + vm.keepServices().length + ' Keep services',
+ vm.keepServices().map(function(keepService) {
+ return m('li', [
+ m('span.label.label-default',
+ keepService().service_type),
+ ' ',
+ m('a',
+ {href: '/show/'+keepService().uuid,
+ config: m.route}, [
+ keepService().service_host,
+ ':',
+ keepService().service_port,
+ ]),
+ ]);
+ }),
+ ]),
+ ]),
+ m('.col-md-4', [
+ !vm.nodes() ? '' : m('ul', [
+ '' + vm.nodes().length + ' worker nodes',
+ vm.nodes().filter(function(node) {
+ return node().crunch_worker_state != 'down';
+ }).map(function(node) {
+ return m('li', [
+ m('span.label.label-default', [
+ node().crunch_worker_state,
+ ]),
+ ' ',
+ m('a', {href: '/show/'+node().uuid,
+ config: m.route},
+ node().hostname),
+ ' ',
+ m('span.label.label-info', {title: 'time since last ping'}, [
+ ((new Date() - Date.parse(node().last_ping_at))/1000).toFixed(),
+ 's'
+ ]),
+ ]);
+ }),
+ ]),
+ ]),
+ ]),
+ m('.row', 'Collection Job PipelineInstance'.split(' ').map(function(arvModelName) {
+ return m('.col-sm-2', [
+ m('a.btn.btn-xs.btn-default', {
+ style: 'width: 100%',
+ href: '/list/'+vm.connection.apiPrefix()+'/'+arvModelName,
+ config: m.route
+ }, arvModelName+'s'),
+ ]);
+ })),
+ ]),
+ ]);
+ };
+ return apistatus;
+}
diff --git a/apps/backstage/app/component.arv-index.js b/apps/backstage/app/component.arv-index.js
new file mode 100644
index 0000000..10a0c93
--- /dev/null
+++ b/apps/backstage/app/component.arv-index.js
@@ -0,0 +1,33 @@
+module.exports = ArvIndexComponent;
+
+var m = require('mithril')
+, BaseController = require('app/base-ctrl')
+, ArvListComponent = require('app/component.arv-list')
+, ArvObjectRowComponent = require('app/component.arv-object-row')
+, InfiniteScroll = require('app/infinitescroll');
+
+function ArvIndexComponent() {
+ this.controller = Controller;
+ this.view = function view(ctrl) {
+ return ctrl.vm.scroller.view(ctrl.vm.scrollerCtrl);
+ };
+ function ViewModel() {
+ this.list =
+ new ArvListComponent(null, null, new ArvObjectRowComponent());
+ this.listCtrl =
+ new this.list.controller();
+ this.scroller =
+ new InfiniteScroll(this.listCtrl, this.list.view,
+ {pxThreshold: 200});
+ this.scrollerCtrl =
+ new this.scroller.controller();
+ }
+ function Controller() {
+ this.vm = new ViewModel();
+ }
+ Controller.prototype = new BaseController();
+ Controller.prototype.controllers =
+ function controllers() {
+ return [this.vm.listCtrl, this.vm.scrollerCtrl];
+ }
+}
diff --git a/apps/backstage/app/component.arv-list.js b/apps/backstage/app/component.arv-list.js
new file mode 100644
index 0000000..c859c12
--- /dev/null
+++ b/apps/backstage/app/component.arv-list.js
@@ -0,0 +1,128 @@
+module.exports = ArvListComponent;
+
+var m = require('mithril')
+, ArvadosClient = require('arvados/client')
+, BaseController = require('app/base-ctrl')
+, Filter = require('app/filter')
+, FilterSet = require('app/filterset')
+, util = require('app/util');
+
+function ArvListComponent(connection, arvModelName, contentModule) {
+ this.controller = Controller;
+ this.view = View;
+
+ Controller.prototype = new BaseController();
+ function Controller() {
+ this.vm = new ViewModel();
+ this.vm.init();
+ this.vm.filterSetCtrl = new this.vm.filterSet.controller(this);
+ }
+ Controller.prototype.getMoreItems =
+ function getMoreItems() {
+ return this.vm.getMoreItems.apply(this.vm, arguments);
+ }
+ Controller.prototype.currentFilter =
+ function currentFilter(key, attr, operator, operand) {
+ if (arguments.length > 1) {
+ this.vm.filters[key] = [attr, operator, operand];
+ util.debounce(500, this.vm.resetContent).
+ then(this.vm.resetContent);
+ }
+ return this.vm.filters[key];
+ }
+
+ function ViewModel() {
+ var vm = this;
+ vm.init = function() {
+ vm.arvModelName = arvModelName || m.route.param('modelName');
+ vm.connection = connection || ArvadosClient.make(m.route.param('connection'));
+ vm.filters = {};
+ vm.filterSet = new FilterSet(
+ [['any', Filter.AnyText],
+ ['type', Filter.ObjectType, {attr:'uuid'}]]);
+ vm.inflight = null;
+ vm.listLimit = 30;
+ vm.listOrders = ['created_at desc'];
+ vm.resetContent();
+ };
+ vm.resetContent = function() {
+ if (vm.inflight) {
+ // Forget about current/stale request. TODO: abort the xhr.
+ vm.inflight.reject();
+ vm.inflight = null;
+ }
+ vm.eof = m.prop(false);
+ vm.items = m.prop([]);
+ vm.itemViews = m.prop([]);
+ vm.beforeRender = function() {
+ // On first render, trigger a scroll event to make the
+ // first page of content appear. The scroll handler can
+ // ignore this if (for example) the content view is
+ // invisible now.
+ vm.beforeRender = function() {};
+ window.setTimeout(function() {
+ window.dispatchEvent(new Event('scroll'));
+ }, 1);
+ };
+ };
+ vm.apiFilters = function() {
+ var filters = [];
+ Object.keys(vm.filters).map(function(key) {
+ if (vm.filters[key])
+ filters.push(vm.filters[key]);
+ });
+ return filters;
+ };
+ vm.makeItemViews = function() {
+ vm.itemViews(vm.items().map(function(item) {
+ return contentModule.view.bind(
+ contentModule.view,
+ new contentModule.controller({item:item}));
+ }));
+ };
+ vm.getMoreItems = function() {
+ var inflight;
+ if (vm.inflight || vm.eof())
+ return false;
+ inflight = m.deferred();
+ vm.connection.api(vm.arvModelName, 'list', {
+ filters: vm.apiFilters(),
+ limit: vm.listLimit,
+ offset: vm.items().length,
+ order: vm.listOrders
+ }).then(function(newItems) {
+ if (inflight !== vm.inflight) {
+ // This request has already been superseded by a
+ // new one. Ignore.
+ return;
+ }
+ vm.eof(newItems.length === 0);
+ vm.items(vm.items().concat(newItems));
+ }, vm.eof).then(vm.makeItemViews).then(function() {
+ // Give the new items a chance to render before
+ // resolving the promise. This makes it possible for
+ // the resolve callback to measure the DOM after the
+ // new elements have been added (notably, in order to
+ // keep fetching pages until the scroll threshold is
+ // satisfied).
+ window.setTimeout(inflight.resolve, 50);
+ vm.inflight = null;
+ });
+ return (vm.inflight = inflight).promise;
+ };
+ return vm;
+ }
+
+ function View(ctrl) {
+ ctrl.vm.beforeRender();
+ return [
+ ctrl.vm.filterSet ? ctrl.vm.filterSet.view(ctrl.vm.filterSetCtrl) : '',
+ ctrl.vm.itemViews().map(function(v) {
+ return v();
+ }),
+ ctrl.vm.eof() ? '' : m('.row', {style: 'background: #ffffdd'}, [
+ m('.col-sm-12', {style: 'text-align: center'}, ['...loading...'])
+ ]),
+ ];
+ }
+}
diff --git a/apps/backstage/app/component.arv-object-row.js b/apps/backstage/app/component.arv-object-row.js
new file mode 100644
index 0000000..9222408
--- /dev/null
+++ b/apps/backstage/app/component.arv-object-row.js
@@ -0,0 +1,35 @@
+// Render an arvados object as a <div class="row">.
+//
+// Usage:
+// x = m.prop({}); // fill in [later] using ArvConnection.find, etc.
+// mod = new ArvObjectRowComponent();
+// ctrl = new mod.controller({item: x});
+// mod.view(ctrl)
+module.exports = ArvObjectRowComponent;
+
+var m = require('mithril')
+, BaseComponent = require('app/base-component');
+
+ArvObjectRowComponent.prototype = new BaseComponent();
+function ArvObjectRowComponent() {
+ this.view = function(ctrl) {
+ var _item = ctrl.vm.item();
+ var _owner = _item.owner_uuid ? _item._conn.find(_item.owner_uuid)() : '';
+ return m('.row', [
+ m('.col-sm-3', [
+ m('.btn.btn-xs',
+ {onclick: ctrl.selectUuid.bind(ctrl, _item.uuid)}, [
+ m('span.glyphicon.glyphicon-link'),
+ ]),
+ _item.uuid,
+ ]),
+ m('.col-sm-4', _item.name),
+ m('.col-sm-2', [
+ m('a[href="/show/'+_item.owner_uuid+'"]', {config:m.route}, [
+ _owner && (_owner.full_name || _owner.name)
+ ]),
+ ]),
+ m('.col-sm-2', new Date(_item.created_at).toLocaleString()),
+ ]);
+ };
+}
diff --git a/apps/backstage/app/component.arv-show.js b/apps/backstage/app/component.arv-show.js
new file mode 100644
index 0000000..c7ca0ed
--- /dev/null
+++ b/apps/backstage/app/component.arv-show.js
@@ -0,0 +1,27 @@
+module.exports = ArvShowComponent;
+
+var ArvadosConnection = require('arvados/client')
+, m = require('mithril');
+
+function ArvShowComponent() {
+ this.controller = function() {
+ this.vm = (function() {
+ var vm = {};
+ vm.uuid = m.route.param('uuid');
+ vm.connection = ArvadosConnection.make(vm.uuid.slice(0,5));
+ vm.model = vm.connection.find(vm.uuid);
+ return vm;
+ })();
+ };
+ this.view = function(ctrl) {
+ return [
+ m('.row', [m('.col-sm-12', ctrl.vm.uuid)]),
+ Object.keys(ctrl.vm.model() || {}).map(function(key) {
+ return m('.row', [
+ m('.col-sm-2.lighten', key),
+ m('.col-sm-10', ctrl.vm.model()[key]),
+ ]);
+ }),
+ ];
+ };
+}
diff --git a/apps/backstage/app/filter.js b/apps/backstage/app/filter.js
new file mode 100644
index 0000000..1da6159
--- /dev/null
+++ b/apps/backstage/app/filter.js
@@ -0,0 +1,42 @@
+module.exports = {
+ AnyText: FilterAnyText,
+ ObjectType: FilterObjectType,
+};
+
+var m = require('mithril');
+
+function FilterAnyText() {
+ this.view = function(ctrl) {
+ return m('.input-group.input-group-sm', [
+ m('input.form-control[type="text"][placeholder="Search"]',
+ {oninput: m.withAttr('value', setFilter)}),
+ ]);
+ function setFilter(value) {
+ ctrl.currentFilter('any', 'ilike', '%'+value+'%')
+ }
+ };
+}
+
+function FilterObjectType(opts) {
+ this.view = function(ctrl) {
+ return [
+ m('.input-group.input-group-sm', [
+ m('.input-group-btn', [
+ m('button.btn.btn-default.dropdown-toggle[type="button"][data-toggle="dropdown"]', [
+ ctrl.currentFilter() ? ctrl.currentFilter()[2].replace(/^.*#/,'') : 'Type',
+ ' ',
+ m('span.caret'),
+ ]),
+ m('ul.dropdown-menu[role="menu"]', [
+ m('li', [
+ m('a[href="#"]', {onclick: ctrl.currentFilter.bind(ctrl, opts.attr, 'is_a', 'arvados#collection')}, 'Collection'),
+ ]),
+ m('li', [
+ m('a[href="#"]', {onclick: ctrl.currentFilter.bind(ctrl, opts.attr, 'is_a', 'arvados#pipelineInstance')}, 'Pipeline instance'),
+ ]),
+ ]),
+ ]),
+ ]),
+ ];
+ };
+}
diff --git a/apps/backstage/app/filterset.js b/apps/backstage/app/filterset.js
new file mode 100644
index 0000000..c1bc5e6
--- /dev/null
+++ b/apps/backstage/app/filterset.js
@@ -0,0 +1,26 @@
+module.exports = FilterSet;
+
+var m = require('mithril');
+
+function FilterSet(viewModules) {
+ var filterSet = {};
+ filterSet.vm = {};
+ filterSet.controller = function(callerCtrl) {
+ var ctrl = this;
+ ctrl.vm = filterSet.vm;
+ ctrl.vm.views = viewModules.map(function(modInfo) {
+ var view = (new modInfo[1](modInfo[2])).view;
+ var boundGettersetter = callerCtrl.currentFilter.bind(
+ callerCtrl, modInfo[0]);
+ return view.bind(view, {currentFilter: boundGettersetter});
+ });
+ };
+ filterSet.view = function(ctrl) {
+ return m('form.form-inline', [
+ ctrl.vm.views.map(function(view) {
+ return [view(), ' '];
+ }),
+ ]);
+ };
+ return filterSet;
+}
diff --git a/apps/backstage/app/infinitescroll.js b/apps/backstage/app/infinitescroll.js
new file mode 100644
index 0000000..2a7dc6a
--- /dev/null
+++ b/apps/backstage/app/infinitescroll.js
@@ -0,0 +1,71 @@
+// Call the content module's getMoreItems() action whenever the bottom
+// edge of the content view is [within pxThreshold of being] visible.
+//
+// If getMoreItems() returns a promise, bottom edge visibility will be
+// tested again when that promise is resolved. This should be used
+// whenever getMoreItems() adds any new items, to cover the case where
+// the bottom of the content view is still visible after a new page of
+// items is rendered.
+//
+// It is the responsibility of getMoreItems() to ignore subsequent
+// calls while it's busy retrieving or preparing additional content.
+module.exports = InfiniteScroll;
+
+var m = require('mithril')
+, jQuery = require('jquery');
+
+function InfiniteScroll(contentCtrl, contentView, opts) {
+ var scroller = {};
+ opts = opts || {};
+ scroller.controller = function() {
+ this.contentCtrl = contentCtrl;
+ this.getMoreItems = this.contentCtrl.getMoreItems.bind(this.contentCtrl);
+ this.pxThreshold = opts.pxThreshold || 0;
+ this.onunload = onunload.bind(this);
+ function onunload () {
+ var i=0;
+ InfiniteScroll.controllers().map(function(ctrl) {
+ if (ctrl === this) {
+ InfiniteScroll.elements().splice(i, 1);
+ InfiniteScroll.controllers().splice(i, 1);
+ } else {
+ i++;
+ }
+ }.bind(this));
+ };
+ };
+ scroller.view = function(ctrl) {
+ return m('.container', {config: function(el, isInit, ctx) {
+ return scroller.configEl(el, isInit, ctx, ctrl);
+ }}, [
+ contentView(ctrl.contentCtrl)
+ ]);
+ };
+ scroller.configEl = function(el, isInit, ctx, ctrl) {
+ if (isInit) return;
+ if (InfiniteScroll.elements().indexOf(el) < 0) {
+ InfiniteScroll.elements().push(el);
+ InfiniteScroll.controllers().push(ctrl);
+ }
+ };
+ return scroller;
+}
+InfiniteScroll.elements = m.prop([]);
+InfiniteScroll.controllers = m.prop([]);
+
+(function() {
+ function scrollHandler(event) {
+ InfiniteScroll.elements().map(function(el, i) {
+ var ctrl = InfiniteScroll.controllers()[i];
+ var pxBeforeEnd =
+ el.getBoundingClientRect().bottom -
+ document.documentElement.clientHeight;
+ var promised;
+ if (pxBeforeEnd > ctrl.pxThreshold)
+ return;
+ if ((promised = ctrl.getMoreItems()) && promised.then)
+ promised.then(scrollHandler);
+ });
+ }
+ jQuery(window).on('DOMContentLoaded load resize scroll', scrollHandler);
+})();
diff --git a/apps/backstage/app/util.js b/apps/backstage/app/util.js
new file mode 100644
index 0000000..f8b4d8e
--- /dev/null
+++ b/apps/backstage/app/util.js
@@ -0,0 +1,50 @@
+module.exports = {
+ choose: choose,
+ debounce: debounce,
+};
+
+// util.choose('a', {a: 'A', b: 'B'}) --> return 'A'
+// util.choose('a', {a: [console.log, 'foo']}) --> return console.log('foo')
+function choose(key, options) {
+ var option = options[key];
+ if (option instanceof Array && option[0] instanceof Function)
+ return option[0].apply(this, option.slice(1));
+ else
+ return option;
+}
+
+// util.debounce(250, key) --> Return a promise. If someone else
+// calls debounce with the same key, reject the promise. If nobody
+// else has done so after 250ms, resolve the promise.
+function debounce(ms, key) {
+ var newpending;
+ util.debounce.pending = util.debounce.pending || [];
+ util.debounce.pending.map(function(found) {
+ if (!newpending && found.key === key) {
+ // Promise already pending with this key. Reject the old
+ // one, reuse its slot for the new one.
+ window.clearTimeout(found.timer);
+ found.deferred.reject();
+ m.endComputation();
+ newpending = found;
+ }
+ });
+ if (!newpending) {
+ // No pending promise with this key.
+ newpending = {key: key}
+ util.debounce.pending.push(newpending);
+ }
+ newpending.deferred = m.deferred();
+ m.startComputation();
+ newpending.timer = window.setTimeout(function() {
+ // Success, no more bouncing. Remove from pending list.
+ util.debounce.pending.map(function(found, i) {
+ if (found === newpending) {
+ util.debounce.pending.splice(i, 1);
+ found.deferred.resolve();
+ m.endComputation();
+ }
+ });
+ }, ms);
+ return newpending.deferred.promise;
+}
diff --git a/apps/backstage/bower.json b/apps/backstage/bower.json
new file mode 100644
index 0000000..9e5913c
--- /dev/null
+++ b/apps/backstage/bower.json
@@ -0,0 +1,24 @@
+{
+ "name": "backstage",
+ "version": "0.0.0",
+ "license": "AGPLv3",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "bootstrap": "~3.3.1",
+ "mithril": "~0.1.27"
+ },
+ "devDependencies": {
+ "requirejs": "~2.1.15",
+ "chai": "~1.10.0",
+ "mocha": "~2.1.0",
+ "chai-jquery": "~2.0.0",
+ "sinon": "~1.12.2"
+ }
+}
diff --git a/apps/backstage/index.html b/apps/backstage/index.html
new file mode 100644
index 0000000..691d347
--- /dev/null
+++ b/apps/backstage/index.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>arvados backstage</title>
+ <link href="dist/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+ <link href="dist/bootstrap/css/bootstrap-theme.min.css" rel="stylesheet">
+ <link href="app/backstage.css" rel="stylesheet">
+ </head>
+ <body>
+ <script src="dist/app.js"></script>
+ </body>
+</html>
diff --git a/apps/backstage/node_modules/app b/apps/backstage/node_modules/app
new file mode 120000
index 0000000..5df94d9
--- /dev/null
+++ b/apps/backstage/node_modules/app
@@ -0,0 +1 @@
+../app
\ No newline at end of file
diff --git a/apps/backstage/node_modules/arvados/client.js b/apps/backstage/node_modules/arvados/client.js
new file mode 100644
index 0000000..380384d
--- /dev/null
+++ b/apps/backstage/node_modules/arvados/client.js
@@ -0,0 +1,298 @@
+// Data service backed by Arvados.
+//
+// c = new ArvadosConnection('xyzzy'); // connect to xyzzy.arvadosapi.com
+// c.token('asdfasdf'); // set token
+// c.state(); // 'loading'
+// c.ready.then(function() { c.state() }); // 'ready'
+//
+// // This part needs a better API:
+// nodelist = m.prop();
+// c.ready.then(function() { c.Node.list().then(nodelist) });
+//
+// // Better?
+// nodelist = m.deferred();
+// nodelist = c.api('nodes.list', {filters: []}, nodelist);
+// nodelist(); // undefined
+// nodelist.then(function() { nodelist(); }); // [{uuid:...},...]
+
+module.exports = ArvadosConnection;
+
+var m = require('mithril');
+
+ArvadosConnection.connections = {};
+ArvadosConnection.make = Make;
+
+function Make(connectionId, apiPrefix) {
+ var conns = ArvadosConnection.connections;
+ apiPrefix = apiPrefix || connectionId;
+ if (!conns[connectionId]) {
+ conns[connectionId] = new ArvadosConnection(apiPrefix);
+ }
+ return conns[connectionId];
+}
+
+function ArvadosConnection(apiPrefix) {
+ var connection = this;
+ var dd = m.prop();
+ connection.apiPrefix = m.prop(apiPrefix);
+ connection.discoveryDoc = dd;
+ connection.state = m.prop('loading');
+ connection.api = api;
+ connection.find = find;
+ connection.loginLink = loginLink;
+ connection.token = token;
+ connection.webSocket = m.prop({});
+
+ // Initialize
+
+ connection.ready = m.request({
+ background: true,
+ method: 'GET',
+ url: 'https://' + apiPrefix + '.arvadosapi.com/discovery/v1/apis/arvados/v1/rest'
+ });
+ connection.ready.
+ then(connection.discoveryDoc).
+ then(setupModelClasses).
+ then(setupWebSocket).
+ then(m.redraw,
+ function(err){connection.state('error: '+err); m.redraw();});
+
+ // Public methods
+
+ // URL that will initiate the login process, pass the new token to
+ // localStorage via login-callback, then return to the current
+ // route.
+ function loginLink() {
+ if (!dd()) return null;
+ return dd().rootUrl + 'login?return_to=' + encodeURIComponent(
+ (location.href.replace(/\?.*/,'') + '?/login-callback?apiPrefix=' + apiPrefix +
+ '&return_to=' + encodeURIComponent(m.route())));
+ }
+
+ // getter-setter, backed by localStorage. Currently supports only
+ // one connection per apiPrefix.
+ function token(newToken) {
+ var tokens;
+ try {
+ tokens = JSON.parse(window.localStorage.tokens);
+ } catch(e) {
+ tokens = {};
+ }
+ if (arguments.length === 0) {
+ return tokens[apiPrefix];
+ } else {
+ tokens[apiPrefix] = newToken;
+ window.localStorage.tokens = JSON.stringify(tokens);
+ return newToken;
+ }
+ }
+
+ // Wait for discovery doc if necessary, then perform API call and
+ // resolve the returned promise.
+ //
+ // modelClass: 'Collection', 'Node', etc.
+ // action: 'get', 'list', 'update', etc.
+ // params: {uuid:'foo',filters:[],...}
+ // deferred (optional): deferred object for response. If not
+ // supplied, a new one is created.
+ function api(modelClass, action, params, deferred) {
+ deferred = deferred || m.deferred();
+ connection.ready.then(function() {
+ connection[modelClass][action](params).
+ then(updateStore).
+ then(deferred.resolve, deferred.reject).
+ then(m.redraw);
+ }, deferred.reject);
+ return deferred.promise;
+ }
+
+ // Private instance variables
+
+ var store = {};
+ var uuidInfixClassName = {};
+
+ // Private methods
+
+ function ModelClass(resourceName) {
+ var resourceClass = function() {
+ var model = this;
+ };
+ resourceClass.resourceName = resourceName;
+ resourceClass.addAction = function(action, method) {
+ resourceClass[action] = function(params) {
+ var path, postdata = {};
+ params = params || {};
+ Object.keys(params).map(function(key) {
+ if (params[key] instanceof Object)
+ postdata[key] = JSON.stringify(params[key]);
+ else
+ postdata[key] = params[key];
+ });
+ path = method.path.replace(/{(.*?)}/, function(_, key) {
+ var val = postdata[key];
+ delete postdata[key];
+ return encodeURIComponent(val);
+ });
+ path = dd().rootUrl + dd().servicePath + path;
+ return request({
+ method: method.httpMethod,
+ url: path,
+ data: postdata,
+ });
+ };
+ };
+ resourceClass.find = function(uuid, refreshFlag) {
+ if (!refreshFlag && store[uuid]) return store[uuid];
+ else return api(resourceName, 'get', {uuid:uuid});
+ };
+ return resourceClass;
+ }
+
+ function find(uuid, refreshFlag) {
+ refreshFlag = refreshFlag || !store[uuid];
+ connection.ready.then(function() {
+ var infix = uuid.slice(6,11);
+ var className = uuidInfixClassName[infix];
+ var theClass = connection[className];
+ if (!theClass) {
+ throw new Error("No class for "+className+" for infix "+infix);
+ }
+ theClass.find(uuid, refreshFlag);
+ });
+ store[uuid] = store[uuid] || m.prop();
+ return store[uuid];
+ }
+
+ function request(args) {
+ args.config = function(xhr) {
+ xhr.setRequestHeader('Authorization', 'OAuth2 '+connection.token());
+ };
+ return m.request(args);
+ }
+
+ // Update local cache with data just received in API response.
+ function updateStore(response) {
+ var items;
+ if (response.items) {
+ // Return an array of getters, with extra properties
+ // (items_available, etc.) tacked on to the array.
+ items = response.items.map(updateStore);
+ Object.keys(response).map(function(key) {
+ if (key !== 'items') {
+ items[key] = response[key];
+ }
+ });
+ return items;
+ } else if (response.uuid) {
+ store[response.uuid] = store[response.uuid] || m.prop();
+ store[response.uuid](response);
+ store[response.uuid]()._cacheTime = new Date();
+ store[response.uuid]()._conn = connection;
+ return store[response.uuid];
+ } else {
+ return response;
+ }
+ }
+
+ function setupModelClasses(x) {
+ var schemas = connection.discoveryDoc().schemas;
+ Object.keys(schemas).map(function(modelClassName) {
+ if (modelClassName.search(/List$/) > -1) return;
+ var modelClass = new ModelClass(modelClassName);
+ modelClass.schema = schemas[modelClassName];
+ connection[modelClassName] = modelClass;
+ uuidInfixClassName[schemas[modelClassName].uuidPrefix] = modelClassName;
+ });
+ var resources = connection.discoveryDoc().resources;
+ Object.keys(resources).map(function(ctrl) {
+ var modelClassName;
+ var methods = resources[ctrl].methods;
+ try {
+ modelClassName = resources[ctrl].methods.get.response.$ref;
+ } catch(e) {
+ console.log("Hm, could not handle resource '"+ctrl+"'");
+ return;
+ }
+ if (!connection[modelClassName]) {
+ console.log("Hm, no schema for response type '"+ctrl+"'");
+ return;
+ }
+ Object.keys(methods).map(function(action) {
+ connection[modelClassName].addAction(action, methods[action]);
+ });
+ });
+ connection.state('ready');
+ }
+
+ function setupWebSocket() {
+ var ws;
+ if (!connection.token()) {
+ // No sense trying to connect without a valid token.
+ return connection.webSocket({});
+ }
+ ws = new WebSocket(
+ dd().websocketUrl + '?api_token=' + connection.token());
+ ws.startedAt = new Date();
+ ws.sendJson = function(object) {
+ ws.send(JSON.stringify(object));
+ };
+ ws.onopen = function(event) {
+ // TODO: subscribe to logs about uuids in
+ // connection.store, not everything.
+ ws.sendJson({method:'subscribe'});
+ };
+ ws.onreadystatechange = m.redraw.bind(m, false);
+ ws.onmessage = function(event) {
+ var message = JSON.parse(event.data);
+ var newAttrs;
+ var objectProp;
+ if (typeof message.object_uuid === 'string' &&
+ message.event_type === 'update' &&
+ message.object_uuid.slice(0,5) === apiPrefix &&
+ (objectProp = store[message.object_uuid])) {
+ newAttrs = message.properties.new_attributes;
+ if (objectProp()) {
+ // A local copy exists. Update whatever attributes
+ // we see in the message.
+ Object.keys(newAttrs).map(function(key) {
+ objectProp()[key] = newAttrs[key];
+ });
+ } else {
+ // We have a getter-setter ready for this object,
+ // but it has no content yet. TODO: make the
+ // server send a full API response, not just the
+ // database columns, with these update messages.
+ objectProp(newAttrs);
+ }
+ objectProp()._cacheTime = new Date();
+ m.redraw();
+ }
+ };
+ ws.onclose = function(event) {
+ if (new Date() - ws.startedAt < 60000) {
+ // If the last connection lasted less than 60 seconds,
+ // there's probably something wrong -- it's not just
+ // the expected occasional server reset or network
+ // interruption -- so we should make sure to use a
+ // pessimistic retry delay of at least 60 seconds, and
+ // use ever-increasing delays until the connection
+ // starts staying alive for more than a minute at a
+ // time.
+ setupWebSocket.backoff = Math.min(
+ (setupWebSocket.backoff || 30), 30) * 2 + 1;
+ }
+ else {
+ // The last connection lasted more than a
+ // minute. Let's assume this is just a brief
+ // interruption and things are going well most of the
+ // time: delay 5 seconds, then try again.
+ setupWebSocket.backoff = 5;
+ }
+ console.log("Websocket closed at " + new Date() +
+ " with code=" + event.code +
+ ", retry in "+setupWebSocket.backoff+"s");
+ window.setTimeout(setupWebSocket, setupWebSocket.backoff*1000);
+ };
+ return connection.webSocket(ws);
+ }
+}
diff --git a/apps/backstage/node_modules/test b/apps/backstage/node_modules/test
new file mode 120000
index 0000000..419df4f
--- /dev/null
+++ b/apps/backstage/node_modules/test
@@ -0,0 +1 @@
+../test
\ No newline at end of file
diff --git a/apps/backstage/test.html b/apps/backstage/test.html
new file mode 100644
index 0000000..253d8a4
--- /dev/null
+++ b/apps/backstage/test.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>arvados backstage tests</title>
+ <link href="node_modules/mocha/mocha.css" rel="stylesheet">
+ </head>
+ <body>
+ <div id="mocha"></div>
+ <script src="node_modules/mocha/mocha.js"></script>
+ <script src="dist/test.js"></script>
+ <script>
+ var global = (function() { return this })();
+ if (global.mochaPhantomJS) {
+ global.mochaPhantomJS.run();
+ }
+ else {
+ mocha.run();
+ }
+ </script>
+ </body>
+</html>
diff --git a/apps/backstage/test/functional/dashboard.js b/apps/backstage/test/functional/dashboard.js
new file mode 100644
index 0000000..95418c5
--- /dev/null
+++ b/apps/backstage/test/functional/dashboard.js
@@ -0,0 +1,11 @@
+require(['chai', 'test/webdriver-client'], function(chai, c) {
+ var assert = chai.assert;
+ describe('Dashboard page', function() {
+ before(function() {
+ c.init().url('http://localhost:5555');
+ });
+ it('has a nav', function() {
+ assert(c.isVisible('nav'));
+ });
+ });
+});
diff --git a/apps/backstage/test/runner.js b/apps/backstage/test/runner.js
new file mode 100644
index 0000000..18270c4
--- /dev/null
+++ b/apps/backstage/test/runner.js
@@ -0,0 +1,9 @@
+var global = (function() { return this })();
+
+global.$ = global.jQuery = require('jquery');
+chaiJquery = require('chai-jquery');
+require('chai').use(chaiJquery);
+mocha.setup({ui: 'tdd'});
+
+require('test/unit/filter.js');
+require('test/unit/filterset.js');
diff --git a/apps/backstage/test/unit/filter.js b/apps/backstage/test/unit/filter.js
new file mode 100644
index 0000000..dc1b6be
--- /dev/null
+++ b/apps/backstage/test/unit/filter.js
@@ -0,0 +1,18 @@
+var mq = require('mithril-query')
+, Filter = require('app/filter')
+, sinon = require('sinon')
+, s = sinon;
+
+suite('Filter', function() {
+ test("changing input fires currentFilter", function() {
+ var tested = new Filter.AnyText();
+ var cfSpy = sinon.spy();
+ var ctrl = {currentFilter: cfSpy};
+ var v = tested.view(ctrl);
+ s.assert.notCalled(cfSpy);
+ mq(v).setValue('input', 'qux');
+ s.assert.calledOnce(cfSpy);
+ s.assert.calledOn(cfSpy, ctrl);
+ s.assert.calledWith(cfSpy, 'any', 'ilike', '%qux%');
+ });
+});
diff --git a/apps/backstage/test/unit/filterset.js b/apps/backstage/test/unit/filterset.js
new file mode 100644
index 0000000..c90a082
--- /dev/null
+++ b/apps/backstage/test/unit/filterset.js
@@ -0,0 +1,39 @@
+var FilterSet = require('app/filterset')
+, chai = require('chai')
+, mq = require('mithril-query')
+, sinon = require('sinon')
+, s = sinon
+, c = chai;
+
+suite('FilterSet', function() {
+ test("viewModules' views bind parentCtrl's currentFilter", function() {
+ FilterStub = function() {};
+ FilterStub.prototype.view = function() {};
+ var stubFilterName = 'foo';
+ var fakeFilterValue = 'bar';
+ var childViewSpy = sinon.spy(FilterStub.prototype, 'view');
+ var stubParentCtrl = {currentFilter: sinon.spy()};
+
+ // Instantiate a component, and its controller.
+ var tested = new FilterSet([[stubFilterName, FilterStub]]);
+ var testedCtrl = new tested.controller(stubParentCtrl);
+
+ // The FilterSet's view should invoke the child's view,
+ // and pass a controller to it.
+ s.assert.notCalled(childViewSpy);
+ var viewOut = tested.view(testedCtrl);
+ s.assert.calledOnce(childViewSpy);
+ c.assert.lengthOf(childViewSpy.getCall(0).args, 1);
+
+ // The controller passed to the child view should have a
+ // currentFilter method which invokes
+ // stubParentCtrl.currentFilter() with the filter name
+ // prepended to its argument list.
+ var childViewCtrl = childViewSpy.getCall(0).args[0];
+ s.assert.notCalled(stubParentCtrl.currentFilter);
+ childViewCtrl.currentFilter(fakeFilterValue);
+ s.assert.calledOnce(stubParentCtrl.currentFilter);
+ s.assert.calledOn(stubParentCtrl.currentFilter, stubParentCtrl);
+ s.assert.calledWith(stubParentCtrl.currentFilter, stubFilterName, fakeFilterValue);
+ });
+});
diff --git a/apps/backstage/test/webdriver-client.js b/apps/backstage/test/webdriver-client.js
new file mode 100644
index 0000000..dc40522
--- /dev/null
+++ b/apps/backstage/test/webdriver-client.js
@@ -0,0 +1,13 @@
+define(['webdriverjs'], function(webdriverjs) {
+ var client = webdriverjs.remote({
+ desiredCapabilities: {
+ // http://code.google.com/p/selenium/wiki/DesiredCapabilities
+ browserName: 'phantomjs'
+ },
+ // webdriverjs has a lot of output which is generally useless
+ // However, if anything goes wrong, remove this to see more details
+ // logLevel: 'silent'
+ });
+ client.init();
+ return client;
+});
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list