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 bf5d95d65c..2b57b9e26c 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 @@ public class TableModel { 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 idSet; @@ -206,14 +207,17 @@ public class TableModel { } /** - * 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 @@ public class TableModel { DESC } + private boolean nullOrEmpty(String s) { + return s == null || EMPTY.equals(s.trim()); + } + /** * Row comparator. */ private class RowComparator implements Comparator { - 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 5cbf01ff9a..f47366cda4 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 com.fasterxml.jackson.databind.node.ObjectNode; 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 @@ public abstract class TableRequestHandler extends RequestHandler { 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 8c79a05892..907ceff65b 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 @@ public class TableModelTest { 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 @@ public class TableModelTest { 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 @@ public class TableModelTest { } // 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 @@ public class TableModelTest { 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 @@ public class TableModelTest { } // 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 @@ public class TableModelTest { 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 class TableModelTest { 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 @@ public class TableModelTest { 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 @@ public class TableModelTest { } } + + // ------------------------ + // 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 e797f8a40d..4bc1ffad7c 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 327aedb967..29098c4bc7 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 11473b651c..c4166d11e8 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 9f9e37d047..022984b3fb 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 @@ - + 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 a103daccfa..6de96d52bc 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 @@
Friendly Name Device ID Master Instance
- +
Host ID MAC Address VLAN ID