GUI -- Allows for col-width="_px" to be specified in the html of table headers.
- Refactored table.js code
- Added helper functions to FnService.
- Deleted "sortable" from html in columns where sorting doesn't make sense (icons).
- Updated unit tests to reflect changes

Change-Id: I425101071bd5c7f237d64d98084a726cfce1d016
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 3c810a5..aa546e1 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -150,6 +150,7 @@
         return found;
     }
 
+    // return true if the object is empty, return false otherwise
     function isEmptyObject(obj) {
         var key;
         for (key in obj) {
@@ -165,6 +166,16 @@
         });
     }
 
+    // return the parameter without a px suffix
+    function noPx(num) {
+        return Number(num.replace(/px$/, ''));
+    }
+
+    // return an element's given style property without px suffix
+    function noPxStyle(elem, prop) {
+        return Number(elem.style(prop).replace(/px$/, ''));
+    }
+
     angular.module('onosUtil')
         .factory('FnService', ['$window', function (_$window_) {
             $window = _$window_;
@@ -183,7 +194,9 @@
                 inArray: inArray,
                 removeFromArray: removeFromArray,
                 isEmptyObject: isEmptyObject,
-                cap: cap
+                cap: cap,
+                noPx: noPx,
+                noPxStyle: noPxStyle
             };
     }]);
 
diff --git a/web/gui/src/main/webapp/app/fw/widget/table.js b/web/gui/src/main/webapp/app/fw/widget/table.js
index 285d21d..003b73c 100644
--- a/web/gui/src/main/webapp/app/fw/widget/table.js
+++ b/web/gui/src/main/webapp/app/fw/widget/table.js
@@ -20,45 +20,74 @@
 (function () {
     'use strict';
 
-    var $log, $window, fs, is,
-        currCol = {},
-        prevCol = {},
-        tableIconTdSize = 33,
-        bottomMargin = 200;
+    // injected refs
+    var $log, $window, fs, is;
+
+    // constants
+    var tableIconTdSize = 33,
+        bottomMargin = 200,
+        colWidth = 'col-width',
+        tableIcon = 'table-icon';
+
+    // internal state
+    var currCol = {},
+        prevCol = {};
 
     // Functions for creating a fixed header on a table (Angular Directive)
 
-    function setTableWidth(t) {
-        var tHeaders, tdElement, colWidth, numIcons, numNonIcons,
-            winWidth = fs.windowSize().width;
+    function setElemWidth(elem, size) {
+        elem.style('width', size + 'px')
+    }
 
-        tHeaders = t.selectAll('th');
-        numIcons = 0;
-        numNonIcons = 0;
+    function setColWidth(th, td, size) {
+        setElemWidth(th, size);
+        setElemWidth(td, size);
+    }
 
-        // FIXME: This should observe custom-set width from the HTML
+    // count number of headers of
+    //   - assigned width,
+    //   - icon width,
+    //   - and default width
+    // assumes assigned width is not given to icons
+    // returns the width of all columns that are not icons have an assigned width
+    function getDefaultWidth(headers) {
+        var winWidth = fs.windowSize().width,
+            iconCols = 0,
+            regCols = 0,
+            cstmColWidth = 0;
 
-        tHeaders.each(function(thElement, index) {
-            thElement = d3.select(this);
-            if (thElement.classed('table-icon')) {
-                numIcons = numIcons + 1;
+        headers.each(function (d, i) {
+            var thElement = d3.select(this),
+                cstmWidth = thElement.attr(colWidth);
+
+            if (cstmWidth) {
+                cstmColWidth += fs.noPx(cstmWidth);
+            } else if (thElement.classed(tableIcon)) {
+                iconCols += 1;
             } else {
-                numNonIcons = numNonIcons + 1;
+                regCols += 1;
             }
         });
 
-        colWidth = Math.floor((winWidth - (numIcons * tableIconTdSize)) / numNonIcons);
+        return Math.floor((winWidth - cstmColWidth -
+                            (iconCols * tableIconTdSize)) / regCols);
+    }
 
-        tHeaders.each(function(thElement, index) {
-            thElement = d3.select(this);
-            tdElement = t.select('td:nth-of-type(' + (index + 1) + ')');
+    function setTableWidth(t) {
+        var tHeaders = t.selectAll('th'),
+            defaultColWidth = getDefaultWidth(tHeaders);
 
-            if (thElement.classed('table-icon')) {
-                thElement.style('width', tableIconTdSize + 'px');
-                tdElement.style('width', tableIconTdSize + 'px');
+        tHeaders.each(function (d, i) {
+            var thElement = d3.select(this),
+                tdElement = t.select('td:nth-of-type(' + (i + 1) + ')'),
+                custWidth = thElement.attr(colWidth);
+
+            if (custWidth) {
+                setColWidth(thElement, tdElement, fs.noPx(custWidth));
+            } else if (thElement.classed(tableIcon)) {
+                setColWidth(thElement, tdElement, tableIconTdSize);
             } else {
-                thElement.style('width', colWidth + 'px');
-                tdElement.style('width', colWidth + 'px');
+                setColWidth(thElement, tdElement, defaultColWidth);
             }
         });
     }
diff --git a/web/gui/src/main/webapp/app/fw/widget/toolbar.js b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
index 74867e5..7b9b6b0 100644
--- a/web/gui/src/main/webapp/app/fw/widget/toolbar.js
+++ b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
@@ -75,11 +75,6 @@
         return null;
     }
 
-    function noPxWidth(elem) {
-        return Number(elem.style('width').replace(/px$/, ''));
-    }
-
-
     // ==================================
 
     function createToolbar(id, opts) {
@@ -119,7 +114,7 @@
         }
 
         function adjustWidth(btnWidth) {
-            if (noPxWidth(currentRow) >= maxWidth) {
+            if (fs.noPxStyle(currentRow, 'width') >= maxWidth) {
                 tbWidth += btnWidth;
                 maxWidth = tbWidth;
             }
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index 8838998..cbce523 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -213,7 +213,8 @@
         expect(fs.areFunctions(fs, [
             'isF', 'isA', 'isS', 'isO', 'contains',
             'areFunctions', 'areFunctionsNonStrict', 'windowSize', 'isMobile',
-            'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'cap'
+            'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'cap',
+            'noPx', 'noPxStyle'
         ])).toBeTruthy();
     });
 
@@ -387,4 +388,27 @@
         expect(fs.cap('foo bar')).toEqual('Foo bar');
     });
 
+    // === Tests for noPx()
+    it('should return the value without px suffix', function () {
+        expect(fs.noPx('10px')).toBe(10);
+        expect(fs.noPx('500px')).toBe(500);
+        expect(fs.noPx('-80px')).toBe(-80);
+    });
+
+    // === Tests for noPxStyle()
+    it("should give a style's property without px suffix", function () {
+        var d3Elem = d3.select('body')
+            .append('div')
+            .attr('id', 'fooElem')
+            .style({
+                width: '500px',
+                height: '200px',
+                'font-size': '12px'
+            });
+        expect(fs.noPxStyle(d3Elem, 'width')).toBe(500);
+        expect(fs.noPxStyle(d3Elem, 'height')).toBe(200);
+        expect(fs.noPxStyle(d3Elem, 'font-size')).toBe(12);
+        d3.select('#fooElem').remove();
+    });
+
 });
diff --git a/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js b/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js
index 107d27d..908ea16 100644
--- a/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js
@@ -32,7 +32,7 @@
                                 '<tr>' +
                                 '<th></th>' +
                                 '<th>Device ID </th>' +
-                                '<th>H/W Version </th>' +
+                                '<th col-width="100px">H/W Version </th>' +
                                 '<th>S/W Version </th>' +
                                 '</tr>' +
                                 '</thead>' +
@@ -51,7 +51,7 @@
 
         onosSortableHeaderTags = '<table ' +
                                 'onos-sortable-header ' +
-                                'sort-callback="sortCallback(urlSuffix)">' +
+                                'sort-callback="sortCallback(requestParams)">' +
                                 '<thead>' +
                                 '<tr>' +
                                 '<th colId="available"></th>' +
@@ -146,8 +146,12 @@
         angular.forEach(thElems, function (thElem, i) {
             thElem = angular.element(thElems[i]);
             tdElem = angular.element(tbody.find('td').eq(i));
+            var custWidth = thElem.attr('col-width');
 
-            if (tdElem.attr('class') === 'table-icon') {
+            if (custWidth) {
+                expect(thElem.css('width')).toBe(custWidth);
+                expect(tdElem.css('width')).toBe(custWidth);
+            } else if (tdElem.attr('class') === 'table-icon') {
                 expect(thElem.css('width')).toBe(tableIconTdSize + 'px');
                 expect(tdElem.css('width')).toBe(tableIconTdSize + 'px');
             } else {