Merge branch 'master' into 11453-federated-tokens
[arvados.git] / apps / workbench / app / assets / javascripts / components / search.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 window.SearchResultsTable = {
6     maybeLoadMore: function(dom) {
7         var loader = this.loader
8         if (loader.state != loader.READY)
9             // Can't start getting more items anyway: no point in
10             // checking anything else.
11             return
12         var contentRect = dom.getBoundingClientRect()
13         var scroller = window // TODO: use dom's nearest ancestor with scrollbars
14         if (contentRect.bottom < 2 * scroller.innerHeight) {
15             // We have less than 1 page worth of content available
16             // below the visible area. Load more.
17             loader.loadMore()
18             // Indicate loading is in progress.
19             window.requestAnimationFrame(m.redraw)
20         }
21     },
22     oncreate: function(vnode) {
23         vnode.state.maybeLoadMore = vnode.state.maybeLoadMore.bind(vnode.state, vnode.dom)
24         window.addEventListener('scroll', vnode.state.maybeLoadMore)
25         window.addEventListener('resize', vnode.state.maybeLoadMore)
26         vnode.state.timer = window.setInterval(vnode.state.maybeLoadMore, 200)
27         vnode.state.loader = vnode.attrs.loader
28         vnode.state.onupdate(vnode)
29     },
30     onupdate: function(vnode) {
31         vnode.state.loader = vnode.attrs.loader
32     },
33     onremove: function(vnode) {
34         window.clearInterval(vnode.state.timer)
35         window.removeEventListener('scroll', vnode.state.maybeLoadMore)
36         window.removeEventListener('resize', vnode.state.maybeLoadMore)
37     },
38     view: function(vnode) {
39         var loader = vnode.attrs.loader
40         var iconsMap = {
41             collections: m('i.fa.fa-fw.fa-archive'),
42             projects: m('i.fa.fa-fw.fa-folder'),
43         }
44         return m('table.table.table-condensed', [
45             m('thead', m('tr', [
46                 m('th'),
47                 m('th', 'uuid'),
48                 m('th', 'name'),
49                 m('th', 'last modified'),
50             ])),
51             m('tbody', [
52                 loader.items().map(function(item) {
53                     return m('tr', [
54                         m('td', [
55                             item.workbenchBaseURL() &&
56                                 m('a.btn.btn-xs.btn-default', {
57                                     'data-original-title': 'show '+item.objectType.description,
58                                     'data-placement': 'top',
59                                     'data-toggle': 'tooltip',
60                                     href: item.workbenchBaseURL()+'/'+item.objectType.wb_path+'/'+item.uuid,
61                                     // Bootstrap's tooltip feature
62                                     oncreate: function(vnode) { $(vnode.dom).tooltip() },
63                                 }, iconsMap[item.objectType.wb_path]),
64                         ]),
65                         m('td.arvados-uuid', item.uuid),
66                         m('td', item.name || '(unnamed)'),
67                         m('td', m(LocalizedDateTime, {parse: item.modified_at})),
68                     ])
69                 }),
70             ]),
71             loader.state == loader.DONE ? null : m('tfoot', m('tr', [
72                 m('th[colspan=4]', m('button.btn.btn-xs', {
73                     className: loader.state == loader.LOADING ? 'btn-default' : 'btn-primary',
74                     style: {
75                         display: 'block',
76                         width: '12em',
77                         marginLeft: 'auto',
78                         marginRight: 'auto',
79                     },
80                     disabled: loader.state == loader.LOADING,
81                     onclick: function() {
82                         loader.loadMore()
83                         return false
84                     },
85                 }, loader.state == loader.LOADING ? '(loading)' : 'Load more')),
86             ])),
87         ])
88     },
89 }
90
91 window.Search = {
92     oninit: function(vnode) {
93         vnode.state.sessionDB = new SessionDB()
94         vnode.state.searchEntered = m.stream()
95         vnode.state.searchActive = m.stream()
96         // When searchActive changes (e.g., when restoring state
97         // after navigation), update the text field too.
98         vnode.state.searchActive.map(vnode.state.searchEntered)
99         // When searchActive changes, create a new loader that filters
100         // with the given search term.
101         vnode.state.searchActive.map(function(q) {
102             var sessions = vnode.state.sessionDB.loadActive()
103             vnode.state.loader = new MergingLoader({
104                 children: Object.keys(sessions).map(function(key) {
105                     var session = sessions[key]
106                     var workbenchBaseURL = function() {
107                         return vnode.state.sessionDB.workbenchBaseURL(session)
108                     }
109                     var searchable_objects = [
110                         {
111                             wb_path: 'projects',
112                             api_path: 'arvados/v1/groups',
113                             filters: [['group_class', '=', 'project']],
114                             description: 'project',
115                         },
116                         {
117                             wb_path: 'collections',
118                             api_path: 'arvados/v1/collections',
119                             filters: [],
120                             description: 'collection',
121                         },
122                     ]
123                     return new MergingLoader({
124                         sessionKey: key,
125                         // For every session, search for every object type
126                         children: searchable_objects.map(function(obj_type) {
127                             return new MultipageLoader({
128                                 sessionKey: key,
129                                 loadFunc: function(filters) {
130                                     // Apply additional type dependant filters
131                                     filters = filters.concat(obj_type.filters)
132                                     var tsquery = to_tsquery(q)
133                                     if (tsquery) {
134                                         filters.push(['any', '@@', tsquery])
135                                     }
136                                     return vnode.state.sessionDB.request(session, obj_type.api_path, {
137                                         data: {
138                                             filters: JSON.stringify(filters),
139                                             count: 'none',
140                                         },
141                                     }).then(function(resp) {
142                                         resp.items.map(function(item) {
143                                             item.workbenchBaseURL = workbenchBaseURL
144                                             item.objectType = obj_type
145                                         })
146                                         return resp
147                                     })
148                                 },
149                             })
150                         }),
151                     })
152                 }),
153             })
154         })
155     },
156     view: function(vnode) {
157         var sessions = vnode.state.sessionDB.loadAll()
158         return m('form', {
159             onsubmit: function() {
160                 vnode.state.searchActive(vnode.state.searchEntered())
161                 vnode.state.forgetSavedHeight = true
162                 return false
163             },
164         }, [
165             m(SaveUIState, {
166                 defaultState: '',
167                 currentState: vnode.state.searchActive,
168                 forgetSavedHeight: vnode.state.forgetSavedHeight,
169                 saveBodyHeight: true,
170             }),
171             vnode.state.loader && [
172                 m('.row', [
173                     m('.col-md-6', [
174                         m('.input-group', [
175                             m('input#search.form-control[placeholder=Search collections and projects]', {
176                                 oninput: m.withAttr('value', vnode.state.searchEntered),
177                                 value: vnode.state.searchEntered(),
178                             }),
179                             m('.input-group-btn', [
180                                 m('input.btn.btn-primary[type=submit][value="Search"]'),
181                             ]),
182                         ]),
183                     ]),
184                     m('.col-md-6', [
185                         'Searching sites: ',
186                         vnode.state.loader.children.length == 0
187                             ? m('span.label.label-xs.label-danger', 'none')
188                             : vnode.state.loader.children.map(function(child) {
189                                 return [m('span.label.label-xs', {
190                                     className: child.state == child.LOADING ? 'label-warning' : 'label-success',
191                                 }, child.sessionKey), ' ']
192                             }),
193                         ' ',
194                         m('a[href="/sessions"]', 'Add/remove sites'),
195                     ]),
196                 ]),
197                 m(SearchResultsTable, {
198                     loader: vnode.state.loader,
199                 }),
200             ],
201         ])
202     },
203 }