4831: Add tb05z
[arvados.git] / apps / backstage / app / component.arv-list.js
1 module.exports = ArvListComponent;
2
3 var m = require('mithril')
4 , ArvadosClient = require('arvados/client')
5 , BaseController = require('app/base-ctrl')
6 , Filter = require('app/filter')
7 , FilterSet = require('app/filterset')
8 , util = require('app/util');
9
10 function ArvListComponent(connection, arvModelName, contentModule) {
11     this.controller = Controller;
12     this.view = View;
13
14     Controller.prototype = new BaseController();
15     function Controller() {
16         this.vm = new ViewModel();
17         this.vm.init();
18         this.vm.filterSetCtrl = new this.vm.filterSet.controller(this);
19     }
20     Controller.prototype.getMoreItems =
21         function getMoreItems() {
22             return this.vm.getMoreItems.apply(this.vm, arguments);
23         }
24     Controller.prototype.eof =
25         function eof() {
26             return this.vm.eof();
27         }
28     Controller.prototype.currentFilter =
29         function currentFilter(key, attr, operator, operand) {
30             if (arguments.length > 1) {
31                 this.vm.filters[key] = [attr, operator, operand];
32                 util.debounce(500, this.vm.resetContent).
33                     then(this.vm.resetContent);
34             }
35             return this.vm.filters[key];
36         }
37
38     function ViewModel() {
39         var vm = this;
40         vm.init = function() {
41             vm.arvModelName = arvModelName || m.route.param('modelName');
42             vm.connection = connection || ArvadosClient.make(m.route.param('connection'));
43             vm.filters = {};
44             vm.filterSet = new FilterSet(
45                 [['any', Filter.AnyText],
46                  ['type', Filter.ObjectType, {attr:'uuid'}]]);
47             vm.inflight = null;
48             vm.listLimit = 30;
49             vm.listOrders = ['created_at desc'];
50             vm.resetContent();
51         };
52         vm.resetContent = function() {
53             if (vm.inflight) {
54                 // Forget about current/stale request. TODO: abort the xhr.
55                 vm.inflight.reject();
56                 vm.inflight = null;
57             }
58             vm.eof = m.prop(false);
59             vm.items = m.prop([]);
60             vm.itemViews = m.prop([]);
61             vm.beforeRender = function() {
62                 // On first render, trigger a scroll event to make the
63                 // first page of content appear. The scroll handler can
64                 // ignore this if (for example) the content view is
65                 // invisible now.
66                 vm.beforeRender = function() {};
67                 window.setTimeout(function() {
68                     window.dispatchEvent(new Event('scroll'));
69                 }, 1);
70             };
71         };
72         vm.apiFilters = function() {
73             var filters = [];
74             Object.keys(vm.filters).map(function(key) {
75                 if (vm.filters[key])
76                     filters.push(vm.filters[key]);
77             });
78             return filters;
79         };
80         vm.makeItemViews = function() {
81             vm.itemViews(vm.items().map(function(item) {
82                 return contentModule.view.bind(
83                     contentModule.view,
84                     new contentModule.controller({item:item}));
85             }));
86         };
87         vm.getMoreItems = function() {
88             var inflight;
89             if (vm.inflight || vm.eof())
90                 return false;
91             inflight = m.deferred();
92             vm.connection.api(vm.arvModelName, 'list', {
93                 filters: vm.apiFilters(),
94                 limit: vm.listLimit,
95                 offset: vm.items().length,
96                 order: vm.listOrders
97             }).then(function(newItems) {
98                 if (inflight !== vm.inflight) {
99                     // This request has already been superseded by a
100                     // new one. Ignore.
101                     return;
102                 }
103                 vm.eof(newItems.length === 0 ||
104                        (typeof newItems.offset === 'number' &&
105                         newItems.items_available === newItems.offset + newItems.length));
106                 vm.items(vm.items().concat(newItems));
107             }, vm.eof).then(vm.makeItemViews).then(function() {
108                 // Give the new items a chance to render before
109                 // resolving the promise. This makes it possible for
110                 // the resolve callback to measure the DOM after the
111                 // new elements have been added (notably, in order to
112                 // keep fetching pages until the scroll threshold is
113                 // satisfied).
114                 window.setTimeout(inflight.resolve, 50);
115                 vm.inflight = null;
116             });
117             return (vm.inflight = inflight).promise;
118         };
119         return vm;
120     }
121
122     function View(ctrl) {
123         ctrl.vm.beforeRender();
124         return [
125             ctrl.vm.filterSet ? ctrl.vm.filterSet.view(ctrl.vm.filterSetCtrl) : '',
126             ctrl.vm.itemViews().map(function(v) {
127                 return v();
128             }),
129             ctrl.vm.eof() ? '' : m('.row', {style: 'background: #ffffdd'}, [
130                 m('.col-sm-12', {style: 'text-align: center'}, ['...loading...'])
131             ]),
132         ];
133     }
134 }