12515: Use workbenchUrl advertised by API server discovery doc.
[arvados.git] / apps / workbench / app / assets / javascripts / components / collections.js
index 9334bcc3fc49ebb0a03849127897a699dba5b04b..591bf38aa74b162e353c9705da1ecbcbba2d8cde 100644 (file)
@@ -2,9 +2,41 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-window.components = window.components || {}
-window.components.collection_table_narrow = {
+window.CollectionsTable = {
+    maybeLoadMore: function(dom) {
+        var loader = this.loader
+        if (loader.state != loader.READY)
+            // Can't start getting more items 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.loader = vnode.attrs.loader
+        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) {
+        var loader = vnode.attrs.loader
         return m('table.table.table-condensed', [
             m('thead', m('tr', [
                 m('th'),
@@ -13,160 +45,80 @@ window.components.collection_table_narrow = {
                 m('th', 'last modified'),
             ])),
             m('tbody', [
-                vnode.attrs.results.displayable.map(function(item) {
+                loader.items().map(function(item) {
                     return m('tr', [
-                        m('td', m('a.btn.btn-xs.btn-default', {href: item.session.baseURL.replace('://', '://workbench.')+'/collections/'+item.uuid}, 'Show')),
-                        m('td', item.uuid),
+                        m('td', [
+                            item.workbenchBaseURL() &&
+                                m('a.btn.btn-xs.btn-default', {
+                                    href: item.workbenchBaseURL()+'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('td', m(LocalizedDateTime, {parse: item.modified_at})),
                     ])
                 }),
             ]),
-            m('tfoot', m('tr', [
+            loader.state == loader.DONE ? null : m('tfoot', m('tr', [
                 m('th[colspan=4]', m('button.btn.btn-xs', {
-                    className: vnode.attrs.results.loadMore ? 'btn-primary' : 'btn-default',
+                    className: loader.state == loader.LOADING ? 'btn-default' : 'btn-primary',
                     style: {
                         display: 'block',
                         width: '12em',
                         marginLeft: 'auto',
                         marginRight: 'auto',
                     },
-                    disabled: !vnode.attrs.results.loadMore,
+                    disabled: loader.state == loader.LOADING,
                     onclick: function() {
-                        vnode.attrs.results.loadMore()
+                        loader.loadMore()
                         return false
                     },
-                }, vnode.attrs.results.loadMore ? 'Load more' : '(loading)')),
+                }, loader.state == loader.LOADING ? '(loading)' : 'Load more')),
             ])),
         ])
     },
 }
 
-function Pager(loadFunc) {
-    // loadFunc(filters) returns a promise for a page of results.
-    var pager = this
-    Object.assign(pager, {
-        done: false,
-        items: m.stream(),
-        thresholdItem: null,
-        loadNextPage: function() {
-            // Get the next page, if there are any more items to get.
-            if (pager.done)
-                return
-            var filters = pager.thresholdItem ? [
-                ["modified_at", "<=", pager.thresholdItem.modified_at],
-                ["uuid", "!=", pager.thresholdItem.uuid],
-            ] : []
-            loadFunc(filters).then(function(resp) {
-                var items = pager.items() || []
-                Array.prototype.push.apply(items, resp.items)
-                if (resp.items.length == 0)
-                    pager.done = true
-                else
-                    pager.thresholdItem = resp.items[resp.items.length-1]
-                pager.items(items)
-            })
-        },
-    })
-}
-
-window.components.collection_search = {
+window.CollectionsSearch = {
     oninit: function(vnode) {
-        vnode.state.sessionDB = new window.models.SessionDB()
-        vnode.state.searchEntered = m.stream('')
-        vnode.state.searchStart = m.stream('')
-        vnode.state.searchStart.map(function(q) {
-            var sessions = vnode.state.sessionDB.loadAll()
-            var cookie = (new Date()).getTime()
-            // Each time searchStart() is called we replace the
-            // vnode.state.results stream with a new one, and use
-            // the local variable to update results in callbacks. This
-            // avoids crosstalk between AJAX calls from consecutive
-            // searches.
-            var results = {
-                // Sorted items ready to display, merged from all
-                // pagers.
-                displayable: [],
-                pagers: {},
-                loadMore: false,
-                // Number of undisplayed items to keep on hand for
-                // each result set. When hitting "load more", if a
-                // result set already has this many additional results
-                // available, we don't bother fetching a new
-                // page. This is the _minimum_ number of rows that
-                // will be added to results.displayable in each "load
-                // more" event (except for the case where all items
-                // are displayed).
-                lowWaterMark: 23,
-            }
-            vnode.state.results = results
-            m.stream.merge(Object.keys(sessions).map(function(key) {
-                var pager = new Pager(function(filters) {
-                    if (q)
-                        filters.push(['any', '@@', q+':*'])
-                    return vnode.state.sessionDB.request(sessions[key], 'arvados/v1/collections', {
-                        data: {
-                            filters: JSON.stringify(filters),
-                            count: 'none',
+        vnode.state.sessionDB = new SessionDB()
+        vnode.state.searchEntered = m.stream()
+        vnode.state.searchActive = m.stream()
+        // When searchActive changes (e.g., when restoring state
+        // after navigation), update the text field too.
+        vnode.state.searchActive.map(vnode.state.searchEntered)
+        // When searchActive changes, create a new loader that filters
+        // with the given search term.
+        vnode.state.searchActive.map(function(q) {
+            var sessions = vnode.state.sessionDB.loadActive()
+            vnode.state.loader = new MergingLoader({
+                children: Object.keys(sessions).map(function(key) {
+                    var session = sessions[key]
+                    var workbenchBaseURL = function() {
+                        return vnode.state.sessionDB.workbenchBaseURL(session)
+                    }
+                    return new MultipageLoader({
+                        sessionKey: key,
+                        loadFunc: function(filters) {
+                            var tsquery = to_tsquery(q)
+                            if (tsquery) {
+                                filters = filters.slice(0)
+                                filters.push(['any', '@@', tsquery])
+                            }
+                            return vnode.state.sessionDB.request(session, 'arvados/v1/collections', {
+                                data: {
+                                    filters: JSON.stringify(filters),
+                                    count: 'none',
+                                },
+                            }).then(function(resp) {
+                                resp.items.map(function(item) {
+                                    item.workbenchBaseURL = workbenchBaseURL
+                                })
+                                return resp
+                            })
                         },
                     })
                 })
-                results.pagers[key] = pager
-                pager.loadNextPage()
-                // Resolve the stream with the session key when the
-                // results arrive.
-                return pager.items.map(function() { return key })
-            })).map(function(keys) {
-                // Top (most recent) of {bottom (oldest) entry of any
-                // pager that still has more pages left to fetch}
-                var cutoff
-                keys.forEach(function(key) {
-                    var pager = results.pagers[key]
-                    var items = pager.items()
-                    if (items.length == 0 || pager.done)
-                        return
-                    var last = items[items.length-1].modified_at
-                    if (!cutoff || cutoff < last)
-                        cutoff = last
-                })
-                var combined = []
-                keys.forEach(function(key) {
-                    var pager = results.pagers[key]
-                    pager.itemsDisplayed = 0
-                    pager.items().every(function(item) {
-                        if (cutoff && item.modified_at < cutoff)
-                            // Some other pagers haven't caught up to
-                            // this point, so don't display this item
-                            // or anything after it.
-                            return false
-                        item.session = sessions[key]
-                        combined.push(item)
-                        pager.itemsDisplayed++
-                        return true // continue
-                    })
-                })
-                results.displayable = combined.sort(function(a, b) {
-                    return a.modified_at < b.modified_at ? 1 : -1
-                })
-                // Make a new loadMore function that hits the pagers
-                // (if necessary according to lowWaterMark)... or set
-                // results.loadMore to false if there is nothing left
-                // to fetch.
-                var loadable = []
-                Object.keys(results.pagers).map(function(key) {
-                    if (!results.pagers[key].done)
-                        loadable.push(results.pagers[key])
-                })
-                if (loadable.length == 0)
-                    results.loadMore = false
-                else
-                    results.loadMore = function() {
-                        results.loadMore = false
-                        loadable.map(function(pager) {
-                            if (pager.items().length - pager.itemsDisplayed < results.lowWaterMark)
-                                pager.loadNextPage()
-                        })
-                    }
             })
         })
     },
@@ -174,37 +126,47 @@ window.components.collection_search = {
         var sessions = vnode.state.sessionDB.loadAll()
         return m('form', {
             onsubmit: function() {
-                vnode.state.searchStart(vnode.state.searchEntered())
+                vnode.state.searchActive(vnode.state.searchEntered())
+                vnode.state.forgetSavedHeight = true
                 return false
             },
         }, [
-            m('.row', [
-                m('.col-md-6', [
-                    m('.input-group', [
-                        m('input#search.form-control[placeholder=Search]', {
-                            oninput: m.withAttr('value', vnode.state.searchEntered),
-                        }),
-                        m('.input-group-btn', [
-                            m('input.btn.btn-primary[type=submit][value="Search"]'),
+            m(SaveUIState, {
+                defaultState: '',
+                currentState: vnode.state.searchActive,
+                forgetSavedHeight: vnode.state.forgetSavedHeight,
+                saveBodyHeight: true,
+            }),
+            vnode.state.loader && [
+                m('.row', [
+                    m('.col-md-6', [
+                        m('.input-group', [
+                            m('input#search.form-control[placeholder=Search]', {
+                                oninput: m.withAttr('value', vnode.state.searchEntered),
+                                value: vnode.state.searchEntered(),
+                            }),
+                            m('.input-group-btn', [
+                                m('input.btn.btn-primary[type=submit][value="Search"]'),
+                            ]),
                         ]),
                     ]),
+                    m('.col-md-6', [
+                        'Searching sites: ',
+                        vnode.state.loader.children.length == 0
+                            ? m('span.label.label-xs.label-danger', 'none')
+                            : vnode.state.loader.children.map(function(child) {
+                                return [m('span.label.label-xs', {
+                                    className: child.state == child.LOADING ? 'label-warning' : 'label-success',
+                                }, child.sessionKey), ' ']
+                            }),
+                        ' ',
+                        m('a[href="/sessions"]', 'Add/remove sites'),
+                    ]),
                 ]),
-                m('.col-md-6', [
-                    'Searching sites: ',
-                    Object.keys(sessions).length == 0
-                        ? m('span.label.label-xs.label-danger', 'none')
-                        : Object.keys(sessions).sort().map(function(key) {
-                            return [m('span.label.label-xs', {
-                                className: vnode.state.results.pagers[key].items() ? 'label-info' : 'label-default',
-                            }, key), ' ']
-                        }),
-                    ' ',
-                    m('a[href="/sessions"]', 'Add/remove sites'),
-                ]),
-            ]),
-            m(window.components.collection_table_narrow, {
-                results: vnode.state.results,
-            }),
+                m(CollectionsTable, {
+                    loader: vnode.state.loader,
+                }),
+            ],
         ])
     },
 }