12033: Fix auto scroll after search; acknowledge end of results.
[arvados.git] / apps / workbench / app / assets / javascripts / components / collections.js
index ddba2e118373d671462f5e2cbd1451f7ce0f0bee..a06e2bba7efc88e0de382c6d45ceee8f9a59bf5d 100644 (file)
@@ -3,21 +3,72 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 window.components = window.components || {}
-window.components.collection_table_narrow = {
+window.components.collection_table = {
+    maybeLoadMore: function(dom) {
+        var loader = this.loader
+        if (loader.done || !loader.loadMore)
+            // Can't load more content anyway: no point in
+            // checking anything else.
+            return
+        var contentRect = dom.getBoundingClientRect()
+        var scroller = window // TODO: use dom's nearest ancestor with scrollbars
+        if (contentRect.bottom < 2 * scroller.innerHeight) {
+            // We have less than 1 page worth of content available
+            // below the visible area. Load more.
+            loader.loadMore()
+            // Indicate loading is in progress.
+            window.requestAnimationFrame(m.redraw)
+        }
+    },
+    oncreate: function(vnode) {
+        vnode.state.maybeLoadMore = vnode.state.maybeLoadMore.bind(vnode.state, vnode.dom)
+        window.addEventListener('scroll', vnode.state.maybeLoadMore)
+        window.addEventListener('resize', vnode.state.maybeLoadMore)
+        vnode.state.timer = window.setInterval(vnode.state.maybeLoadMore, 200)
+        vnode.state.onupdate(vnode)
+    },
+    onupdate: function(vnode) {
+        vnode.state.loader = vnode.attrs.loader
+    },
+    onremove: function(vnode) {
+        window.clearInterval(vnode.state.timer)
+        window.removeEventListener('scroll', vnode.state.maybeLoadMore)
+        window.removeEventListener('resize', vnode.state.maybeLoadMore)
+    },
     view: function(vnode) {
         return m('table.table.table-condensed', [
-            m('thead', m('tr', m('th', vnode.attrs.key))),
+            m('thead', m('tr', [
+                m('th'),
+                m('th', 'uuid'),
+                m('th', 'name'),
+                m('th', 'last modified'),
+            ])),
             m('tbody', [
-                vnode.attrs.items().map(function(item) {
+                vnode.attrs.loader.displayable.map(function(item) {
                     return m('tr', [
-                        m('td', [
-                            m('a', {href: '/collections/'+item.uuid}, item.name || '(unnamed)'),
-                            m('br'),
-                            item.modified_at,
-                        ]),
+                        m('td', m('a.btn.btn-xs.btn-default', {href: item.session.baseURL.replace('://', '://workbench.')+'/collections/'+item.uuid}, 'Show')),
+                        m('td.arvados-uuid', item.uuid),
+                        m('td', item.name || '(unnamed)'),
+                        m('td', m(window.components.datetime, {parse: item.modified_at})),
                     ])
                 }),
             ]),
+            m('tfoot', m('tr', [
+                vnode.attrs.loader.done ? null : m('th[colspan=4]', m('button.btn.btn-xs', {
+                    className: vnode.attrs.loader.loadMore ? 'btn-primary' : 'btn-default',
+                    style: {
+                        display: 'block',
+                        width: '12em',
+                        marginLeft: 'auto',
+                        marginRight: 'auto',
+                    },
+                    disabled: !vnode.attrs.loader.loadMore,
+                    onclick: function() {
+                        vnode.attrs.loader.loadMore()
+                        return false
+                    },
+                }, vnode.attrs.loader.loadMore ? 'Load more' : '(loading)')),
+            ])),
         ])
     },
 }
@@ -27,31 +78,24 @@ window.components.collection_search = {
         vnode.state.sessionDB = new window.models.SessionDB()
         vnode.state.searchEntered = m.stream('')
         vnode.state.searchStart = m.stream('')
-        vnode.state.items = {}
         vnode.state.searchStart.map(function(q) {
-            var sessions = vnode.state.sessionDB.loadAll()
-            var cookie = (new Date()).getTime()
-            vnode.state.cookie = cookie
-            Object.keys(sessions).map(function(key) {
-                if (!vnode.state.items[key])
-                    vnode.state.items[key] = m.stream([])
-                vnode.state.items[key].dirty = true
-                vnode.state.sessionDB.request(sessions[key], 'arvados/v1/collections', {
-                    data: {
-                        filters: JSON.stringify(!q ? [] : [['any', '@@', q+':*']]),
-                    },
-                }).then(function(resp) {
-                    if (cookie !== vnode.state.cookie)
-                        // a newer query is in progress; ignore this result.
-                        return
-                    vnode.state.items[key](resp.items)
-                    vnode.state.items[key].dirty = false
-                })
+            vnode.state.loader = new window.models.MultisiteLoader({
+                loadFunc: function(session, filters) {
+                    if (q)
+                        filters.push(['any', '@@', q+':*'])
+                    return vnode.state.sessionDB.request(session, 'arvados/v1/collections', {
+                        data: {
+                            filters: JSON.stringify(filters),
+                            count: 'none',
+                        },
+                    })
+                },
+                sessionDB: vnode.state.sessionDB,
             })
         })
     },
     view: function(vnode) {
-        var items = vnode.state.items
+        var sessions = vnode.state.sessionDB.loadAll()
         return m('form', {
             onsubmit: function() {
                 vnode.state.searchStart(vnode.state.searchEntered())
@@ -62,7 +106,7 @@ window.components.collection_search = {
                 m('.col-md-6', [
                     m('.input-group', [
                         m('input#search.form-control[placeholder=Search]', {
-                            oninput: m.withAttr('value', debounce(200, vnode.state.searchEntered)),
+                            oninput: m.withAttr('value', vnode.state.searchEntered),
                         }),
                         m('.input-group-btn', [
                             m('input.btn.btn-primary[type=submit][value="Search"]'),
@@ -71,44 +115,20 @@ window.components.collection_search = {
                 ]),
                 m('.col-md-6', [
                     'Searching sites: ',
-                    Object.keys(items).length == 0
+                    Object.keys(sessions).length == 0
                         ? m('span.label.label-xs.label-danger', 'none')
-                        : Object.keys(items).sort().map(function(key) {
-                            return [m('span.label.label-xs.label-info', key), ' ']
+                        : Object.keys(sessions).sort().map(function(key) {
+                            return [m('span.label.label-xs', {
+                                className: vnode.state.loader.pagers[key].items() ? 'label-info' : 'label-default',
+                            }, key), ' ']
                         }),
                     ' ',
                     m('a[href="/sessions"]', 'Add/remove sites'),
                 ]),
             ]),
-            m('.row', Object.keys(items).sort().map(function(key) {
-                return m('.col-md-3', {key: key, style: {
-                    opacity: items[key].dirty ? 0.5 : 1,
-                }}, [
-                    m(window.components.collection_table_narrow, {
-                        key: key,
-                        items: items[key],
-                    }),
-                ])
-            })),
+            m(window.components.collection_table, {
+                loader: vnode.state.loader,
+            }),
         ])
     },
 }
-
-function debounce(t, f) {
-    // Return a new function that waits until t milliseconds have
-    // passed since it was last called, then calls f with its most
-    // recent arguments.
-    var this_was = this
-    var pending
-    return function() {
-        var args = arguments
-        if (pending) {
-            console.log("debounce!")
-            window.clearTimeout(pending)
-        }
-        pending = window.setTimeout(function() {
-            pending = undefined
-            f.apply(this_was, args)
-        }, t)
-    }
-}