//
// 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'),
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()
- })
- }
})
})
},
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,
+ }),
+ ],
])
},
}