Merge branch 'jordan' of ssh://git.onelab.eu/git/myslice into jordan
[myslice.git] / plugins / hazelnut / static / js / hazelnut.js
1 /**
2  * Description: display a query result in a datatables-powered <table>
3  * Copyright (c) 2012-2013 UPMC Sorbonne Universite - INRIA
4  * License: GPLv3
5  */
6
7 (function($){
8
9     // TEMP
10     var ELEMENT_KEY = 'resource_hrn';
11     var debug=false;
12     debug=true
13
14     var Hazelnut = Plugin.extend({
15
16         init: function(options, element) 
17         {
18             this._super(options, element);
19
20             /* Member variables */
21             // query status
22             this.received_all = false;
23             this.received_set = false;
24             this.in_set_buffer = Array();
25
26             /* XXX Events XXX */
27             this.$element.on('show.Datatables', this.on_show);
28             // Unbind all events using namespacing
29             // TODO in destructor
30             // $(window).unbind('Hazelnut');
31
32             /* Setup query and record handlers */
33             this.listen_query(options.query_uuid);
34             this.listen_query(options.query_all_uuid, 'all');
35
36             /* GUI setup and event binding */
37             this.initialize_table();
38         },
39
40         default_options: {
41             'checkboxes': false
42         },
43
44         /* PLUGIN EVENTS */
45
46         on_show: function()
47         {
48             // XXX
49             var $this=$(this);
50             // xxx wtf. why [1] ? would expect 0...
51             if (debug)
52                 messages.debug("Hitting suspicious line in hazelnut.show");
53             var oTable = $($('.dataTable', $this)[1]).dataTable();
54             oTable.fnAdjustColumnSizing()
55         
56             /* Refresh dataTabeles if click on the menu to display it : fix dataTables 1.9.x Bug */        
57             $(this).each(function(i,elt) {
58                 if (jQuery(elt).hasClass('dataTables')) {
59                     var myDiv=jQuery('#hazelnut-' + this.id).parent();
60                     if(myDiv.height()==0) {
61                         var oTable=$('#hazelnut-' + this.id).dataTable();            
62                         oTable.fnDraw();
63                     }
64                 }
65             });
66         }, // on_show
67
68         /* GUI EVENTS */
69
70         /* GUI MANIPULATION */
71
72         initialize_table: function() 
73         {
74             /* Transforms the table into DataTable, and keep a pointer to it */
75             var self = this;
76             actual_options = {
77                 // Customize the position of Datatables elements (length,filter,button,...)
78                 // we use a fluid row on top and another on the bottom, making sure we take 12 grid elt's each time
79                 sDom: "<'row-fluid'<'span5'l><'span1'r><'span6'f>>t<'row-fluid'<'span5'i><'span7'p>>",
80                 sPaginationType: 'bootstrap',
81                 // Handle the null values & the error : Datatables warning Requested unknown parameter
82                 // http://datatables.net/forums/discussion/5331/datatables-warning-...-requested-unknown-parameter/p2
83                 aoColumnDefs: [{sDefaultContent: '',aTargets: [ '_all' ]}],
84                 // WARNING: this one causes tables in a 'tabs' that are not exposed at the time this is run to show up empty
85                 // sScrollX: '100%',       /* Horizontal scrolling */
86                 bProcessing: true,      /* Loading */
87                 fnDrawCallback: function() { self._hazelnut_draw_callback.call(self); }
88                 // XXX use $.proxy here !
89             };
90             // the intention here is that options.datatables_options as coming from the python object take precedence
91             //  XXX DISABLED by jordan: was causing errors in datatables.js     $.extend(actual_options, options.datatables_options );
92             this.table = this.el('table').dataTable(actual_options);
93
94             /* Setup the SelectAll button in the dataTable header */
95             /* xxx not sure this is still working */
96             var oSelectAll = $('#datatableSelectAll-'+ this.options.plugin_uuid);
97             oSelectAll.html("<span class='ui-icon ui-icon-check' style='float:right;display:inline-block;'></span>Select All");
98             oSelectAll.button();
99             oSelectAll.css('font-size','11px');
100             oSelectAll.css('float','right');
101             oSelectAll.css('margin-right','15px');
102             oSelectAll.css('margin-bottom','5px');
103             oSelectAll.unbind('click');
104             oSelectAll.click(this._selectAll);
105
106             /* Add a filtering function to the current table 
107              * Note: we use closure to get access to the 'options'
108              */
109             $.fn.dataTableExt.afnFiltering.push(function( oSettings, aData, iDataIndex ) { 
110                 /* No filtering if the table does not match */
111                 if (oSettings.nTable.id != "hazelnut-" + self.options.plugin_uuid)
112                     return true;
113                 return this._hazelnut_filter.call(self, oSettings, aData, iDataIndex);
114             });
115
116             /* Processing hidden_columns */
117             $.each(this.options.hidden_columns, function(i, field) {
118                 self.hide_column(field);
119             });
120         }, // initialize_table
121
122         /**
123          * @brief Determine index of key in the table columns 
124          * @param key
125          * @param cols
126          */
127         getColIndex: function(key, cols) {
128             var tabIndex = $.map(cols, function(x, i) { if (x.sTitle == key) return i; });
129             return (tabIndex.length > 0) ? tabIndex[0] : -1;
130         }, // getColIndex
131
132  // UNUSED ? //         this.update_plugin = function(e, rows) {
133  // UNUSED ? //             // e.data is what we passed in second argument to subscribe
134  // UNUSED ? //             // so here it is the jquery object attached to the plugin <div>
135  // UNUSED ? //             var $plugindiv=e.data;
136  // UNUSED ? //             if (debug)
137  // UNUSED ? //                 messages.debug("entering hazelnut.update_plugin on id '" + $plugindiv.attr('id') + "'");
138  // UNUSED ? //             // clear the spinning wheel: look up an ancestor that has the need-spin class
139  // UNUSED ? //             // do this before we might return
140  // UNUSED ? //             $plugindiv.closest('.need-spin').spin(false);
141  // UNUSED ? // 
142  // UNUSED ? //             var options = this.options;
143  // UNUSED ? //             var hazelnut = this;
144  // UNUSED ? //     
145  // UNUSED ? //             /* if we get no result, or an error, try to make that clear, and exit */
146  // UNUSED ? //             if (rows.length==0) {
147  // UNUSED ? //                 if (debug) 
148  // UNUSED ? //                     messages.debug("Empty result on hazelnut " + this.options.domid);
149  // UNUSED ? //                 var placeholder=$(this.table).find("td.dataTables_empty");
150  // UNUSED ? //                 console.log("placeholder "+placeholder);
151  // UNUSED ? //                 if (placeholder.length==1) 
152  // UNUSED ? //                     placeholder.html(unfold.warning("Empty result"));
153  // UNUSED ? //                 else
154  // UNUSED ? //                     this.table.html(unfold.warning("Empty result"));
155  // UNUSED ? //                     return;
156  // UNUSED ? //             } else if (typeof(rows[0].error) != 'undefined') {
157  // UNUSED ? //                 // we now should have another means to report errors that this inline/embedded hack
158  // UNUSED ? //                 if (debug) 
159  // UNUSED ? //                     messages.error ("undefined result on " + this.options.domid + " - should not happen anymore");
160  // UNUSED ? //                 this.table.html(unfold.error(rows[0].error));
161  // UNUSED ? //                 return;
162  // UNUSED ? //             }
163  // UNUSED ? // 
164  // UNUSED ? //             /* 
165  // UNUSED ? //              * fill the dataTables object
166  // UNUSED ? //              * we cannot set html content directly here, need to use fnAddData
167  // UNUSED ? //              */
168  // UNUSED ? //             var lines = new Array();
169  // UNUSED ? //     
170  // UNUSED ? //             this.current_resources = Array();
171  // UNUSED ? //     
172  // UNUSED ? //             $.each(rows, function(index, row) {
173  // UNUSED ? //                 // this models a line in dataTables, each element in the line describes a cell
174  // UNUSED ? //                 line = new Array();
175  // UNUSED ? //      
176  // UNUSED ? //                 // go through table headers to get column names we want
177  // UNUSED ? //                 // in order (we have temporarily hack some adjustments in names)
178  // UNUSED ? //                 var cols = object.table.fnSettings().aoColumns;
179  // UNUSED ? //                 var colnames = cols.map(function(x) {return x.sTitle})
180  // UNUSED ? //                 var nb_col = cols.length;
181  // UNUSED ? //                 /* if we've requested checkboxes, then forget about the checkbox column for now */
182  // UNUSED ? //                 if (options.checkboxes) nb_col -= 1;
183  // UNUSED ? // 
184  // UNUSED ? //                 /* fill in stuff depending on the column name */
185  // UNUSED ? //                 for (var j = 0; j < nb_col; j++) {
186  // UNUSED ? //                     if (typeof colnames[j] == 'undefined') {
187  // UNUSED ? //                         line.push('...');
188  // UNUSED ? //                     } else if (colnames[j] == 'hostname') {
189  // UNUSED ? //                         if (row['type'] == 'resource,link')
190  // UNUSED ? //                             //TODO: we need to add source/destination for links
191  // UNUSED ? //                             line.push('');
192  // UNUSED ? //                         else
193  // UNUSED ? //                             line.push(row['hostname']);
194  // UNUSED ? //                     } else {
195  // UNUSED ? //                         if (row[colnames[j]])
196  // UNUSED ? //                             line.push(row[colnames[j]]);
197  // UNUSED ? //                         else
198  // UNUSED ? //                             line.push('');
199  // UNUSED ? //                     }
200  // UNUSED ? //                 }
201  // UNUSED ? //     
202  // UNUSED ? //                 /* catch up with the last column if checkboxes were requested */
203  // UNUSED ? //                 if (options.checkboxes) {
204  // UNUSED ? //                     var checked = '';
205  // UNUSED ? //                     // xxx problem is, we don't get this 'sliver' thing set apparently
206  // UNUSED ? //                     if (typeof(row['sliver']) != 'undefined') { /* It is equal to null when <sliver/> is present */
207  // UNUSED ? //                         checked = 'checked ';
208  // UNUSED ? //                         hazelnut.current_resources.push(row[ELEMENT_KEY]);
209  // UNUSED ? //                     }
210  // UNUSED ? //                     // Use a key instead of hostname (hard coded...)
211  // UNUSED ? //                     line.push(hazelnut.checkbox(options.plugin_uuid, row[ELEMENT_KEY], row['type'], checked, false));
212  // UNUSED ? //                 }
213  // UNUSED ? //     
214  // UNUSED ? //                 lines.push(line);
215  // UNUSED ? //     
216  // UNUSED ? //             });
217  // UNUSED ? //     
218  // UNUSED ? //             this.table.fnClearTable();
219  // UNUSED ? //             if (debug)
220  // UNUSED ? //                 messages.debug("hazelnut.update_plugin: total of " + lines.length + " rows");
221  // UNUSED ? //             this.table.fnAddData(lines);
222  // UNUSED ? //         
223  // UNUSED ? //         }, // update_plugin
224
225         checkbox: function (plugin_uuid, header, field, selected_str, disabled_str)
226         {
227             var result="";
228             if (header === null)
229                 header = '';
230             // Prefix id with plugin_uuid
231             result += "<input";
232             result += " class='hazelnut-checkbox-" + plugin_uuid + "'";
233             result += " id='hazelnut-checkbox-" + plugin_uuid + "-" + unfold.get_value(header).replace(/\\/g, '')  + "'";
234             result += " name='" + unfold.get_value(field) + "'";
235             result += " type='checkbox'";
236             result += selected_str;
237             result += disabled_str;
238             result += " autocomplete='off'";
239             result += " value='" + unfold.get_value(header) + "'";
240             result += "></input>";
241             return result;
242         }, // checkbox
243
244
245         new_record: function(record)
246         {
247             // this models a line in dataTables, each element in the line describes a cell
248             line = new Array();
249      
250             // go through table headers to get column names we want
251             // in order (we have temporarily hack some adjustments in names)
252             var cols = this.table.fnSettings().aoColumns;
253             var colnames = cols.map(function(x) {return x.sTitle})
254             var nb_col = cols.length;
255             /* if we've requested checkboxes, then forget about the checkbox column for now */
256             if (this.options.checkboxes) nb_col -= 1;
257
258             /* fill in stuff depending on the column name */
259             for (var j = 0; j < nb_col; j++) {
260                 if (typeof colnames[j] == 'undefined') {
261                     line.push('...');
262                 } else if (colnames[j] == 'hostname') {
263                     if (record['type'] == 'resource,link')
264                         //TODO: we need to add source/destination for links
265                         line.push('');
266                     else
267                         line.push(record['hostname']);
268                 } else {
269                     if (record[colnames[j]])
270                         line.push(record[colnames[j]]);
271                     else
272                         line.push('');
273                 }
274             }
275     
276             /* catch up with the last column if checkboxes were requested */
277             if (this.options.checkboxes) {
278                 var checked = '';
279                 // xxx problem is, we don't get this 'sliver' thing set apparently
280                 if (typeof(record['sliver']) != 'undefined') { /* It is equal to null when <sliver/> is present */
281                     checked = 'checked ';
282                     hazelnut.current_resources.push(record[ELEMENT_KEY]);
283                 }
284                 // Use a key instead of hostname (hard coded...)
285                 line.push(this.checkbox(this.options.plugin_uuid, record[ELEMENT_KEY], record['type'], checked, false));
286             }
287     
288             // XXX Is adding an array of lines more efficient ?
289             this.table.fnAddData(line);
290
291         },
292
293         clear_table: function()
294         {
295             this.table.fnClearTable();
296         },
297
298         redraw_table: function()
299         {
300             this.table.fnDraw();
301         },
302
303         show_column: function(field)
304         {
305             var oSettings = this.table.fnSettings();
306             var cols = oSettings.aoColumns;
307             var index = this.getColIndex(field,cols);
308             if (index != -1)
309                 this.table.fnSetColumnVis(index, true);
310         },
311
312         hide_column: function(field)
313         {
314             var oSettings = this.table.fnSettings();
315             var cols = oSettings.aoColumns;
316             var index = this.getColIndex(field,cols);
317             if (index != -1)
318                 this.table.fnSetColumnVis(index, false);
319         },
320
321         set_checkbox: function(record)
322         {
323             // XXX urn should be replaced by the key
324             // XXX we should enforce that both queries have the same key !!
325             checkbox_id = "#hazelnut-checkbox-" + this.options.plugin_uuid + "-" + unfold.escape_id(record[ELEMENT_KEY].replace(/\\/g, ''))
326             $(checkbox_id, this.table.fnGetNodes()).attr('checked', true);
327         },
328
329         /*************************** QUERY HANDLER ****************************/
330
331         on_filter_added: function(filter)
332         {
333             // XXX
334             this.redraw_table();
335         },
336
337         on_filter_removed: function(filter)
338         {
339             // XXX
340             this.redraw_table();
341         },
342         
343         on_filter_clear: function()
344         {
345             // XXX
346             this.redraw_table();
347         },
348
349         on_field_added: function(field)
350         {
351             this.show_column(field);
352         },
353
354         on_field_removed: function(field)
355         {
356             this.hide_column(field);
357         },
358
359         on_field_clear: function()
360         {
361             alert('Hazelnut::clear_fields() not implemented');
362         },
363
364         /*************************** RECORD HANDLER ***************************/
365
366         on_new_record: function(record)
367         {
368             /* NOTE in fact we are doing a join here */
369             if (this.received_all)
370                 // update checkbox for record
371                 this.set_checkbox(record);
372             else
373                 // store for later update of checkboxes
374                 this.in_set_buffer.push(record);
375         },
376
377         on_clear_records: function()
378         {
379         },
380
381         // Could be the default in parent
382         on_query_in_progress: function()
383         {
384             this.spin();
385         },
386
387         on_query_done: function()
388         {
389             if (this.received_all)
390                 this.unspin();
391             this.received_set = true;
392         },
393
394         // all
395
396         on_all_new_record: function(record)
397         {
398             this.new_record(record);
399         },
400
401         on_all_clear_records: function()
402         {
403             this.clear_table();
404
405         },
406
407         on_all_query_in_progress: function()
408         {
409             // XXX parent
410             this.spin();
411         }, // on_all_query_in_progress
412
413         on_all_query_done: function()
414         {
415             var self = this;
416             if (this.received_set) {
417                 /* XXX needed ? XXX We uncheck all checkboxes ... */
418                 $("[id^='datatables-checkbox-" + this.options.plugin_uuid +"']").attr('checked', false);
419
420                 /* ... and check the ones specified in the resource list */
421                 $.each(this.in_set_buffer, function(i, record) {
422                     self.set_checkbox(record);
423                 });
424
425                 this.unspin();
426             }
427             this.received_all = true;
428
429         }, // on_all_query_done
430
431         /************************** PRIVATE METHODS ***************************/
432
433         /** 
434          * @brief Hazelnut filtering function
435          */
436         _hazelnut_filter: function(oSettings, aData, iDataIndex)
437         {
438             var cur_query = this.current_query;
439             if (!cur_query) return true;
440             var ret = true;
441
442             /* We have an array of filters : a filter is an array (key op val) 
443              * field names (unless shortcut)    : oSettings.aoColumns  = [ sTitle ]
444              *     can we exploit the data property somewhere ?
445              * field values (unless formatting) : aData
446              *     formatting should leave original data available in a hidden field
447              *
448              * The current line should validate all filters
449              */
450             $.each (cur_query.filters, function(index, filter) { 
451                 /* XXX How to manage checkbox ? */
452                 var key = filter[0]; 
453                 var op = filter[1];
454                 var value = filter[2];
455
456                 /* Determine index of key in the table columns */
457                 var col = $.map(oSettings.aoColumns, function(x, i) {if (x.sTitle == key) return i;})[0];
458
459                 /* Unknown key: no filtering */
460                 if (typeof(col) == 'undefined')
461                     return;
462
463                 col_value=unfold.get_value(aData[col]);
464                 /* Test whether current filter is compatible with the column */
465                 if (op == '=' || op == '==') {
466                     if ( col_value != value || col_value==null || col_value=="" || col_value=="n/a")
467                         ret = false;
468                 }else if (op == '!=') {
469                     if ( col_value == value || col_value==null || col_value=="" || col_value=="n/a")
470                         ret = false;
471                 } else if(op=='<') {
472                     if ( parseFloat(col_value) >= value || col_value==null || col_value=="" || col_value=="n/a")
473                         ret = false;
474                 } else if(op=='>') {
475                     if ( parseFloat(col_value) <= value || col_value==null || col_value=="" || col_value=="n/a")
476                         ret = false;
477                 } else if(op=='<=' || op=='≤') {
478                     if ( parseFloat(col_value) > value || col_value==null || col_value=="" || col_value=="n/a")
479                         ret = false;
480                 } else if(op=='>=' || op=='≥') {
481                     if ( parseFloat(col_value) < value || col_value==null || col_value=="" || col_value=="n/a")
482                         ret = false;
483                 }else{
484                     // How to break out of a loop ?
485                     alert("filter not supported");
486                     return false;
487                 }
488
489             });
490             return ret;
491         },
492
493         _hazelnut_draw_callback: function()
494         {
495             /* 
496              * Handle clicks on checkboxes: reassociate checkbox click every time
497              * the table is redrawn 
498              */
499             $('.hazelnut-checkbox-' + this.options.plugin_uuid).unbind('click');
500             $('.hazelnut-checkbox-' + this.options.plugin_uuid).click({instance: this}, this._check_click);
501
502             if (!this.table)
503                 return;
504
505             /* Remove pagination if we show only a few results */
506             var wrapper = this.table; //.parent().parent().parent();
507             var rowsPerPage = this.table.fnSettings()._iDisplayLength;
508             var rowsToShow = this.table.fnSettings().fnRecordsDisplay();
509             var minRowsPerPage = this.table.fnSettings().aLengthMenu[0];
510
511             if ( rowsToShow <= rowsPerPage || rowsPerPage == -1 ) {
512                 $('.hazelnut_paginate', wrapper).css('visibility', 'hidden');
513             } else {
514                 $('.hazelnut_paginate', wrapper).css('visibility', 'visible');
515             }
516
517             if ( rowsToShow <= minRowsPerPage ) {
518                 $('.hazelnut_length', wrapper).css('visibility', 'hidden');
519             } else {
520                 $('.hazelnut_length', wrapper).css('visibility', 'visible');
521             }
522         },
523
524         _check_click: function(e) 
525         {
526
527             var self = e.data.instance;
528
529             // XXX this.value = key of object to be added... what about multiple keys ?
530             manifold.raise_event(self.options.query_uuid, this.checked?SET_ADD:SET_REMOVED, this.value);
531             
532         },
533
534         _selectAll: function() 
535         {
536             // requires jQuery id
537             var uuid=this.id.split("-");
538             var oTable=$("#hazelnut-"+uuid[1]).dataTable();
539             // Function available in Hazelnut 1.9.x
540             // Filter : displayed data only
541             var filterData = oTable._('tr', {"filter":"applied"});   
542             /* TODO: WARNING if too many nodes selected, use filters to reduce nuber of nodes */        
543             if(filterData.length<=100){
544                 $.each(filterData, function(index, obj) {
545                     var last=$(obj).last();
546                     var key_value=unfold.get_value(last[0]);
547                     if(typeof($(last[0]).attr('checked'))=="undefined"){
548                         $.publish('selected', 'add/'+key_value);
549                     }
550                 });
551             }
552         },
553
554     });
555
556     $.plugin('Hazelnut', Hazelnut);
557
558 })(jQuery);