All new aditions to topology
[VSoRC/.git] / js / topology / common.js
1 // Copyright (c) 2018 Maen Artimy
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //   http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 /**
16  * An object to build html tables.
17  */
18 function Tables(category) {
19
20     // Category may be: flows, groups, meters, (and logs?)
21     var category = category;
22
23     /**
24      * Build table rows.
25      */
26     function makeRows(dpid, table, hdr_format, cell_format) {
27         // Create table header and rows and link them to data
28         // Modifies the 'table' object as it proceeds
29
30         var cols = [];
31         var $col = $('<tr></tr>');
32
33         // The rows in flow tables have checkboxes
34         if (category === 'flow') {
35             var $checkbox = $('<input type="checkbox" class="checkall"/>');
36             var $checktr = $('<th></th>').attr('data-sort', "nosort");
37             $checktr.append($checkbox);
38             $col.append($checktr);
39         }
40
41         // Build the table header
42         for (var i in table.fields) {
43             cols.push(table.fields[i]);
44             var $hdr = $('<th></th>');
45             var format = hdr_format(table.fields[i]);
46             $hdr.text(format[0]);
47             $hdr.attr('data-sort', format[1]);
48             $col.append($hdr);
49         }
50         table['$header'] = $col;
51
52         // Build the table rows
53         // The rows is an array of objects. Each object has the original data
54         // and the formatted data displayed in the row's cells
55         var rows = [];
56         table.data.forEach(function (item) {
57             var $row = $('<tr></tr>'); //.addClass('editable');
58             if (category === 'flow') {
59                 $row.append($('<td><input type="checkbox" class="rowbox"/></td>'));
60             }
61             item.dpid = dpid;
62             for (var i in cols) {
63                 var field = cols[i];
64                 var txt = cell_format(item[field]);
65                 $row.append($('<td></td>').text(txt));
66             }
67             rows.push({
68                 dataitem: item,
69                 $row: $row
70             });
71         });
72
73         // Flow tables are sorted by default by flow priority
74         if (category === 'flow') {
75             function compare(a, b) {
76                 return b.dataitem.priority - a.dataitem.priority;
77             }
78             rows.sort(compare);
79         }
80
81         table['rows'] = rows;
82
83         // The row's checkbox value will match the header's checkbox value.
84         //if (typeof ($checkbox) !== "undefined") {
85         if ($checkbox) {
86             $checkbox.change(function () {
87                 // Execlude hidden rows
88                 //$(this).closest('table').find('tr').not('.hiddenrow').find('.rowbox').prop('checked', this.checked);
89                 $(this).closest('table').find('.rowbox').prop('checked', this.checked);
90             });
91         }
92     }
93
94
95     /**
96      * Build the table's footer using the 'table' object
97      */
98     function makeFooter(dpid, table, ftr_format, cell_format) {
99
100         var $footer = $('<table></table>').addClass('oneliner');
101
102         var $col = $('<tr></tr>');
103         for (var i in table.extra.labels) {
104             var item = table.extra.labels[i];
105             var $hdr = $('<td></td>');
106             var label = ftr_format(item);
107             var data = cell_format(table.extra.data[item]);
108             $hdr.text(label + " : " + data);
109             $col.append($hdr);
110         }
111         $footer.append($col);
112         table['$footer'] = $footer;
113     }
114
115     // Attach event listener for rows
116     // function eventListener(row) {
117     //     $(row.$row).unbind('click');
118     //     $(row.$row).on('click', function(e) {
119     //         e.preventDefault();
120     //         sessionStorage.setItem(category, JSON.stringify(row.dataitem));
121     //         var msg = "Table entry copied to session storage.";
122     //         displayMessage(msg);
123     //     });
124     // };
125
126     /**
127      * Build the complete table by adding the components created by other functions.
128      */
129     function updateTable(dp_table) {
130         var $table = $('<table></table>').addClass('sortable').addClass('fixed');
131         var $tableHead = $('<thead></thead>');
132         var $tableBody = $('<tbody></tbody>');
133
134         $tableHead.append(dp_table.$header);
135         if (dp_table.rows) {
136             dp_table.rows.forEach(function (row) {
137                 $tableBody.append(row.$row);
138                 //eventListener(row);
139             });
140         }
141         $table.append($tableHead);
142         $table.append($tableBody);
143
144         //dp_table['$table'] = $table; //if a reference needs to be kept
145
146         return $table;
147     }
148
149     /**
150      * Build the card surrounding the table. The card
151      * may have additional data, such as caption and stats
152      */
153     function buildTableCard(dp_table) {
154
155         var $card = $('<div></div>').addClass('tableframe');
156         var $header = $('<div></div>').addClass('header');
157         var $container = $('<div></div>').addClass('container');
158         var $footer = $('<div></div>').addClass('footing');
159
160         var $title = $('<h1></h1>').text(dp_table.label + ' ' + dp_table.table_id);
161         var $alerts = $('<span class="alert"</span>');
162
163         $header.append($title);
164         $header.append($alerts);
165
166         // Apply only to flow tables
167         if (dp_table.type === "flows") {
168             var dpid = dp_table.extra;      // Flow tables have dpid in the extra field
169             var id = 'C' + dpid + '-' + dp_table.table_id;
170             $container.attr('id', id)
171
172             var $menu = getMenu(dp_table);
173
174             // Add collapse button
175             $hide = $('<button type="button" class="tablebtn"></button>');
176             $icon = $('<span class="icon icon-minimize"></span>');
177             //$icon = $('<a href="#0" class="icon icon-minimize"></a>)');
178             $hide.append($icon);
179             $hide.on('click', function () {
180                 $icon.toggleClass("icon-minimize icon-maximize");
181                 $container.slideToggle('fast', function () {
182                     $menu.toggle($container.is(':visible'))
183                     saveInSession("hidden", id, !$container.is(':visible'));
184                 });
185                 console.log($icon)
186
187             })
188             if (getFromSession("hidden", id)) {
189                 $menu.toggle(false);
190                 $container.toggle(false);
191                 $icon.toggleClass("icon-minimize icon-maximize");
192             };
193
194             // Add move button
195             $move = $('<button type="button" class="tablebtn"><i class="icon icon-move"></i></button>');
196             $move.on('click', function () {
197                 var $prev = $card.prev();
198                 console.log($prev)
199                 if ($prev.length > 0) {
200                     // if the table card has a previous sibling
201                     // swap with animation.
202                     $prev.animate({
203                         opacity: 0.25,
204                         height: "toggle"
205                     }, 300, function () {
206                         $card.insertBefore($prev);
207                         $prev.animate({
208                             opacity: 1.0,
209                             height: "toggle"
210                         }, 300, function () {
211                             var order_list = [];
212                             $card.parent('div').children().each(function (idx, elem) {
213                                 order_list.push($(elem).data('order'));
214                             });
215                             var tab_id = $card.closest('.tab-panel').attr('id');
216                             saveInSession("order", tab_id, order_list);
217                         });
218                     });
219                 }
220
221             })
222
223             $header.prepend($move);
224             $header.prepend($hide);
225             $header.append($menu);
226         }
227
228         var $table = updateTable(dp_table);
229         $container.append($table);
230
231         if (dp_table.$footer) {
232             $footer.append(dp_table.$footer);
233         }
234
235         $card.append($header);
236         $card.append($container);
237         $card.append($footer);
238
239         return $card;
240     }
241
242     /**
243      * Return an array of rows whose checkboxes are cheched
244      */
245     function getSelectedRows(dp_table) {
246         var selected = [];
247         dp_table.rows.forEach(function (item) {
248             var $f = item.$row.children(':has(:checkbox:checked)');
249             if ($f.length > 0) {
250                 selected.push(item);
251             }
252         });
253         return selected;
254     }
255
256     /**
257      * Set event listeneres for table options menu
258      */
259     function setMenuEvents($list, dp_table) {
260
261         // Hide a row
262         $list.on('click', 'a[href=hide]', function (e) {
263             e.preventDefault();
264             var selected = getSelectedRows(dp_table);
265             selected.forEach(function (row) {
266                 row.$row.addClass("hiddenrow");
267             })
268             if (selected.length > 0) {
269                 $(this).closest('.header').find('.alert').text('There are ' + selected.length + ' hidden rows!');
270             }
271         });
272
273         // Unhide all hidden rows
274         $list.on('click', 'a[href=unhide]', function (e) {
275             e.preventDefault();
276             dp_table.rows.forEach(function (row) {
277                 row.$row.removeClass("hiddenrow");
278             });
279             $(this).closest('.header').find('.alert').empty();
280         });
281
282         // Delete a row.
283         // Sends a request to delete flows and hides the rows until table is refreshed.
284         // The drawback is that the entry will be hiddeneven if delete is not successful.
285         $list.on('click', 'a[href=delete]', function (e) {
286             e.preventDefault();
287             var selected = getSelectedRows(dp_table);
288             var flows = [];
289             selected.forEach(function (row) {
290                 flows.push(row.dataitem)
291             });
292             if (flows.length > 0) {
293                 $.post("/flowdel", JSON.stringify(flows))
294                     .done(function (response) {
295                         displayMessage(response);
296                         selected.forEach(function (row) {
297                             row.$row.addClass("hiddenrow"); //temp
298                         })
299                     })
300                     .fail(function () {
301                         var msg = "No response from controller.";
302                         displayMessage(msg);
303                     })
304             }
305         });
306
307         // Sends a request to monitor flows.
308         $list.on('click', 'a[href=monitor]', function (e) {
309             e.preventDefault();
310             var selected = getSelectedRows(dp_table);
311             var flows = [];
312             selected.forEach(function (row) {
313                 flows.push(row.dataitem)
314             });
315             if (flows.length > 0) {
316                 $.post("/flowmonitor", JSON.stringify(flows))
317                     .done(function (response) {
318                         displayMessage(response);
319                         selected.forEach(function (row) {
320                             row.$row.addClass("monitorrow"); //temp
321                         })
322                     })
323                     .fail(function () {
324                         var msg = "No response from controller.";
325                         displayMessage(msg);
326                     })
327             }
328         });
329
330         // Saves the row to session storage
331         $list.on('click', 'a[href=edit]', function (e) {
332             e.preventDefault();
333             var selected = getSelectedRows(dp_table);
334             if (selected.length > 0) {
335                 sessionStorage.setItem(category, JSON.stringify(selected[0].dataitem));
336                 var msg = "Table entry copied to session storage.";
337                 displayMessage(msg);
338             }
339         });
340
341         $list.on('click', 'a', function (e) {
342             e.preventDefault();
343             var selected = getSelectedRows(dp_table);
344         });
345     }
346
347     /**
348      * Build the table's options menu
349      * @param {*} dp_table
350      */
351     function getMenu(dp_table) {
352
353         var $menu = $('<div></div>').addClass("dropdown");
354         var $button = $('<button><i class="icon icon-menu"></i></button>');
355         //$button.html('Options');
356
357         var $list = $('<div></div>').addClass("dropdown-content");
358         $list.html('<a href="delete">Delete</a> \
359             <a href="edit" disabled>Edit</a> \
360             <a href="hide">Hide</a> \
361             <a href="monitor">Monitor</a> \
362             <a href="unhide">Unhide</a>');
363
364         setMenuEvents($list, dp_table);
365
366         $menu.append($button);
367         $menu.append($list);
368
369         return $menu;
370     }
371
372     return {
373         buildTableCard: buildTableCard,
374         makeFooter: makeFooter,
375         makeRows: makeRows
376     }
377 }
378
379 /**
380  * An object to build html tabs
381  */
382 function Tabs(category) {
383     var category = category;
384
385     // Create the tab structure
386     function htmlCode(tab_labels, msg) {
387         var keys = Object.keys(tab_labels).sort();
388         // Add tab buttons
389         var tabs = '<ul class="tab-list">';
390         for (var idx in keys) {
391             var d = keys[idx];
392             var label = category === 'switches' ? 'SW_' + tab_labels[d] : tab_labels[d];
393             tabs += '<li class="tab-control" data-tab="tab-' + tab_labels[d] + '">' + label + '</li>';
394         }
395         tabs += '</ul>';
396
397         for (var idx in keys) {
398             var s = keys[idx]
399             tabs += '<div class="tab-panel" id="tab-' + tab_labels[s] + '"><h1>' + msg + '</h1></div>';
400         }
401         return tabs;
402     }
403
404     /**
405      * Set listeners to user events.
406      */
407     function listenToEvents() {
408         // only one tab list is allowed per page
409         $('.tab-list').on('click', '.tab-control', function () {
410             //var tab_id = $(this).attr('data-tab');
411             var tab_id = $(this).data('tab');
412
413             $('.tab-control').removeClass('active');
414             $('.tab-panel').removeClass('active');
415
416             $(this).addClass('active');
417             $("#" + tab_id).addClass('active');
418
419             // Save active tab per category
420             saveInSession('activetab', '', tab_id);
421         })
422     }
423
424     // Append HTML
425     function buildTabs(parent, tab_labels, msg) {
426         var html_code = htmlCode(tab_labels, msg)
427         $(parent).empty().append(html_code);
428         listenToEvents();
429     }
430
431     // Fill tab panel
432     function buildContent(id, envelope) {
433         envelope.children('.tableframe').each(function (i, v) {
434             $(v).data('order', i);
435         })
436         var order_list = getFromSession('order', 'tab-' + id);
437         if (order_list) {
438             var $cards = envelope.children('.tableframe');
439             if ($cards.length != order_list.length) {
440                 // pass, a table added/removed so we cannot use the previous order
441                 saveInSession("order", null);
442             } else {
443                 //var $clone = envelope.clone().empty();
444                 for (var i in order_list) {
445                     var $card = $cards.eq(order_list[i]).detach();
446                     envelope.append($card);
447                 }
448                 //envelope = $clone;
449             }
450         }
451         $('#tab-' + id).empty().append(envelope);
452     }
453
454     // Set active tab
455     function setActive() {
456         $('.tab-control').removeClass('active');
457         $('.tab-panel').removeClass('active');
458
459         var tab_id = getFromSession('activetab', '');
460         if (tab_id) {   // Active tab has been saved
461             var $first = $('[data-tab=' + tab_id + ']')
462             $first.addClass('active');
463             $("#" + tab_id).addClass('active');
464         } else {        // No active tab saved
465             var $first = $('.tab-control').first();
466             //var tab_id = $first.attr('data-tab');
467             var tab_id = $first.data('tab');
468             $first.addClass('active');
469             $("#" + tab_id).addClass('active');
470             saveInSession('activetab', '', tab_id);
471         }
472     }
473
474     return {
475         buildTabs: buildTabs,
476         buildContent: buildContent,
477         setActive: setActive
478     };
479 }
480
481 /**
482  * Save in sessionStorage
483  */
484 function saveInSession(objname, id, value) {
485     var page = $(document).find("title").text().replace(' ', '');
486     if (objname === 'activetab') {
487         sessionStorage.setItem(objname + page, value);
488     } else if (objname === 'hidden') {
489         if (!sessionStorage.hidden) {
490             sessionStorage.hidden = '';
491         }
492         var s = value ? sessionStorage.hidden + id : sessionStorage.hidden.replace(id, '');
493         sessionStorage.hidden = s;
494     } else if (objname === 'order') {
495         sessionStorage.setItem(objname + page + id, JSON.stringify(value));
496     }
497 }
498
499 /**
500  * Get from sessionStorage
501  */
502 function getFromSession(objname, id) {
503     var page = $(document).find("title").text().replace(' ', '');
504     if (objname === 'activetab') {
505         return sessionStorage.getItem(objname + page);
506     } else if (objname === 'hidden') {
507         if (sessionStorage.hidden) {
508             return sessionStorage.hidden.indexOf(id) > -1
509         }
510     } else if (objname === 'order') {
511         var res = sessionStorage.getItem(objname + page + id);
512         if (res) {
513             return JSON.parse(res);
514         }
515     }
516     return null;
517 }
518
519 // Display messages
520 function displayMessage(msg) {
521     var $x = $("#snackbar");
522     $x.text(msg)
523     $x.toggleClass("show");
524     setTimeout(function () { $x.toggleClass("show"); }, 3000);
525 }
526
527 // Download JSON file as text
528 function downloadFile(filename, data) {
529     var element = document.createElement('a');
530     element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data));
531     element.setAttribute('download', filename);
532     element.style.display = 'none';
533     document.body.appendChild(element);
534     element.click();
535     document.body.removeChild(element);
536 }
537
538 /**
539  * A Datapath Table Object.
540  *
541  */
542 function DPTable(id, type, label, fields, data, extra) {
543     this.table_id = id;
544     this.type = type;
545     this.label = label;
546     this.fields = fields;
547     this.data = data;
548     this.extra = extra;
549 }
550
551 // fix compatibility issue with RYU output
552 function fix_compatibility(odata) {
553     return odata.replace(/dl_/g, 'eth_')
554         .replace(/nw_/g, 'ipv4_')
555         .replace(/eth_vlan/g, 'vlan_vid')
556         .replace(/tp_dst/g, 'udp_dst')
557         .replace(/ipv4_proto/g, 'ip_proto');
558 }
559
560 // remove underscore and switch to uppercase
561 function hc(myString) {
562     return myString.replace("_", " ").replace(/\b\w/g, l => l.toUpperCase())
563 }
564
565 // Get information from datapaths
566 function getSwitchData(request, f, g) {
567     $.get("/data", "list=switches")
568         .done(function (switches) {
569             if ($.isEmptyObject(switches)) {
570                 var msg = "No switches found!";
571                 displayMessage(msg);
572                 return
573             }
574
575             // Process the switches list
576             f(switches);
577
578             var lst = [];
579             var all_data = [];
580
581             // Request flows from all switches
582             for (var sw in switches) {
583                 lst.push(
584                     $.get("/status", { status: request, dpid: switches[sw] })
585                         .done(function (flows) {
586                             all_data.push(flows)
587                         })
588                         .fail(function () {
589                             var msg = "Cannot read " + request + " form " + switches[sw] + "!";
590                             displayMessage(msg);
591                         })
592                 )
593             }
594
595             // Wait for all switches to reply
596             $.when.apply(this, lst).then(function () {
597                 // Process the flows
598                 g(all_data);
599             });
600
601         })
602         .fail(function () {
603             var msg = "No response from server!";
604             displayMessage(msg);
605         })
606 }
607
608 // Format the cell content
609 function cellFormating(cell) {
610     var newcell = cell;
611
612     if (typeof cell === 'object') {
613         newcell = JSON.stringify(cell);
614         newcell = newcell.replace('{}', 'ANY').replace('[]', 'DROP')
615             .replace(/{/g, '').replace(/}/g, '')
616             .replace(/^\[/, '').replace(/\]$/, '')
617             .replace(/":/g, '" = ').replace(/"/g, '')
618             .replace(/,/g, '\n');
619     } else if (cell === 4294967295) {
620         newcell = 'ANY';
621     }
622     return newcell;
623 }
624
625 // Table sort
626 var compare = {                           // Declare compare object
627     number: function (a, b) {                  // Add a method called name
628         a = Number(a);
629         b = Number(b);
630         return a - b;
631     },
632     alphanum: function (a, b) {                  // Add a method called name
633         if (a < b) {                          // If value a is less than value b
634             return -1;                          // Return -1
635         } else {                              // Otherwise
636             return a > b ? 1 : 0;               // If a is greater than b return 1 OR
637         }                                     // if they are the same return 0
638     },
639     duration: function (a, b) {              // Add a method called duration
640         a = a.split(':');                     // Split the time at the colon
641         b = b.split(':');                     // Split the time at the colon
642
643         a = Number(a[0]) * 60 + Number(a[1]); // Convert the time to seconds
644         b = Number(b[0]) * 60 + Number(b[1]); // Convert the time to seconds
645
646         return a - b;                         // Return a minus b
647     },
648     date: function (a, b) {                  // Add a method called date
649         a = new Date(a);                      // New Date object to hold the date
650         b = new Date(b);                      // New Date object to hold the date
651
652         return a - b;                         // Return a minus b
653     }
654 };
655
656 $('body').on('click', '.sortable th', function (e) {
657     var $header = $(this);                  // Get the header
658     var order = $header.data('sort');       // Get value of data-sort attribute
659     var column;                             // Declare variable called column
660     var $table = $header.parents('table');
661     var $tbody = $table.find('tbody');        // Store table body
662     var $controls = $table.find('th');        // Store table headers
663     var rows = $tbody.find('tr').toArray();   // Store array containing rows
664
665     // If selected item has ascending or descending class, reverse contents
666     if ($header.is('.ascending') || $header.is('.descending')) {
667         $header.toggleClass('ascending descending');    // Toggle to other class
668         $tbody.append(rows.reverse());                // Reverse the array
669     } else if (order !== "nosort") {                 // Otherwise perform a sort
670         $header.addClass('ascending');                // Add class to header
671         // Remove asc or desc from all other headers
672         $header.siblings().removeClass('ascending descending');
673         if (compare.hasOwnProperty(order)) {  // If compare object has method
674             column = $controls.index(this);     // Search for column's index no
675
676             rows.sort(function (a, b) {               // Call sort() on rows array
677                 a = $(a).find('td').eq(column).text(); // Get text of column in row a
678                 b = $(b).find('td').eq(column).text(); // Get text of column in row b
679                 return compare[order](a, b);           // Call compare method
680             });
681
682             $tbody.append(rows);
683         }
684     }
685 });