12033: Add "load more" button.
[arvados.git] / apps / workbench / app / assets / javascripts / components / collections.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 window.components = window.components || {}
6 window.components.collection_table_narrow = {
7     view: function(vnode) {
8         return m('table.table.table-condensed', [
9             m('thead', m('tr', [
10                 m('th'),
11                 m('th', 'uuid'),
12                 m('th', 'name'),
13                 m('th', 'last modified'),
14             ])),
15             m('tbody', [
16                 vnode.attrs.results.displayable.map(function(item) {
17                     return m('tr', [
18                         m('td', m('a.btn.btn-xs.btn-default', {href: item.session.baseURL.replace('://', '://workbench.')+'/collections/'+item.uuid}, 'Show')),
19                         m('td', item.uuid),
20                         m('td', item.name || '(unnamed)'),
21                         m('td', m(window.components.datetime, {parse: item.modified_at})),
22                     ])
23                 }),
24             ]),
25             m('tfoot', m('tr', [
26                 m('th[colspan=4]', m('button.btn.btn-xs', {
27                     className: vnode.attrs.results.loadMore ? 'btn-primary' : 'btn-default',
28                     style: {
29                         display: 'block',
30                         width: '12em',
31                         marginLeft: 'auto',
32                         marginRight: 'auto',
33                     },
34                     disabled: !vnode.attrs.results.loadMore,
35                     onclick: function() {
36                         vnode.attrs.results.loadMore()
37                         return false
38                     },
39                 }, vnode.attrs.results.loadMore ? 'Load more' : '(loading)')),
40             ])),
41         ])
42     },
43 }
44
45 function Pager(loadFunc) {
46     // loadFunc(filters) returns a promise for a page of results.
47     var pager = this
48     Object.assign(pager, {
49         done: false,
50         items: m.stream(),
51         thresholdItem: null,
52         loadNextPage: function() {
53             // Get the next page, if there are any more items to get.
54             if (pager.done)
55                 return
56             var filters = pager.thresholdItem ? [
57                 ["modified_at", "<=", pager.thresholdItem.modified_at],
58                 ["uuid", "!=", pager.thresholdItem.uuid],
59             ] : []
60             loadFunc(filters).then(function(resp) {
61                 var items = pager.items() || []
62                 Array.prototype.push.apply(items, resp.items)
63                 if (resp.items.length == 0)
64                     pager.done = true
65                 else
66                     pager.thresholdItem = resp.items[resp.items.length-1]
67                 pager.items(items)
68             })
69         },
70     })
71 }
72
73 window.components.collection_search = {
74     oninit: function(vnode) {
75         vnode.state.sessionDB = new window.models.SessionDB()
76         vnode.state.searchEntered = m.stream('')
77         vnode.state.searchStart = m.stream('')
78         vnode.state.searchStart.map(function(q) {
79             var sessions = vnode.state.sessionDB.loadAll()
80             var cookie = (new Date()).getTime()
81             // Each time searchStart() is called we replace the
82             // vnode.state.results stream with a new one, and use
83             // the local variable to update results in callbacks. This
84             // avoids crosstalk between AJAX calls from consecutive
85             // searches.
86             var results = {
87                 // Sorted items ready to display, merged from all
88                 // pagers.
89                 displayable: [],
90                 pagers: {},
91                 loadMore: false,
92                 // Number of undisplayed items to keep on hand for
93                 // each result set. When hitting "load more", if a
94                 // result set already has this many additional results
95                 // available, we don't bother fetching a new
96                 // page. This is the _minimum_ number of rows that
97                 // will be added to results.displayable in each "load
98                 // more" event (except for the case where all items
99                 // are displayed).
100                 lowWaterMark: 23,
101             }
102             vnode.state.results = results
103             m.stream.merge(Object.keys(sessions).map(function(key) {
104                 var pager = new Pager(function(filters) {
105                     if (q)
106                         filters.push(['any', '@@', q+':*'])
107                     return vnode.state.sessionDB.request(sessions[key], 'arvados/v1/collections', {
108                         data: {
109                             filters: JSON.stringify(filters),
110                             count: 'none',
111                         },
112                     })
113                 })
114                 results.pagers[key] = pager
115                 pager.loadNextPage()
116                 // Resolve the stream with the session key when the
117                 // results arrive.
118                 return pager.items.map(function() { return key })
119             })).map(function(keys) {
120                 // Top (most recent) of {bottom (oldest) entry of any
121                 // pager that still has more pages left to fetch}
122                 var cutoff
123                 keys.forEach(function(key) {
124                     var pager = results.pagers[key]
125                     var items = pager.items()
126                     if (items.length == 0 || pager.done)
127                         return
128                     var last = items[items.length-1].modified_at
129                     if (!cutoff || cutoff < last)
130                         cutoff = last
131                 })
132                 var combined = []
133                 keys.forEach(function(key) {
134                     var pager = results.pagers[key]
135                     pager.itemsDisplayed = 0
136                     pager.items().every(function(item) {
137                         if (cutoff && item.modified_at < cutoff)
138                             // Some other pagers haven't caught up to
139                             // this point, so don't display this item
140                             // or anything after it.
141                             return false
142                         item.session = sessions[key]
143                         combined.push(item)
144                         pager.itemsDisplayed++
145                         return true // continue
146                     })
147                 })
148                 results.displayable = combined.sort(function(a, b) {
149                     return a.modified_at < b.modified_at ? 1 : -1
150                 })
151                 // Make a new loadMore function that hits the pagers
152                 // (if necessary according to lowWaterMark)... or set
153                 // results.loadMore to false if there is nothing left
154                 // to fetch.
155                 var loadable = []
156                 Object.keys(results.pagers).map(function(key) {
157                     if (!results.pagers[key].done)
158                         loadable.push(results.pagers[key])
159                 })
160                 if (loadable.length == 0)
161                     results.loadMore = false
162                 else
163                     results.loadMore = function() {
164                         results.loadMore = false
165                         loadable.map(function(pager) {
166                             if (pager.items().length - pager.itemsDisplayed < results.lowWaterMark)
167                                 pager.loadNextPage()
168                         })
169                     }
170             })
171         })
172     },
173     view: function(vnode) {
174         var sessions = vnode.state.sessionDB.loadAll()
175         return m('form', {
176             onsubmit: function() {
177                 vnode.state.searchStart(vnode.state.searchEntered())
178                 return false
179             },
180         }, [
181             m('.row', [
182                 m('.col-md-6', [
183                     m('.input-group', [
184                         m('input#search.form-control[placeholder=Search]', {
185                             oninput: m.withAttr('value', vnode.state.searchEntered),
186                         }),
187                         m('.input-group-btn', [
188                             m('input.btn.btn-primary[type=submit][value="Search"]'),
189                         ]),
190                     ]),
191                 ]),
192                 m('.col-md-6', [
193                     'Searching sites: ',
194                     Object.keys(sessions).length == 0
195                         ? m('span.label.label-xs.label-danger', 'none')
196                         : Object.keys(sessions).sort().map(function(key) {
197                             return [m('span.label.label-xs', {
198                                 className: vnode.state.results.pagers[key].items() ? 'label-info' : 'label-default',
199                             }, key), ' ']
200                         }),
201                     ' ',
202                     m('a[href="/sessions"]', 'Add/remove sites'),
203                 ]),
204             ]),
205             m(window.components.collection_table_narrow, {
206                 results: vnode.state.results,
207             }),
208         ])
209     },
210 }