12033: Generalize MultisiteLoader to MergingLoader.
[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 = {
7     maybeLoadMore: function(dom) {
8         var loader = this.loader
9         if (loader.done || loader.loading)
10             // Can't start getting more items anyway: no point in
11             // checking anything else.
12             return
13         var contentRect = dom.getBoundingClientRect()
14         var scroller = window // TODO: use dom's nearest ancestor with scrollbars
15         if (contentRect.bottom < 2 * scroller.innerHeight) {
16             // We have less than 1 page worth of content available
17             // below the visible area. Load more.
18             loader.loadMore()
19             // Indicate loading is in progress.
20             window.requestAnimationFrame(m.redraw)
21         }
22     },
23     oncreate: function(vnode) {
24         vnode.state.maybeLoadMore = vnode.state.maybeLoadMore.bind(vnode.state, vnode.dom)
25         window.addEventListener('scroll', vnode.state.maybeLoadMore)
26         window.addEventListener('resize', vnode.state.maybeLoadMore)
27         vnode.state.timer = window.setInterval(vnode.state.maybeLoadMore, 200)
28         vnode.state.loader = vnode.attrs.loader
29         vnode.state.onupdate(vnode)
30     },
31     onupdate: function(vnode) {
32         vnode.state.loader = vnode.attrs.loader
33     },
34     onremove: function(vnode) {
35         window.clearInterval(vnode.state.timer)
36         window.removeEventListener('scroll', vnode.state.maybeLoadMore)
37         window.removeEventListener('resize', vnode.state.maybeLoadMore)
38     },
39     view: function(vnode) {
40         return m('table.table.table-condensed', [
41             m('thead', m('tr', [
42                 m('th'),
43                 m('th', 'uuid'),
44                 m('th', 'name'),
45                 m('th', 'last modified'),
46             ])),
47             m('tbody', [
48                 vnode.attrs.loader.items() && vnode.attrs.loader.items().map(function(item) {
49                     return m('tr', [
50                         m('td', m('a.btn.btn-xs.btn-default', {href: item.session.baseURL.replace('://', '://workbench.')+'collections/'+item.uuid}, 'Show')),
51                         m('td.arvados-uuid', item.uuid),
52                         m('td', item.name || '(unnamed)'),
53                         m('td', m(window.components.datetime, {parse: item.modified_at})),
54                     ])
55                 }),
56             ]),
57             m('tfoot', m('tr', [
58                 vnode.attrs.loader.done ? null : m('th[colspan=4]', m('button.btn.btn-xs', {
59                     className: vnode.attrs.loader.loading ? 'btn-default' : 'btn-primary',
60                     style: {
61                         display: 'block',
62                         width: '12em',
63                         marginLeft: 'auto',
64                         marginRight: 'auto',
65                     },
66                     disabled: vnode.attrs.loader.loading,
67                     onclick: function() {
68                         vnode.attrs.loader.loadMore()
69                         return false
70                     },
71                 }, vnode.attrs.loader.loading ? '(loading)' : 'Load more')),
72             ])),
73         ])
74     },
75 }
76
77 window.components.collection_search = {
78     oninit: function(vnode) {
79         vnode.state.sessionDB = new window.models.SessionDB()
80         vnode.state.searchEntered = m.stream()
81         vnode.state.searchActive = m.stream()
82         // When searchActive changes (e.g., when restoring state
83         // after navigation), update the text field too.
84         vnode.state.searchActive.map(vnode.state.searchEntered)
85         // When searchActive changes, create a new loader that filters
86         // with the given search term.
87         vnode.state.searchActive.map(function(q) {
88             var sessions = vnode.state.sessionDB.loadActive()
89             vnode.state.loader = new window.models.MergingLoader({
90                 children: Object.keys(sessions).map(function(key) {
91                     var session = sessions[key]
92                     return new window.models.MultipageLoader({
93                         loadFunc: function(filters) {
94                             if (q)
95                                 filters.push(['any', '@@', q+':*'])
96                             return vnode.state.sessionDB.request(session, 'arvados/v1/collections', {
97                                 data: {
98                                     filters: JSON.stringify(filters),
99                                     count: 'none',
100                                 },
101                             }).then(function(resp) {
102                                 resp.items.map(function(item) {
103                                     item.session = session
104                                 })
105                                 return resp
106                             })
107                         },
108                     })
109                 })
110             })
111         })
112     },
113     view: function(vnode) {
114         var sessions = vnode.state.sessionDB.loadAll()
115         return m('form', {
116             onsubmit: function() {
117                 vnode.state.searchActive(vnode.state.searchEntered())
118                 vnode.state.forgetSavedState = true
119                 return false
120             },
121         }, [
122             m(window.components.save_state, {
123                 defaultState: '',
124                 currentState: vnode.state.searchActive,
125                 forgetSavedState: vnode.state.forgetSavedState,
126                 saveBodyHeight: true,
127             }),
128             vnode.state.loader && [
129                 m('.row', [
130                     m('.col-md-6', [
131                         m('.input-group', [
132                             m('input#search.form-control[placeholder=Search]', {
133                                 oninput: m.withAttr('value', vnode.state.searchEntered),
134                                 value: vnode.state.searchEntered(),
135                             }),
136                             m('.input-group-btn', [
137                                 m('input.btn.btn-primary[type=submit][value="Search"]'),
138                             ]),
139                         ]),
140                     ]),
141                     m('.col-md-6', [
142                         'Searching sites: ',
143                         Object.keys(sessions).length == 0
144                             ? m('span.label.label-xs.label-danger', 'none')
145                             : Object.keys(sessions).sort().map(function(key) {
146                                 return [m('span.label.label-xs', {
147                                     className: !vnode.state.loader.children[key] ? 'label-default' :
148                                         vnode.state.loader.children[key].items() ? 'label-success' :
149                                         'label-warning',
150                                 }, key), ' ']
151                             }),
152                         ' ',
153                         m('a[href="/sessions"]', 'Add/remove sites'),
154                     ]),
155                 ]),
156                 m(window.components.collection_table, {
157                     loader: vnode.state.loader,
158                 }),
159             ],
160         ])
161     },
162 }