X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/2014757448a9ce52f2aa4f6af4ce2284c6858bb5..3854e6bfcd5344bce5ee0388248cb115e3c5e902:/apps/workbench/app/assets/javascripts/components/collections.js diff --git a/apps/workbench/app/assets/javascripts/components/collections.js b/apps/workbench/app/assets/javascripts/components/collections.js index 9334bcc3fc..33fca6c7b7 100644 --- a/apps/workbench/app/assets/javascripts/components/collections.js +++ b/apps/workbench/app/assets/javascripts/components/collections.js @@ -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,81 @@ 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', [ + // Guess workbench.{apihostport} is a + // Workbench... unless the host part of + // apihostport is an IPv4 or [IPv6] + // address. + item.session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])') ? null : + 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('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] + 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.session = session + }) + 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 +127,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, + }), + ], ]) }, }