1 // Copyright (c) 2018 Maen Artimy
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
7 // http://www.apache.org/licenses/LICENSE-2.0
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.
16 * An object to build html tables.
18 function Tables(category) {
20 // Category may be: flows, groups, meters, (and logs?)
21 var category = category;
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
31 var $col = $('<tr></tr>');
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);
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]);
47 $hdr.attr('data-sort', format[1]);
50 table['$header'] = $col;
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
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>'));
64 var txt = cell_format(item[field]);
65 $row.append($('<td></td>').text(txt));
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;
83 // The row's checkbox value will match the header's checkbox value.
84 //if (typeof ($checkbox) !== "undefined") {
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);
96 * Build the table's footer using the 'table' object
98 function makeFooter(dpid, table, ftr_format, cell_format) {
100 var $footer = $('<table></table>').addClass('oneliner');
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);
111 $footer.append($col);
112 table['$footer'] = $footer;
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);
127 * Build the complete table by adding the components created by other functions.
129 function updateTable(dp_table) {
130 var $table = $('<table></table>').addClass('sortable').addClass('fixed');
131 var $tableHead = $('<thead></thead>');
132 var $tableBody = $('<tbody></tbody>');
134 $tableHead.append(dp_table.$header);
136 dp_table.rows.forEach(function (row) {
137 $tableBody.append(row.$row);
138 //eventListener(row);
141 $table.append($tableHead);
142 $table.append($tableBody);
144 //dp_table['$table'] = $table; //if a reference needs to be kept
150 * Build the card surrounding the table. The card
151 * may have additional data, such as caption and stats
153 function buildTableCard(dp_table) {
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');
160 var $title = $('<h1></h1>').text(dp_table.label + ' ' + dp_table.table_id);
161 var $alerts = $('<span class="alert"</span>');
163 $header.append($title);
164 $header.append($alerts);
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)
172 var $menu = getMenu(dp_table);
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>)');
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'));
188 if (getFromSession("hidden", id)) {
190 $container.toggle(false);
191 $icon.toggleClass("icon-minimize icon-maximize");
195 $move = $('<button type="button" class="tablebtn"><i class="icon icon-move"></i></button>');
196 $move.on('click', function () {
197 var $prev = $card.prev();
199 if ($prev.length > 0) {
200 // if the table card has a previous sibling
201 // swap with animation.
205 }, 300, function () {
206 $card.insertBefore($prev);
210 }, 300, function () {
212 $card.parent('div').children().each(function (idx, elem) {
213 order_list.push($(elem).data('order'));
215 var tab_id = $card.closest('.tab-panel').attr('id');
216 saveInSession("order", tab_id, order_list);
223 $header.prepend($move);
224 $header.prepend($hide);
225 $header.append($menu);
228 var $table = updateTable(dp_table);
229 $container.append($table);
231 if (dp_table.$footer) {
232 $footer.append(dp_table.$footer);
235 $card.append($header);
236 $card.append($container);
237 $card.append($footer);
243 * Return an array of rows whose checkboxes are cheched
245 function getSelectedRows(dp_table) {
247 dp_table.rows.forEach(function (item) {
248 var $f = item.$row.children(':has(:checkbox:checked)');
257 * Set event listeneres for table options menu
259 function setMenuEvents($list, dp_table) {
262 $list.on('click', 'a[href=hide]', function (e) {
264 var selected = getSelectedRows(dp_table);
265 selected.forEach(function (row) {
266 row.$row.addClass("hiddenrow");
268 if (selected.length > 0) {
269 $(this).closest('.header').find('.alert').text('There are ' + selected.length + ' hidden rows!');
273 // Unhide all hidden rows
274 $list.on('click', 'a[href=unhide]', function (e) {
276 dp_table.rows.forEach(function (row) {
277 row.$row.removeClass("hiddenrow");
279 $(this).closest('.header').find('.alert').empty();
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) {
287 var selected = getSelectedRows(dp_table);
289 selected.forEach(function (row) {
290 flows.push(row.dataitem)
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
301 var msg = "No response from controller.";
307 // Sends a request to monitor flows.
308 $list.on('click', 'a[href=monitor]', function (e) {
310 var selected = getSelectedRows(dp_table);
312 selected.forEach(function (row) {
313 flows.push(row.dataitem)
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
324 var msg = "No response from controller.";
330 // Saves the row to session storage
331 $list.on('click', 'a[href=edit]', function (e) {
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.";
341 $list.on('click', 'a', function (e) {
343 var selected = getSelectedRows(dp_table);
348 * Build the table's options menu
349 * @param {*} dp_table
351 function getMenu(dp_table) {
353 var $menu = $('<div></div>').addClass("dropdown");
354 var $button = $('<button><i class="icon icon-menu"></i></button>');
355 //$button.html('Options');
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>');
364 setMenuEvents($list, dp_table);
366 $menu.append($button);
373 buildTableCard: buildTableCard,
374 makeFooter: makeFooter,
380 * An object to build html tabs
382 function Tabs(category) {
383 var category = category;
385 // Create the tab structure
386 function htmlCode(tab_labels, msg) {
387 var keys = Object.keys(tab_labels).sort();
389 var tabs = '<ul class="tab-list">';
390 for (var idx in keys) {
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>';
397 for (var idx in keys) {
399 tabs += '<div class="tab-panel" id="tab-' + tab_labels[s] + '"><h1>' + msg + '</h1></div>';
405 * Set listeners to user events.
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');
413 $('.tab-control').removeClass('active');
414 $('.tab-panel').removeClass('active');
416 $(this).addClass('active');
417 $("#" + tab_id).addClass('active');
419 // Save active tab per category
420 saveInSession('activetab', '', tab_id);
425 function buildTabs(parent, tab_labels, msg) {
426 var html_code = htmlCode(tab_labels, msg)
427 $(parent).empty().append(html_code);
432 function buildContent(id, envelope) {
433 envelope.children('.tableframe').each(function (i, v) {
434 $(v).data('order', i);
436 var order_list = getFromSession('order', 'tab-' + id);
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);
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);
451 $('#tab-' + id).empty().append(envelope);
455 function setActive() {
456 $('.tab-control').removeClass('active');
457 $('.tab-panel').removeClass('active');
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);
475 buildTabs: buildTabs,
476 buildContent: buildContent,
482 * Save in sessionStorage
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 = '';
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));
500 * Get from sessionStorage
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
510 } else if (objname === 'order') {
511 var res = sessionStorage.getItem(objname + page + id);
513 return JSON.parse(res);
520 function displayMessage(msg) {
521 var $x = $("#snackbar");
523 $x.toggleClass("show");
524 setTimeout(function () { $x.toggleClass("show"); }, 3000);
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);
535 document.body.removeChild(element);
539 * A Datapath Table Object.
542 function DPTable(id, type, label, fields, data, extra) {
546 this.fields = fields;
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');
560 // remove underscore and switch to uppercase
561 function hc(myString) {
562 return myString.replace("_", " ").replace(/\b\w/g, l => l.toUpperCase())
565 // Get information from datapaths
566 function getSwitchData(request, f, g) {
568 .done(function (switches) {
569 if ($.isEmptyObject(switches)) {
570 var msg = "No switches found!";
575 // Process the switches list
581 // Request flows from all switches
582 for (var sw in switches) {
584 $.get("/status", { status: request, dpid: switches[sw] })
585 .done(function (flows) {
589 var msg = "Cannot read " + request + " form " + switches[sw] + "!";
595 // Wait for all switches to reply
596 $.when.apply(this, lst).then(function () {
603 var msg = "No response from server!";
608 // Format the cell content
609 function cellFormating(cell) {
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) {
626 var compare = { // Declare compare object
627 number: function (a, b) { // Add a method called name
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
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
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
646 return a - b; // Return a minus b
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
652 return a - b; // Return a minus b
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
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
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