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

Change-Id: I8899d56fdca2084e4a7ca0392c21d14f1bc6ea62
This commit is contained in:
Simon Hunt 2016-01-19 15:54:22 -08:00 committed by Gerrit Code Review
parent ed627a58ed
commit 051e9faa1b
8 changed files with 356 additions and 204 deletions

View File

@ -52,6 +52,7 @@ public class TableModel {
private static final CellComparator DEF_CMP = DefaultCellComparator.INSTANCE; private static final CellComparator DEF_CMP = DefaultCellComparator.INSTANCE;
private static final CellFormatter DEF_FMT = DefaultCellFormatter.INSTANCE; private static final CellFormatter DEF_FMT = DefaultCellFormatter.INSTANCE;
private static final String EMPTY = "";
private final String[] columnIds; private final String[] columnIds;
private final Set<String> idSet; private final Set<String> idSet;
@ -206,14 +207,17 @@ public class TableModel {
} }
/** /**
* Sorts the table rows based on the specified column, in the * Sorts the table rows based on the specified columns, in the
* specified direction. * specified directions. The second column is optional, and can be
* disregarded by passing null into id2 and dir2.
* *
* @param columnId column identifier * @param id1 first column identifier
* @param dir sort direction * @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) { public void sort(String id1, SortDir dir1, String id2, SortDir dir2) {
Collections.sort(rows, new RowComparator(columnId, dir)); Collections.sort(rows, new RowComparator(id1, dir1, id2, dir2));
} }
@ -225,33 +229,54 @@ public class TableModel {
DESC DESC
} }
private boolean nullOrEmpty(String s) {
return s == null || EMPTY.equals(s.trim());
}
/** /**
* Row comparator. * Row comparator.
*/ */
private class RowComparator implements Comparator<Row> { private class RowComparator implements Comparator<Row> {
private final String columnId; private final String id1;
private final SortDir dir; private final SortDir dir1;
private final CellComparator cellComparator; private final String id2;
private final SortDir dir2;
private final CellComparator cc1;
private final CellComparator cc2;
/** /**
* Constructs a row comparator based on the specified * 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 id1 first column identifier
* @param dir sort direction * @param dir1 first column sort direction
* @param id2 second column identifier
* @param dir2 second column sort direction
*/ */
public RowComparator(String columnId, SortDir dir) { public RowComparator(String id1, SortDir dir1, String id2, SortDir dir2) {
this.columnId = columnId; this.id1 = id1;
this.dir = dir; this.dir1 = dir1;
cellComparator = getComparator(columnId); this.id2 = id2;
this.dir2 = dir2;
cc1 = getComparator(id1);
cc2 = nullOrEmpty(id2) ? null : getComparator(id2);
} }
@Override @Override
public int compare(Row a, Row b) { public int compare(Row a, Row b) {
Object cellA = a.get(columnId); Object cellA = a.get(id1);
Object cellB = b.get(columnId); Object cellB = b.get(id1);
int result = cellComparator.compare(cellA, cellB); int result = cc1.compare(cellA, cellB);
return dir == SortDir.ASC ? result : -result; 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;
} }
} }

View File

@ -20,13 +20,23 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onosproject.ui.JsonUtils; import org.onosproject.ui.JsonUtils;
import org.onosproject.ui.RequestHandler; import org.onosproject.ui.RequestHandler;
import static org.onosproject.ui.table.TableModel.sortDir;
/** /**
* Message handler specifically for table views. * Message handler specifically for table views.
*/ */
public abstract class TableRequestHandler extends RequestHandler { 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 ANNOTS = "annots";
private static final String NO_ROWS_MSG_KEY = "no_rows_msg"; private static final String NO_ROWS_MSG_KEY = "no_rows_msg";
private final String respType; private final String respType;
private final String nodeName; private final String nodeName;
@ -51,9 +61,11 @@ public abstract class TableRequestHandler extends RequestHandler {
TableModel tm = createTableModel(); TableModel tm = createTableModel();
populateTable(tm, payload); populateTable(tm, payload);
String sortCol = JsonUtils.string(payload, "sortCol", defaultColumnId()); String firstCol = JsonUtils.string(payload, FIRST_COL, defaultColumnId());
String sortDir = JsonUtils.string(payload, "sortDir", "asc"); String firstDir = JsonUtils.string(payload, FIRST_DIR, ASC);
tm.sort(sortCol, TableModel.sortDir(sortDir)); 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); addTableConfigAnnotations(tm, payload);

View File

@ -35,6 +35,9 @@ public class TableModelTest {
private static final String FOO = "foo"; private static final String FOO = "foo";
private static final String BAR = "bar"; private static final String BAR = "bar";
private static final String ZOO = "zoo"; 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 { private enum StarWars {
LUKE_SKYWALKER, LEIA_ORGANA, HAN_SOLO, C3PO, R2D2, JABBA_THE_HUTT LUKE_SKYWALKER, LEIA_ORGANA, HAN_SOLO, C3PO, R2D2, JABBA_THE_HUTT
@ -191,7 +194,7 @@ public class TableModelTest {
initUnsortedTable(); initUnsortedTable();
// sort by name // sort by name
tm.sort(FOO, SortDir.ASC); tm.sort(FOO, SortDir.ASC, null, null);
// verify results // verify results
rows = tm.getRows(); rows = tm.getRows();
@ -202,7 +205,7 @@ public class TableModelTest {
} }
// now the other way // now the other way
tm.sort(FOO, SortDir.DESC); tm.sort(FOO, SortDir.DESC, null, null);
// verify results // verify results
rows = tm.getRows(); rows = tm.getRows();
@ -219,7 +222,7 @@ public class TableModelTest {
initUnsortedTable(); initUnsortedTable();
// sort by number // sort by number
tm.sort(BAR, SortDir.ASC); tm.sort(BAR, SortDir.ASC, null, null);
// verify results // verify results
rows = tm.getRows(); rows = tm.getRows();
@ -230,7 +233,7 @@ public class TableModelTest {
} }
// now the other way // now the other way
tm.sort(BAR, SortDir.DESC); tm.sort(BAR, SortDir.DESC, null, null);
// verify results // verify results
rows = tm.getRows(); rows = tm.getRows();
@ -250,7 +253,7 @@ public class TableModelTest {
tm.setFormatter(BAR, HexFormatter.INSTANCE); tm.setFormatter(BAR, HexFormatter.INSTANCE);
// sort by number // sort by number
tm.sort(BAR, SortDir.ASC); tm.sort(BAR, SortDir.ASC, null, null);
// verify results // verify results
rows = tm.getRows(); rows = tm.getRows();
@ -276,7 +279,7 @@ public class TableModelTest {
public void sortAndFormatTwo() { public void sortAndFormatTwo() {
initUnsortedTable(); initUnsortedTable();
tm.setFormatter(BAR, HexFormatter.INSTANCE); tm.setFormatter(BAR, HexFormatter.INSTANCE);
tm.sort(FOO, SortDir.ASC); tm.sort(FOO, SortDir.ASC, null, null);
rows = tm.getRows(); rows = tm.getRows();
int nr = rows.length; int nr = rows.length;
for (int i = 0; i < nr; i++) { 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.R2D2);
tm.addRow().cell(FOO, StarWars.LUKE_SKYWALKER); tm.addRow().cell(FOO, StarWars.LUKE_SKYWALKER);
tm.sort(FOO, SortDir.ASC); tm.sort(FOO, SortDir.ASC, null, null);
// verify expected results // verify expected results
StarWars[] ordered = StarWars.values(); 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 @Test
public void stringAnnotation() { public void stringAnnotation() {
tm = new TableModel(FOO); tm = new TableModel(FOO);

View File

@ -209,26 +209,16 @@
} }
function sortIcons() { function sortIcons() {
function sortAsc(div) { function _s(div, gid) {
div.style('display', 'inline-block'); div.style('display', 'inline-block');
loadEmbeddedIcon(div, 'upArrow', 10); loadEmbeddedIcon(div, gid, 10);
div.classed('tableColSort', true); 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 { return {
sortAsc: sortAsc, asc: function (div) { _s(div, 'upArrow'); },
sortDesc: sortDesc, desc: function (div) { _s(div, 'downArrow'); },
sortNone: sortNone none: function (div) { div.remove(); }
}; };
} }

View File

@ -28,16 +28,11 @@
pdg = 22, pdg = 22,
flashTime = 1500, flashTime = 1500,
colWidth = 'col-width', colWidth = 'col-width',
tableIcon = 'table-icon', tableIcon = 'table-icon';
asc = 'asc',
desc = 'desc',
none = 'none';
// internal state // internal state
var currCol = {}, var cstmWidths = {},
prevCol = {}, api;
cstmWidths = {},
sortIconAPI;
// Functions for resizing a tabular view to the window // Functions for resizing a tabular view to the window
@ -94,52 +89,80 @@
} }
} }
// 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 // Functions for sorting table rows by header
function updateSortDirection(thElem) { function updateSortDirection(thElem) {
sortIconAPI.sortNone(thElem.select('div')); var adiv = thElem.select('div'),
currCol.div = thElem.append('div'); id = thElem.attr('colId');
currCol.colId = thElem.attr('colId');
if (currCol.colId === prevCol.colId) { api.none(adiv);
(currCol.dir === desc) ? currCol.dir = asc : currCol.dir = desc; adiv = thElem.append('div');
prevCol.dir = currCol.dir; sortState.touch(id, adiv);
} else { sortState.update();
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;
} }
function sortRequestParams() { function sortRequestParams() {
var s = sortState.s,
s1 = s.first,
s2 = s.second,
id2 = s2 && s2.id,
dir2 = s2 && s2.dir;
return { return {
sortCol: currCol.colId, firstCol: s1.id,
sortDir: currCol.dir 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') angular.module('onosWidget')
.directive('onosTableResize', ['$log','$window', .directive('onosTableResize', ['$log','$window', 'FnService', 'MastService',
'FnService', 'MastService',
function (_$log_, _$window_, _fs_, _mast_) { function (_$log_, _$window_, _fs_, _mast_) {
return function (scope, element) { return function (scope, element) {
@ -194,7 +217,8 @@
$log = _$log_; $log = _$log_;
is = _is_; is = _is_;
var header = d3.select(element[0]); var header = d3.select(element[0]);
sortIconAPI = is.sortIcons();
api = is.sortIcons();
header.selectAll('td').on('click', function () { header.selectAll('td').on('click', function () {
var col = d3.select(this); var col = d3.select(this);
@ -207,7 +231,7 @@
}); });
scope.$on('$destroy', function () { scope.$on('$destroy', function () {
resetSort(); sortState.reset();
}); });
}; };
}]) }])

View File

@ -77,8 +77,10 @@
respCb: refreshCtrls, respCb: refreshCtrls,
// pre-populate sort so active apps are at the top of the list // pre-populate sort so active apps are at the top of the list
sortParams: { sortParams: {
sortCol: 'state', firstCol: 'state',
sortDir: 'desc' firstDir: 'desc',
secondCol: 'id',
secondDir: 'asc'
} }
}); });

View File

@ -39,7 +39,7 @@
<table> <table>
<tr> <tr>
<td colId="available" class="table-icon" sortable></td> <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="name" sortable>Friendly Name </td>
<td colId="id" sortable>Device ID </td> <td colId="id" sortable>Device ID </td>
<td colId="masterid" sortable>Master Instance </td> <td colId="masterid" sortable>Master Instance </td>

View File

@ -14,7 +14,7 @@
<div class="table-header" onos-sortable-header> <div class="table-header" onos-sortable-header>
<table> <table>
<tr> <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="id" sortable>Host ID </td>
<td colId="mac" sortable>MAC Address </td> <td colId="mac" sortable>MAC Address </td>
<td colId="vlan" sortable>VLAN ID </td> <td colId="vlan" sortable>VLAN ID </td>