ONOS-3780: Table model now handles two column sorts.

Change-Id: I8899d56fdca2084e4a7ca0392c21d14f1bc6ea62
diff --git a/core/api/src/main/java/org/onosproject/ui/table/TableModel.java b/core/api/src/main/java/org/onosproject/ui/table/TableModel.java
index bf5d95d..2b57b9e 100644
--- a/core/api/src/main/java/org/onosproject/ui/table/TableModel.java
+++ b/core/api/src/main/java/org/onosproject/ui/table/TableModel.java
@@ -52,6 +52,7 @@
 
     private static final CellComparator DEF_CMP = DefaultCellComparator.INSTANCE;
     private static final CellFormatter DEF_FMT = DefaultCellFormatter.INSTANCE;
+    private static final String EMPTY = "";
 
     private final String[] columnIds;
     private final Set<String> idSet;
@@ -206,14 +207,17 @@
     }
 
     /**
-     * Sorts the table rows based on the specified column, in the
-     * specified direction.
+     * Sorts the table rows based on the specified columns, in the
+     * specified directions. The second column is optional, and can be
+     * disregarded by passing null into id2 and dir2.
      *
-     * @param columnId column identifier
-     * @param dir sort direction
+     * @param id1 first column identifier
+     * @param dir1 first column sort direction
+     * @param id2 second column identifier (may be null)
+     * @param dir2 second column sort direction (may be null)
      */
-    public void sort(String columnId, SortDir dir) {
-        Collections.sort(rows, new RowComparator(columnId, dir));
+    public void sort(String id1, SortDir dir1, String id2, SortDir dir2) {
+        Collections.sort(rows, new RowComparator(id1, dir1, id2, dir2));
     }
 
 
@@ -225,33 +229,54 @@
         DESC
     }
 
+    private boolean nullOrEmpty(String s) {
+        return s == null || EMPTY.equals(s.trim());
+    }
+
     /**
      * Row comparator.
      */
     private class RowComparator implements Comparator<Row> {
-        private final String columnId;
-        private final SortDir dir;
-        private final CellComparator cellComparator;
+        private final String id1;
+        private final SortDir dir1;
+        private final String id2;
+        private final SortDir dir2;
+        private final CellComparator cc1;
+        private final CellComparator cc2;
 
         /**
          * Constructs a row comparator based on the specified
-         * column identifier and sort direction.
+         * column identifiers and sort directions. Note that id2 and dir2 may
+         * be null.
          *
-         * @param columnId column identifier
-         * @param dir sort direction
+         * @param id1 first column identifier
+         * @param dir1 first column sort direction
+         * @param id2 second column identifier
+         * @param dir2 second column sort direction
          */
-        public RowComparator(String columnId, SortDir dir) {
-            this.columnId = columnId;
-            this.dir = dir;
-            cellComparator = getComparator(columnId);
+        public RowComparator(String id1, SortDir dir1, String id2, SortDir dir2) {
+            this.id1 = id1;
+            this.dir1 = dir1;
+            this.id2 = id2;
+            this.dir2 = dir2;
+            cc1 = getComparator(id1);
+            cc2 = nullOrEmpty(id2) ? null : getComparator(id2);
         }
 
         @Override
         public int compare(Row a, Row b) {
-            Object cellA = a.get(columnId);
-            Object cellB = b.get(columnId);
-            int result = cellComparator.compare(cellA, cellB);
-            return dir == SortDir.ASC ? result : -result;
+            Object cellA = a.get(id1);
+            Object cellB = b.get(id1);
+            int result = cc1.compare(cellA, cellB);
+            result = dir1 == SortDir.ASC ? result : -result;
+
+            if (result == 0 && cc2 != null) {
+                cellA = a.get(id2);
+                cellB = b.get(id2);
+                result = cc2.compare(cellA, cellB);
+                result = dir2 == SortDir.ASC ? result : -result;
+            }
+            return result;
         }
     }
 
diff --git a/core/api/src/main/java/org/onosproject/ui/table/TableRequestHandler.java b/core/api/src/main/java/org/onosproject/ui/table/TableRequestHandler.java
index 5cbf01f..f47366c 100644
--- a/core/api/src/main/java/org/onosproject/ui/table/TableRequestHandler.java
+++ b/core/api/src/main/java/org/onosproject/ui/table/TableRequestHandler.java
@@ -20,13 +20,23 @@
 import org.onosproject.ui.JsonUtils;
 import org.onosproject.ui.RequestHandler;
 
+import static org.onosproject.ui.table.TableModel.sortDir;
+
 /**
  * Message handler specifically for table views.
  */
 public abstract class TableRequestHandler extends RequestHandler {
 
+    private static final String FIRST_COL = "firstCol";
+    private static final String FIRST_DIR = "firstDir";
+    private static final String SECOND_COL = "secondCol";
+    private static final String SECOND_DIR = "secondDir";
+
+    private static final String ASC = "asc";
+
     private static final String ANNOTS = "annots";
     private static final String NO_ROWS_MSG_KEY = "no_rows_msg";
+
     private final String respType;
     private final String nodeName;
 
@@ -51,9 +61,11 @@
         TableModel tm = createTableModel();
         populateTable(tm, payload);
 
-        String sortCol = JsonUtils.string(payload, "sortCol", defaultColumnId());
-        String sortDir = JsonUtils.string(payload, "sortDir", "asc");
-        tm.sort(sortCol, TableModel.sortDir(sortDir));
+        String firstCol = JsonUtils.string(payload, FIRST_COL, defaultColumnId());
+        String firstDir = JsonUtils.string(payload, FIRST_DIR, ASC);
+        String secondCol = JsonUtils.string(payload, SECOND_COL, null);
+        String secondDir = JsonUtils.string(payload, SECOND_DIR, null);
+        tm.sort(firstCol, sortDir(firstDir), secondCol, sortDir(secondDir));
 
         addTableConfigAnnotations(tm, payload);
 
diff --git a/core/api/src/test/java/org/onosproject/ui/table/TableModelTest.java b/core/api/src/test/java/org/onosproject/ui/table/TableModelTest.java
index 8c79a05..907ceff 100644
--- a/core/api/src/test/java/org/onosproject/ui/table/TableModelTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/table/TableModelTest.java
@@ -35,6 +35,9 @@
     private static final String FOO = "foo";
     private static final String BAR = "bar";
     private static final String ZOO = "zoo";
+    private static final String ID = "id";
+    private static final String ALPHA = "alpha";
+    private static final String NUMBER = "number";
 
     private enum StarWars {
         LUKE_SKYWALKER, LEIA_ORGANA, HAN_SOLO, C3PO, R2D2, JABBA_THE_HUTT
@@ -191,7 +194,7 @@
         initUnsortedTable();
 
         // sort by name
-        tm.sort(FOO, SortDir.ASC);
+        tm.sort(FOO, SortDir.ASC, null, null);
 
         // verify results
         rows = tm.getRows();
@@ -202,7 +205,7 @@
         }
 
         // now the other way
-        tm.sort(FOO, SortDir.DESC);
+        tm.sort(FOO, SortDir.DESC, null, null);
 
         // verify results
         rows = tm.getRows();
@@ -219,7 +222,7 @@
         initUnsortedTable();
 
         // sort by number
-        tm.sort(BAR, SortDir.ASC);
+        tm.sort(BAR, SortDir.ASC, null, null);
 
         // verify results
         rows = tm.getRows();
@@ -230,7 +233,7 @@
         }
 
         // now the other way
-        tm.sort(BAR, SortDir.DESC);
+        tm.sort(BAR, SortDir.DESC, null, null);
 
         // verify results
         rows = tm.getRows();
@@ -250,7 +253,7 @@
         tm.setFormatter(BAR, HexFormatter.INSTANCE);
 
         // sort by number
-        tm.sort(BAR, SortDir.ASC);
+        tm.sort(BAR, SortDir.ASC, null, null);
 
         // verify results
         rows = tm.getRows();
@@ -276,7 +279,7 @@
     public void sortAndFormatTwo() {
         initUnsortedTable();
         tm.setFormatter(BAR, HexFormatter.INSTANCE);
-        tm.sort(FOO, SortDir.ASC);
+        tm.sort(FOO, SortDir.ASC, null, null);
         rows = tm.getRows();
         int nr = rows.length;
         for (int i = 0; i < nr; i++) {
@@ -324,7 +327,7 @@
         tm.addRow().cell(FOO, StarWars.R2D2);
         tm.addRow().cell(FOO, StarWars.LUKE_SKYWALKER);
 
-        tm.sort(FOO, SortDir.ASC);
+        tm.sort(FOO, SortDir.ASC, null, null);
 
         // verify expected results
         StarWars[] ordered = StarWars.values();
@@ -336,6 +339,102 @@
         }
     }
 
+
+    // ------------------------
+    // Second sort column tests
+
+    private static final String A1 = "a1";
+    private static final String A2 = "a2";
+    private static final String A3 = "a3";
+    private static final String B1 = "b1";
+    private static final String B2 = "b2";
+    private static final String B3 = "b3";
+    private static final String C1 = "c1";
+    private static final String C2 = "c2";
+    private static final String C3 = "c3";
+    private static final String A = "A";
+    private static final String B = "B";
+    private static final String C = "C";
+
+    private static final String[] UNSORTED_IDS = {
+            A3, B2, A1, C2, A2, C3, B1, C1, B3
+    };
+    private static final String[] UNSORTED_ALPHAS = {
+            A, B, A, C, A, C, B, C, B
+    };
+    private static final int[] UNSORTED_NUMBERS = {
+            3, 2, 1, 2, 2, 3, 1, 1, 3
+    };
+
+    private static final String[] ROW_ORDER_AA_NA = {
+            A1, A2, A3, B1, B2, B3, C1, C2, C3
+    };
+    private static final String[] ROW_ORDER_AD_NA = {
+            C1, C2, C3, B1, B2, B3, A1, A2, A3
+    };
+    private static final String[] ROW_ORDER_AA_ND = {
+            A3, A2, A1, B3, B2, B1, C3, C2, C1
+    };
+    private static final String[] ROW_ORDER_AD_ND = {
+            C3, C2, C1, B3, B2, B1, A3, A2, A1
+    };
+
+    private void testAddRow(TableModel tm, int index) {
+        tm.addRow().cell(ID, UNSORTED_IDS[index])
+                .cell(ALPHA, UNSORTED_ALPHAS[index])
+                .cell(NUMBER, UNSORTED_NUMBERS[index]);
+    }
+
+    private TableModel unsortedDoubleTableModel() {
+        tm = new TableModel(ID, ALPHA, NUMBER);
+        for (int i = 0; i < 9; i++) {
+            testAddRow(tm, i);
+        }
+        return tm;
+    }
+
+    private void verifyRowOrder(String tag, TableModel tm, String[] rowOrder) {
+        int i = 0;
+        for (TableModel.Row row : tm.getRows()) {
+            assertEquals(tag + ": unexpected row id", rowOrder[i++], row.get(ID));
+        }
+    }
+
+    @Test
+    public void sortAlphaAscNumberAsc() {
+        tm = unsortedDoubleTableModel();
+        verifyRowOrder("unsorted", tm, UNSORTED_IDS);
+        tm.sort(ALPHA, SortDir.ASC, NUMBER, SortDir.ASC);
+        verifyRowOrder("aana", tm, ROW_ORDER_AA_NA);
+    }
+
+    @Test
+    public void sortAlphaDescNumberAsc() {
+        tm = unsortedDoubleTableModel();
+        verifyRowOrder("unsorted", tm, UNSORTED_IDS);
+        tm.sort(ALPHA, SortDir.DESC, NUMBER, SortDir.ASC);
+        verifyRowOrder("adna", tm, ROW_ORDER_AD_NA);
+    }
+
+    @Test
+    public void sortAlphaAscNumberDesc() {
+        tm = unsortedDoubleTableModel();
+        verifyRowOrder("unsorted", tm, UNSORTED_IDS);
+        tm.sort(ALPHA, SortDir.ASC, NUMBER, SortDir.DESC);
+        verifyRowOrder("aand", tm, ROW_ORDER_AA_ND);
+    }
+
+    @Test
+    public void sortAlphaDescNumberDesc() {
+        tm = unsortedDoubleTableModel();
+        verifyRowOrder("unsorted", tm, UNSORTED_IDS);
+        tm.sort(ALPHA, SortDir.DESC, NUMBER, SortDir.DESC);
+        verifyRowOrder("adnd", tm, ROW_ORDER_AD_ND);
+    }
+
+    // ----------------
+    // Annotation tests
+
     @Test
     public void stringAnnotation() {
         tm = new TableModel(FOO);
diff --git a/web/gui/src/main/webapp/app/fw/svg/icon.js b/web/gui/src/main/webapp/app/fw/svg/icon.js
index e797f8a..4bc1ffa 100644
--- a/web/gui/src/main/webapp/app/fw/svg/icon.js
+++ b/web/gui/src/main/webapp/app/fw/svg/icon.js
@@ -209,26 +209,16 @@
     }
 
     function sortIcons() {
-        function sortAsc(div) {
+        function _s(div, gid) {
             div.style('display', 'inline-block');
-            loadEmbeddedIcon(div, 'upArrow', 10);
+            loadEmbeddedIcon(div, gid, 10);
             div.classed('tableColSort', true);
         }
 
-        function sortDesc(div) {
-            div.style('display', 'inline-block');
-            loadEmbeddedIcon(div, 'downArrow', 10);
-            div.classed('tableColSort', true);
-        }
-
-        function sortNone(div) {
-            div.remove();
-        }
-
         return {
-            sortAsc: sortAsc,
-            sortDesc: sortDesc,
-            sortNone: sortNone
+            asc: function (div) { _s(div, 'upArrow'); },
+            desc: function (div) { _s(div, 'downArrow'); },
+            none: function (div) { div.remove(); }
         };
     }
 
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 327aedb..29098c4 100644
--- a/web/gui/src/main/webapp/app/fw/widget/table.js
+++ b/web/gui/src/main/webapp/app/fw/widget/table.js
@@ -28,16 +28,11 @@
         pdg = 22,
         flashTime = 1500,
         colWidth = 'col-width',
-        tableIcon = 'table-icon',
-        asc = 'asc',
-        desc = 'desc',
-        none = 'none';
+        tableIcon = 'table-icon';
 
     // internal state
-    var currCol = {},
-        prevCol = {},
-        cstmWidths = {},
-        sortIconAPI;
+    var cstmWidths = {},
+        api;
 
     // Functions for resizing a tabular view to the window
 
@@ -94,179 +89,208 @@
         }
     }
 
+    // sort columns state model and functions
+    var sortState = {
+        s: {
+            first: null,
+            second: null,
+            touched: null
+        },
+
+        reset: function () {
+            var s = sortState.s;
+            s.first && api.none(s.first.adiv);
+            s.second && api.none(s.second.adiv);
+            sortState.s = { first: null, second: null, touched: null };
+        },
+
+        touch: function (id, adiv) {
+            var s = sortState.s,
+                s1 = s.first,
+                d;
+
+            if (!s.touched) {
+                s.first = { id: id, dir: 'asc', adiv: adiv };
+                s.touched = id;
+            } else {
+                if (id === s.touched) {
+                    d = s1.dir === 'asc' ? 'desc' : 'asc';
+                    s1.dir = d;
+                    s1.adiv = adiv;
+
+                } else {
+                    s.second = s.first;
+                    s.first = { id: id, dir: 'asc', adiv: adiv };
+                    s.touched = id;
+                }
+            }
+        },
+
+        update: function () {
+            var s = sortState.s,
+                s1 = s.first,
+                s2 = s.second;
+            api[s1.dir](s1.adiv);
+            s2 && api.none(s2.adiv);
+        }
+    };
+
     // Functions for sorting table rows by header
 
     function updateSortDirection(thElem) {
-        sortIconAPI.sortNone(thElem.select('div'));
-        currCol.div = thElem.append('div');
-        currCol.colId = thElem.attr('colId');
+        var adiv = thElem.select('div'),
+            id = thElem.attr('colId');
 
-        if (currCol.colId === prevCol.colId) {
-            (currCol.dir === desc) ? currCol.dir = asc : currCol.dir = desc;
-            prevCol.dir = currCol.dir;
-        } else {
-            currCol.dir = asc;
-            prevCol.dir = none;
-        }
-        (currCol.dir === asc) ?
-            sortIconAPI.sortAsc(currCol.div) : sortIconAPI.sortDesc(currCol.div);
-
-        if (prevCol.colId && prevCol.dir === none) {
-            sortIconAPI.sortNone(prevCol.div);
-        }
-
-        prevCol.colId = currCol.colId;
-        prevCol.div = currCol.div;
+        api.none(adiv);
+        adiv = thElem.append('div');
+        sortState.touch(id, adiv);
+        sortState.update();
     }
 
     function sortRequestParams() {
+        var s = sortState.s,
+            s1 = s.first,
+            s2 = s.second,
+            id2 = s2 && s2.id,
+            dir2 = s2 && s2.dir;
         return {
-            sortCol: currCol.colId,
-            sortDir: currCol.dir
+            firstCol: s1.id,
+            firstDir: s1.dir,
+            secondCol: id2,
+            secondDir: dir2
         };
     }
 
-    function resetSort() {
-        if (currCol.div) {
-            sortIconAPI.sortNone(currCol.div);
-        }
-        if (prevCol.div) {
-            sortIconAPI.sortNone(prevCol.div);
-        }
-        currCol = {};
-        prevCol = {};
-    }
-
     angular.module('onosWidget')
-        .directive('onosTableResize', ['$log','$window',
-            'FnService', 'MastService',
+    .directive('onosTableResize', ['$log','$window', 'FnService', 'MastService',
 
-            function (_$log_, _$window_, _fs_, _mast_) {
-            return function (scope, element) {
-                $log = _$log_;
-                $window = _$window_;
-                fs = _fs_;
-                mast = _mast_;
+        function (_$log_, _$window_, _fs_, _mast_) {
+        return function (scope, element) {
+            $log = _$log_;
+            $window = _$window_;
+            fs = _fs_;
+            mast = _mast_;
 
-                var table = d3.select(element[0]),
-                    tableElems = {
-                        table: table,
-                        thead: table.select('.table-header').select('table'),
-                        tbody: table.select('.table-body').select('table')
-                    },
-                    wsz;
+            var table = d3.select(element[0]),
+                tableElems = {
+                    table: table,
+                    thead: table.select('.table-header').select('table'),
+                    tbody: table.select('.table-body').select('table')
+                },
+                wsz;
 
-                findCstmWidths(table);
+            findCstmWidths(table);
 
-                // adjust table on window resize
-                scope.$watchCollection(function () {
-                    return {
-                        h: $window.innerHeight,
-                        w: $window.innerWidth
-                    };
-                }, function () {
-                    wsz = fs.windowSize(0, 30);
-                    adjustTable(
-                        scope.tableData.length,
-                        tableElems,
-                        wsz.width, wsz.height
-                    );
-                });
+            // adjust table on window resize
+            scope.$watchCollection(function () {
+                return {
+                    h: $window.innerHeight,
+                    w: $window.innerWidth
+                };
+            }, function () {
+                wsz = fs.windowSize(0, 30);
+                adjustTable(
+                    scope.tableData.length,
+                    tableElems,
+                    wsz.width, wsz.height
+                );
+            });
 
-                // adjust table when data changes
-                scope.$watchCollection('tableData', function () {
-                    adjustTable(
-                        scope.tableData.length,
-                        tableElems,
-                        wsz.width, wsz.height
-                    );
-                });
+            // adjust table when data changes
+            scope.$watchCollection('tableData', function () {
+                adjustTable(
+                    scope.tableData.length,
+                    tableElems,
+                    wsz.width, wsz.height
+                );
+            });
 
-                scope.$on('$destroy', function () {
-                    cstmWidths = {};
-                });
-            };
-        }])
+            scope.$on('$destroy', function () {
+                cstmWidths = {};
+            });
+        };
+    }])
 
-        .directive('onosSortableHeader', ['$log', 'IconService',
-            function (_$log_, _is_) {
-            return function (scope, element) {
-                $log = _$log_;
-                is = _is_;
-                var header = d3.select(element[0]);
-                    sortIconAPI = is.sortIcons();
+    .directive('onosSortableHeader', ['$log', 'IconService',
+        function (_$log_, _is_) {
+        return function (scope, element) {
+            $log = _$log_;
+            is = _is_;
+            var header = d3.select(element[0]);
 
-                header.selectAll('td').on('click', function () {
-                    var col = d3.select(this);
+            api = is.sortIcons();
 
-                    if (col.attr('sortable') === '') {
-                        updateSortDirection(col);
-                        scope.sortParams = sortRequestParams();
-                        scope.sortCallback(scope.sortParams);
-                    }
-                });
+            header.selectAll('td').on('click', function () {
+                var col = d3.select(this);
 
-                scope.$on('$destroy', function () {
-                    resetSort();
-                });
-            };
-        }])
-
-        .directive('onosFlashChanges',
-            ['$log', '$parse', '$timeout', 'FnService',
-            function ($log, $parse, $timeout, fs) {
-
-            return function (scope, element, attrs) {
-                var idProp = attrs.idProp,
-                    table = d3.select(element[0]),
-                    trs, promise;
-
-                function highlightRows() {
-                    var changedRows = [];
-                    function classRows(b) {
-                        if (changedRows.length) {
-                            angular.forEach(changedRows, function (tr) {
-                                tr.classed('data-change', b);
-                            });
-                        }
-                    }
-                    // timeout because 'row-id' was the un-interpolated value
-                    // "{{link.one}}" for example, instead of link.one evaluated
-                    // timeout executes on the next digest -- after evaluation
-                    $timeout(function () {
-                        if (scope.tableData.length) {
-                            trs = table.selectAll('tr');
-                        }
-
-                        if (trs && !trs.empty()) {
-                            trs.each(function () {
-                                var tr = d3.select(this);
-                                if (fs.find(tr.attr('row-id'),
-                                        scope.changedData,
-                                        idProp) > -1) {
-                                    changedRows.push(tr);
-                                }
-                            });
-                            classRows(true);
-                            promise = $timeout(function () {
-                                classRows(false);
-                            }, flashTime);
-                            trs = undefined;
-                        }
-                    });
+                if (col.attr('sortable') === '') {
+                    updateSortDirection(col);
+                    scope.sortParams = sortRequestParams();
+                    scope.sortCallback(scope.sortParams);
                 }
+            });
 
-                // new items added:
-                scope.$on('ngRepeatComplete', highlightRows);
-                // items changed in existing set:
-                scope.$watchCollection('changedData', highlightRows);
+            scope.$on('$destroy', function () {
+                sortState.reset();
+            });
+        };
+    }])
 
-                scope.$on('$destroy', function () {
-                    if (promise) {
-                        $timeout.cancel(promise);
+    .directive('onosFlashChanges',
+        ['$log', '$parse', '$timeout', 'FnService',
+        function ($log, $parse, $timeout, fs) {
+
+        return function (scope, element, attrs) {
+            var idProp = attrs.idProp,
+                table = d3.select(element[0]),
+                trs, promise;
+
+            function highlightRows() {
+                var changedRows = [];
+                function classRows(b) {
+                    if (changedRows.length) {
+                        angular.forEach(changedRows, function (tr) {
+                            tr.classed('data-change', b);
+                        });
+                    }
+                }
+                // timeout because 'row-id' was the un-interpolated value
+                // "{{link.one}}" for example, instead of link.one evaluated
+                // timeout executes on the next digest -- after evaluation
+                $timeout(function () {
+                    if (scope.tableData.length) {
+                        trs = table.selectAll('tr');
+                    }
+
+                    if (trs && !trs.empty()) {
+                        trs.each(function () {
+                            var tr = d3.select(this);
+                            if (fs.find(tr.attr('row-id'),
+                                    scope.changedData,
+                                    idProp) > -1) {
+                                changedRows.push(tr);
+                            }
+                        });
+                        classRows(true);
+                        promise = $timeout(function () {
+                            classRows(false);
+                        }, flashTime);
+                        trs = undefined;
                     }
                 });
-            };
-        }]);
+            }
+
+            // new items added:
+            scope.$on('ngRepeatComplete', highlightRows);
+            // items changed in existing set:
+            scope.$watchCollection('changedData', highlightRows);
+
+            scope.$on('$destroy', function () {
+                if (promise) {
+                    $timeout.cancel(promise);
+                }
+            });
+        };
+    }]);
 
 }());
diff --git a/web/gui/src/main/webapp/app/view/app/app.js b/web/gui/src/main/webapp/app/view/app/app.js
index 11473b6..c4166d1 100644
--- a/web/gui/src/main/webapp/app/view/app/app.js
+++ b/web/gui/src/main/webapp/app/view/app/app.js
@@ -77,8 +77,10 @@
             respCb: refreshCtrls,
             // pre-populate sort so active apps are at the top of the list
             sortParams: {
-                sortCol: 'state',
-                sortDir: 'desc'
+                firstCol: 'state',
+                firstDir: 'desc',
+                secondCol: 'id',
+                secondDir: 'asc'
             }
         });
 
diff --git a/web/gui/src/main/webapp/app/view/device/device.html b/web/gui/src/main/webapp/app/view/device/device.html
index 9f9e37d..022984b 100644
--- a/web/gui/src/main/webapp/app/view/device/device.html
+++ b/web/gui/src/main/webapp/app/view/device/device.html
@@ -39,7 +39,7 @@
             <table>
                 <tr>
                     <td colId="available" class="table-icon" sortable></td>
-                    <td colId="type" class="table-icon" sortable></td>
+                    <td colId="type" class="table-icon"></td>
                     <td colId="name" sortable>Friendly Name </td>
                     <td colId="id" sortable>Device ID </td>
                     <td colId="masterid" sortable>Master Instance </td>
diff --git a/web/gui/src/main/webapp/app/view/host/host.html b/web/gui/src/main/webapp/app/view/host/host.html
index a103dac..6de96d5 100644
--- a/web/gui/src/main/webapp/app/view/host/host.html
+++ b/web/gui/src/main/webapp/app/view/host/host.html
@@ -14,7 +14,7 @@
         <div class="table-header" onos-sortable-header>
             <table>
                 <tr>
-                    <td colId="type" class="table-icon" sortable></td>
+                    <td colId="type" class="table-icon"></td>
                     <td colId="id" sortable>Host ID </td>
                     <td colId="mac" sortable>MAC Address </td>
                     <td colId="vlan" sortable>VLAN ID </td>