adding some tracability into the plugins code
[myslice.git] / plugins / querygrid / static / js / querygrid.js
1 // -*- js-indent-tab:2 -*-
2 /**
3  * Description: display a query result in a slickgrid-powered table
4  * Copyright (c) 2012-2013 UPMC Sorbonne Universite - INRIA
5  * License: GPLv3
6  */
7
8 /* 
9  * WARNINGS
10  *
11  * This is very rough for now and not deemed working
12  * 
13  * WARNINGS
14  */
15
16 /* ongoing adaptation to slickgrid 
17    still missing are
18 . checkboxes really running properly
19 . ability to sort on columns (should be straightforward
20   IIRC this got broken when moving to dataview, see dataview doc
21 . ability to sort on the checkboxes column 
22   (e.g. have resources 'in' the slice show up first)
23   not quite clear how to do this
24 . searching
25 . filtering
26 . style improvement
27 . rendering in the sliceview - does not use up all space, 
28   this is different from the behaviour with simpleview
29 */
30
31 (function($) {
32
33     var debug=false;
34     debug=true
35     var debug_deep=false;
36 //    debug_deep=true;
37
38     var QueryGrid = Plugin.extend({
39
40         init: function(options, element) {
41             this.classname="querygrid";
42             this._super(options, element);
43
44             /* Member variables */
45             // in general we expect 2 queries here
46             // query_uuid refers to a single object (typically a slice)
47             // query_all_uuid refers to a list (typically resources or users)
48             // these can return in any order so we keep track of which has been received yet
49             this.received_all_query = false;
50             this.received_query = false;
51
52 //            // We need to remember the active filter for filtering
53 //            this.filters = Array(); 
54
55             // an internal buffer for records that are 'in' and thus need to be checked 
56             this.buffered_records_to_check = [];
57
58             /* Events */
59             this.elmt().on('show', this, this.on_show);
60
61             var query = manifold.query_store.find_analyzed_query(this.options.query_uuid);
62             this.object = query.object;
63
64             //// we need 2 different keys
65             // * canonical_key is the primary key as derived from metadata (typically: urn)
66             //   and is used to communicate about a given record with the other plugins
67             // * init_key is a key that both kinds of records 
68             //   (i.e. records returned by both queries) must have (typically: hrn or hostname)
69             //   in general query_all will return well populated records, but query
70             //   returns records with only the fields displayed on startup
71             var keys = manifold.metadata.get_key(this.object);
72             this.canonical_key = (keys && keys.length == 1) ? keys[0] : undefined;
73             // 
74             this.init_key = this.options.init_key;
75             // have init_key default to canonical_key
76             this.init_key = this.init_key || this.canonical_key;
77             // sanity check
78             if ( ! this.init_key ) messages.warning ("QueryGrid : cannot find init_key");
79             if ( ! this.canonical_key ) messages.warning ("QueryGrid : cannot find canonical_key");
80             if (debug) messages.debug("querygrid: canonical_key="+this.canonical_key+" init_key="+this.init_key);
81
82             /* Setup query and record handlers */
83             this.listen_query(options.query_uuid);
84             this.listen_query(options.query_all_uuid, 'all');
85
86             /* GUI setup and event binding */
87             this.initialize_table();
88         },
89
90         /* PLUGIN EVENTS */
91
92         on_show: function(e) {
93             var self = e.data;
94             self.redraw_table();
95         }, // on_show
96
97         /* GUI EVENTS */
98
99         /* GUI MANIPULATION */
100
101         initialize_table: function() {
102             // compute columns based on columns and hidden_columns
103             this.slick_columns = [];
104             var all_columns = this.options.columns; // .concat(this.options.hidden_columns)
105             // xxx would be helpful to support a column_renamings options arg
106             // for redefining some labels like 'network_hrn' that really are not meaningful
107             for (c in all_columns) {
108                 var column=all_columns[c];
109                 this.slick_columns.push ( {id:column, name:column, field:column, 
110                                            cssClass: "querygrid-column-"+column,
111                                            width:100, minWidth:40, });
112             }
113             var checkbox_selector = new Slick.CheckboxSelectColumn({
114                 cssClass: "slick-checkbox"
115             });
116             this.slick_columns.push(checkbox_selector.getColumnDefinition());
117
118             // xxx should be extensible from caller with this.options.slickgrid_options 
119             this.slick_options = {
120                 enableCellNavigation: false,
121                 enableColumnReorder: true,
122                 showHeaderRow: true,
123                 syncColumnCellResize: true,
124             };
125
126             this.slick_data = [];
127             this.slick_dataview = new Slick.Data.UnfoldDataView();
128 // capturing for debug
129 window.dv=this.slick_dataview;
130             var self=this;
131             this.slick_dataview.onRowCountChanged.subscribe ( function (e,args) {
132                 self.slick_grid.updateRowCount();
133                 self.slick_grid.autosizeColumns();
134                 self.slick_grid.render();
135             });
136           
137             
138             var selector="#grid-"+this.options.domid;
139             if (debug_deep) {
140                 messages.debug("slick grid selector is " + selector);
141                 for (c in this.slick_columns) {
142                     var col=this.slick_columns[c];
143                     var msg="";
144                     for (k in col) msg = msg+" col["+k+"]="+col[k];
145                     messages.debug("slick_column["+c+"]:"+msg);
146                 }
147             }
148
149             this.slick_grid = new Slick.Grid(selector, this.slick_dataview, this.slick_columns, this.slick_options);
150 //          this.slick_grid.setSelectionModel (new Slick.RowSelectionModel ({selectActiveRow: false}));
151             this.slick_grid.setSelectionModel (new Slick.UnfoldSelectionModel({selectActiveRow: false}));
152             this.slick_grid.registerPlugin (checkbox_selector);
153             // autotooltips: for showing the full column name when ellipsed
154             var auto_tooltips = new Slick.AutoTooltips ({ enableForHeaderCells: true });
155             this.slick_grid.registerPlugin (auto_tooltips);
156             
157             this.columnpicker = new Slick.Controls.ColumnPicker (this.slick_columns, this.slick_grid, this.slick_options)
158
159         }, // initialize_table
160
161         new_record: function(record) {
162             this.slick_data.push(record);
163         },
164
165         clear_table: function() {
166             this.slick_data=[];
167           this.slick_dataview.setItems(this.slick_data,this.init_key,this.canonical_key);
168         },
169
170         redraw_table: function() {
171             this.slick_grid.autosizeColumns();
172             this.slick_grid.render();
173         },
174
175         show_column: function(field) {
176             console.log ("querygrid.show_column not yet implemented with slickgrid - field="+field);
177         },
178
179         hide_column: function(field) {
180             console.log("querygrid.hide_column not implemented with slickgrid - field="+field);
181         },
182
183         /*************************** QUERY HANDLER ****************************/
184
185         on_filter_added: function(filter) {
186             this.filters.push(filter);
187             this.redraw_table();
188         },
189
190         on_filter_removed: function(filter) {
191             // Remove corresponding filters
192             this.filters = $.grep(this.filters, function(x) {
193                 return x != filter;
194             });
195             this.redraw_table();
196         },
197         
198         on_filter_clear: function() {
199             this.redraw_table();
200         },
201
202         on_field_added: function(field) {
203             this.show_column(field);
204         },
205
206         on_field_removed: function(field) {
207             this.hide_column(field);
208         },
209
210         on_field_clear: function() {
211             alert('QueryGrid::clear_fields() not implemented');
212         },
213
214         /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
215         /*************************** ALL QUERY HANDLER ****************************/
216
217         on_all_filter_added: function(filter) {
218             // XXX
219             this.redraw_table();
220         },
221
222         on_all_filter_removed: function(filter) {
223             // XXX
224             this.redraw_table();
225         },
226         
227         on_all_filter_clear: function() {
228             // XXX
229             this.redraw_table();
230         },
231
232         on_all_field_added: function(field) {
233             this.show_column(field);
234         },
235
236         on_all_field_removed: function(field) {
237             this.hide_column(field);
238         },
239
240         on_all_field_clear: function() {
241             alert('QueryGrid::clear_fields() not implemented');
242         },
243
244
245         /*************************** RECORD HANDLER ***************************/
246
247         on_new_record: function(record) {
248             if (this.received_all_query) {
249                 // if the 'all' query has been dealt with already we may turn on the checkbox
250                 this._set_checkbox_from_record(record, true);
251             } else {
252                 // otherwise we need to remember that and do it later on
253                 if (debug) messages.debug("Remembering record to check, "+this.init_key+'='+ record[this.init_key]);
254                 this.buffered_records_to_check.push(record);
255             }
256         },
257
258         on_clear_records: function() {
259         },
260
261         // Could be the default in parent
262         on_query_in_progress: function() {
263             this.spin();
264         },
265
266         on_query_done: function() {
267             this.received_query = true;
268             // unspin once we have received both
269             if (this.received_all_query && this.received_query) {
270                 this._init_checkboxes();
271                 this.unspin();
272             }
273         },
274         
275         on_field_state_changed: function(data) {
276             switch(data.request) {
277                 case FIELD_REQUEST_ADD:
278                 case FIELD_REQUEST_ADD_RESET:
279                     this._set_checkbox_from_data(data.value, true);
280                     break;
281                 case FIELD_REQUEST_REMOVE:
282                 case FIELD_REQUEST_REMOVE_RESET:
283                     this._set_checkbox_from_data(data.value, false);
284                     break;
285                 default:
286                     break;
287             }
288         },
289
290         /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
291         // all
292         on_all_field_state_changed: function(data) {
293             switch(data.request) {
294                 case FIELD_REQUEST_ADD:
295                 case FIELD_REQUEST_ADD_RESET:
296                     this._set_checkbox_from_data(data.value, true);
297                     break;
298                 case FIELD_REQUEST_REMOVE:
299                 case FIELD_REQUEST_REMOVE_RESET:
300                     this._set_checkbox_from_data(data.value, false);
301                     break;
302                 default:
303                     break;
304             }
305         },
306
307         on_all_new_record: function(record) {
308             this.new_record(record);
309         },
310
311         on_all_clear_records: function() {
312             this.clear_table();
313
314         },
315
316         on_all_query_in_progress: function() {
317             // XXX parent
318             this.spin();
319         }, // on_all_query_in_progress
320
321         on_all_query_done: function() {
322             var start=new Date();
323             if (debug) messages.debug("1-shot initializing slickgrid content with " + this.slick_data.length + " lines");
324             // use this.init_key as the key for identifying rows
325           this.slick_dataview.setItems (this.slick_data, this.init_key,this.canonical_key);
326             var duration=new Date()-start;
327             if (debug) messages.debug("setItems " + duration + " ms");
328             if (debug_deep) {
329                 // show full contents of first row app
330                 for (k in this.slick_data[0]) messages.debug("slick_data[0]["+k+"]="+this.slick_data[0][k]);
331             }
332             
333             var self = this;
334             // if we've already received the slice query, we have not been able to set 
335             // checkboxes on the fly at that time (dom not yet created)
336             $.each(this.buffered_records_to_check, function(i, record) {
337                 if (debug) messages.debug ("delayed turning on checkbox " + i + " record= " + record);
338                 self._set_checkbox_from_record(record, true);
339             });
340             this.buffered_records_to_check = [];
341
342             this.received_all_query = true;
343             // unspin once we have received both
344             if (this.received_all_query && this.received_query) {
345                 this._init_checkboxes();
346                 this.unspin();
347             }
348
349         }, // on_all_query_done
350
351         /************************** PRIVATE METHODS ***************************/
352
353         _set_checkbox_from_record : function(record, checked) {
354             var init_id = record[this.init_key];
355             if (debug) messages.debug("querygrid.set_checkbox_from_record, init_id="+init_id);
356             var index = this.slick_dataview.getIdxById(init_id);
357             this._set_checkbox_from_index (index,checked);
358         },
359
360         _set_checkbox_from_data : function (id, checked) {
361             if (debug) messages.debug("querygrid.set_checkbox_from_data, id="+id);
362             // this is a local addition to mainstream dataview
363             // it's kind if slow in this first implementation (no hashing)
364             // but we should not notice that much
365             var index = this.slick_dataview.getIdxByIdKey(id,this.canonical_key);
366             this._set_checkbox_from_index (index,checked);
367         },
368
369         _set_checkbox_from_index : function (index, checked) {
370           if (index === undefined) { messages.warn("querygrid.set_checkbox - cannot find index"); return;}
371             if (checked === undefined) checked = true;
372             var selectedRows=this.slick_grid.getSelectedRows();
373             if (checked) // add index in current list
374                 selectedRows=selectedRows.concat(index);
375             else // remove index from current list
376                 selectedRows=selectedRows.filter(function(idx) {return idx!=index;});
377             // set new selection
378             this.slick_grid.setSelectedRows(selectedRows);
379         },
380
381 // initializing checkboxes
382 // have tried 2 approaches, but none seems to work as we need it
383 // issue summarized in here 
384 // http://stackoverflow.com/questions/20425193/slickgrid-selection-changed-callback-how-to-tell-between-manual-and-programmat
385         // arm the click callback on checkboxes
386         _init_checkboxes_manual : function () {
387             // xxx looks like checkboxes can only be the last column??
388             var checkbox_col = this.slick_grid.getColumns().length-1; // -1 +1 =0
389             console.log ("checkbox_col="+checkbox_col);
390             var self=this;
391             console.log ("HERE 1 with "+this.slick_dataview.getLength()+" sons");
392             for (var index=0; index < this.slick_dataview.getLength(); index++) {
393                 // retrieve key (i.e. hrn) for this line
394                 var key=this.slick_dataview.getItem(index)[this.key];
395                 // locate cell <div> for the checkbox
396                 var div=this.slick_grid.getCellNode(index,checkbox_col);
397                 if (index <=30) console.log("HERE2 div",div," index="+index+" col="+checkbox_col);
398                 // arm callback on single son of <div> that is the <input>
399                 $(div).children("input").each(function () {
400                     if (index<=30) console.log("HERE 3, index="+index+" key="+key);
401                     $(this).click(function() {self._checkbox_clicked(self,this,key);});
402                 });
403             }
404         },
405
406         // onSelectedRowsChanged will fire even when 
407         _init_checkboxes : function () {
408             console.log("_init_checkboxes");
409             var grid=this.slick_grid;
410             this.slick_grid.onSelectedRowsChanged.subscribe(function(){
411                 row_ids = grid.getSelectedRows();
412                 console.log(row_ids);
413             });
414         },
415
416         // the callback for when user clicks 
417         _checkbox_clicked: function(querygrid,input,key) {
418             // XXX this.value = key of object to be added... what about multiple keys ?
419             if (debug) messages.debug("querygrid click handler checked=" + input.checked + " key=" + key);
420             manifold.raise_event(querygrid.options.query_uuid, input.checked?SET_ADD:SET_REMOVED, key);
421             //return false; // prevent checkbox to be checked, waiting response from manifold plugin api
422             
423         },
424
425         // xxx from this and down, probably needs further tweaks for slickgrid
426
427         _querygrid_filter: function(oSettings, aData, iDataIndex) {
428             var ret = true;
429             $.each (this.filters, function(index, filter) { 
430                 /* XXX How to manage checkbox ? */
431                 var key = filter[0]; 
432                 var op = filter[1];
433                 var value = filter[2];
434
435                 /* Determine index of key in the table columns */
436                 var col = $.map(oSettings.aoColumns, function(x, i) {if (x.sTitle == key) return i;})[0];
437
438                 /* Unknown key: no filtering */
439                 if (typeof(col) == 'undefined')
440                     return;
441
442                 col_value=unfold.get_value(aData[col]);
443                 /* Test whether current filter is compatible with the column */
444                 if (op == '=' || op == '==') {
445                     if ( col_value != value || col_value==null || col_value=="" || col_value=="n/a")
446                         ret = false;
447                 }else if (op == '!=') {
448                     if ( col_value == value || col_value==null || col_value=="" || col_value=="n/a")
449                         ret = false;
450                 } else if(op=='<') {
451                     if ( parseFloat(col_value) >= value || col_value==null || col_value=="" || col_value=="n/a")
452                         ret = false;
453                 } else if(op=='>') {
454                     if ( parseFloat(col_value) <= value || col_value==null || col_value=="" || col_value=="n/a")
455                         ret = false;
456                 } else if(op=='<=' || op=='≤') {
457                     if ( parseFloat(col_value) > value || col_value==null || col_value=="" || col_value=="n/a")
458                         ret = false;
459                 } else if(op=='>=' || op=='≥') {
460                     if ( parseFloat(col_value) < value || col_value==null || col_value=="" || col_value=="n/a")
461                         ret = false;
462                 }else{
463                     // How to break out of a loop ?
464                     alert("filter not supported");
465                     return false;
466                 }
467
468             });
469             return ret;
470         },
471
472         _selectAll: function() {
473             // requires jQuery id
474             var uuid=this.id.split("-");
475             var oTable=$("#querygrid-"+uuid[1]).dataTable();
476             // Function available in QueryGrid 1.9.x
477             // Filter : displayed data only
478             var filterData = oTable._('tr', {"filter":"applied"});   
479             /* TODO: WARNING if too many nodes selected, use filters to reduce nuber of nodes */        
480             if(filterData.length<=100){
481                 $.each(filterData, function(index, obj) {
482                     var last=$(obj).last();
483                     var key_value=unfold.get_value(last[0]);
484                     if(typeof($(last[0]).attr('checked'))=="undefined"){
485                         $.publish('selected', 'add/'+key_value);
486                     }
487                 });
488             }
489         },
490
491     });
492
493     $.plugin('QueryGrid', QueryGrid);
494
495 })(jQuery);
496