Bri Prebilic Cole

ONOS-1842 - GUI -- Tables have better resizing behavior -- column headers stay a…

…bove their columns and table cell text is constrained to one size.

Change-Id: I89ca7d25d46d895c78c41b8250ce40408fbaba85
......@@ -16,68 +16,80 @@
/* ------ for summary-list tables ------ */
table.summary-list {
margin: 0 20px 16px 12px;
div.summary-list {
margin: 0 20px 16px 10px;
font-size: 10pt;
border-spacing: 0;
}
table.summary-list td.nodata {
div.summary-list table {
border-collapse: collapse;
table-layout: fixed;
empty-cells: show;
margin: 0;
}
div.summary-list div.table-body {
overflow-y: scroll;
}
div.summary-list tr.no-data td {
text-align: center;
font-style: italic;
}
.light table.summary-list tr:nth-child(even) {
.light div.summary-list tr:nth-child(even) {
background-color: #ddd;
}
.light table.summary-list tr:nth-child(odd) {
.light div.summary-list tr:nth-child(odd) {
background-color: #eee;
}
.dark table.summary-list tr:nth-child(even) {
.dark div.summary-list tr:nth-child(even) {
background-color: #333;
}
.dark table.summary-list tr:nth-child(odd) {
.dark div.summary-list tr:nth-child(odd) {
background-color: #444;
}
.light table.summary-list tr.selected {
.light div.summary-list tr.selected {
background-color: deepskyblue;
}
.dark table.summary-list tr.selected {
.dark div.summary-list tr.selected {
background-color: #304860;
}
table.summary-list td,
table.summary-list th {
div.summary-list td {
padding: 6px;
text-align: left;
word-wrap: break-word;
}
table.summary-list th {
div.summary-list .table-header td {
letter-spacing: 0.02em;
cursor: pointer;
font-weight: bold;
}
table.summary-list th:first-child {
div.summary-list .table-header td:first-child {
border-radius: 8px 0 0 0;
}
table.summary-list th:last-child {
div.summary-list .table-header td:last-child {
border-radius: 0 8px 0 0;
}
.light table.summary-list th {
.light div.summary-list .table-header td {
background-color: #bbb;
}
.dark table.summary-list th {
.dark div.summary-list .table-header td {
background-color: #222;
color: #ccc;
}
/* rows are selectable */
table.summary-list td {
div.summary-list .table-body td {
cursor: pointer;
}
.dark table.summary-list td {
.dark div.summary-list td {
color: #ccc;
}
......
......@@ -37,86 +37,68 @@
prevCol = {},
sortIconAPI;
// Functions for creating a fixed header on a table (Angular Directive)
// Functions for creating a scrolling table body with fixed table header
function setElemWidth(elem, size) {
elem.style('width', size + 'px')
function _width(elem, width) {
elem.style('width', width);
}
function setColWidth(th, td, size) {
setElemWidth(th, size);
setElemWidth(td, size);
function defaultSize(table, width) {
var thead = table.select('.table-header').select('table'),
tbody = table.select('.table-body').select('table'),
wpx = width + 'px';
_width(thead, wpx);
_width(tbody, wpx);
}
// 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
// or have an assigned width
function getDefaultWidth(headers) {
var winWidth = fs.windowSize().width,
iconCols = 0,
regCols = 0,
cstmColWidth = 0;
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 {
regCols += 1;
}
});
return Math.floor((winWidth - cstmColWidth -
(iconCols * tableIconTdSize)) / regCols);
}
function adjustTable(table, width, height) {
var thead = table.select('.table-header').select('table'),
tbodyDiv = table.select('.table-body'),
tbody = tbodyDiv.select('table'),
cstmWidths = {};
function setTableWidth(t) {
var tHeaders = t.selectAll('th'),
defaultColWidth = getDefaultWidth(tHeaders);
tHeaders.each(function (d, i) {
var thElement = d3.select(this),
tr = t.select('tr:nth-of-type(2)'),
tdElement = tr.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 {
setColWidth(thElement, tdElement, defaultColWidth);
}
});
}
function findCstmWidths() {
var headers = thead.selectAll('td');
// get the size of the window and then subtract the extra space at the top
// to get the height of the table
function setTableHeight(thead, tbody) {
var ttlHgt = fs.noPxStyle(d3.select('.tabular-header'), 'height'),
thHgt = fs.noPxStyle(thead, 'height'),
totalHgt = ttlHgt + thHgt + pdg,
tbleHgt = fs.windowSize(mast.mastHeight() + totalHgt).height;
thead.style('display', 'block');
tbody.style({
display: 'block',
height: tbleHgt + 'px',
overflow: 'auto'
});
}
headers.each(function (d, i) {
var h = d3.select(this),
index = i.toString();
if (h.classed(tableIcon)) {
cstmWidths[index] = tableIconTdSize + 'px';
}
if (h.attr(colWidth)) {
cstmWidths[index] = h.attr(colWidth);
}
});
$log.debug('Headers with custom widths: ', cstmWidths);
}
function fixTable(t, th, tb) {
setTableWidth(t);
setTableHeight(th, tb);
function setTdWidths(elem) {
var tds = elem.selectAll('tr:not(.ignore-width)').selectAll('td');
_width(elem, width + 'px');
tds.each(function (d, i) {
var td = d3.select(this),
index = i.toString();
if (cstmWidths.hasOwnProperty(index)) {
_width(td, cstmWidths[index]);
}
});
}
function setHeight(body) {
var h = height - (mast.mastHeight() +
fs.noPxStyle(d3.select('.tabular-header'), 'height') +
fs.noPxStyle(thead, 'height') + pdg);
body.style('height', h + 'px');
}
findCstmWidths();
setTdWidths(thead);
setTdWidths(tbody);
setHeight(tbodyDiv);
cstmWidths = {};
}
// Functions for sorting table rows by header
......@@ -163,40 +145,42 @@
}
angular.module('onosWidget')
.directive('onosFixedHeader', ['$window', 'FnService', 'MastService',
function (_$window_, _fs_, _mast_) {
.directive('onosFixedHeader', ['$log','$window',
'FnService', 'MastService',
function (_$log_, _$window_, _fs_, _mast_) {
return function (scope, element) {
$log = _$log_;
$window = _$window_;
fs = _fs_;
mast = _mast_;
var w = angular.element($window),
table = d3.select(element[0]),
thead = table.select('thead'),
tbody = table.select('tbody'),
canAdjust = false;
scope.$watch(function () {
return {
h: window.innerHeight,
w: window.innerWidth
h: $window.innerHeight,
w: $window.innerWidth
};
}, function (newVal) {
var wsz = fs.windowSize(0, 30);
scope.windowHeight = newVal.h;
scope.windowWidth = newVal.w;
}, function () {
var wsz = fs.windowSize(0, 30),
wWidth = wsz.width,
wHeight = wsz.height;
// default table size in case no data elements
table.style('width', wsz.width + 'px');
if (!scope.tableData.length) {
defaultSize(table, wWidth);
}
scope.$on('LastElement', function () {
// only adjust the table once it's completely loaded
fixTable(table, thead, tbody);
adjustTable(table, wWidth, wHeight);
canAdjust = true;
});
if (canAdjust) {
fixTable(table, thead, tbody);
adjustTable(table, wWidth, wHeight);
}
}, true);
......@@ -215,16 +199,16 @@
link: function (scope, element) {
$log = _$log_;
is = _is_;
var table = d3.select(element[0]);
var header = d3.select(element[0]);
sortIconAPI = is.sortIcons();
// when a header is clicked, change its icon tag
// when a header is clicked, change its sort direction
// and get sorting order to send to the server.
table.selectAll('th').on('click', function () {
var thElem = d3.select(this);
header.selectAll('td').on('click', function () {
var col = d3.select(this);
if (thElem.attr('sortable') === '') {
updateSortDirection(thElem);
if (col.attr('sortable') === '') {
updateSortDirection(col);
scope.ctrlCallback({
requestParams: sortRequestParams()
});
......@@ -234,9 +218,9 @@
};
}])
.factory('TableService', ['$log', 'IconService',
.factory('TableService', ['IconService',
function ($log, is) {
function (is) {
sortIconAPI = is.sortIcons();
return {
......
......@@ -25,7 +25,6 @@
// example params to buildTable:
// {
// self: this, <- controller object
// scope: $scope, <- controller scope
// tag: 'device', <- table identifier
// selCb: selCb <- row selection callback (optional)
......@@ -43,10 +42,10 @@
resp = o.tag + 'DataResponse',
onSel = fs.isF(o.selCb);
o.self.tableData = [];
o.scope.tableData = [];
function respCb(data) {
o.self.tableData = data[root];
o.scope.tableData = data[root];
o.scope.$apply();
}
......
<!-- app partial HTML -->
<div id="ov-app">
<div class="tabular-header">
<h2>Applications ({{ctrl.tableData.length}} total)</h2>
<h2>Applications ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh active"
icon icon-size="36" icon-id="refresh"
......@@ -19,39 +19,44 @@
</form>
</div>
<table class="summary-list"
onos-fixed-header
onos-sortable-header
sort-callback="sortCallback(requestParams)">
<thead>
<tr>
<th colId="state" class="table-icon" sortable></th>
<th colId="id" sortable>App ID </th>
<th colId="version" sortable>Version </th>
<th colId="origin" sortable>Origin </th>
<th colId="desc" col-width="640px">Description </th>
</tr>
</thead>
<div class="summary-list" onos-fixed-header>
<tbody>
<tr ng-hide="ctrl.tableData.length">
<td class="nodata" colspan="5">
No Applications found
</td>
</tr>
<div class="table-header"
onos-sortable-header sort-callback="sortCallback(requestParams)">
<table>
<tr>
<td colId="state" class="table-icon" sortable></td>
<td colId="id" sortable>App ID </td>
<td colId="version" sortable>Version </td>
<td colId="origin" sortable>Origin </td>
<td colId="desc" col-width="475px">Description </td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr ng-hide="tableData.length" class="no-data ignore-width">
<td colspan="5">
No Applications found
</td>
</tr>
<tr ng-repeat="app in tableData"
ng-click="selectCallback($event, app)"
ng-class="{selected: app === sel}"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{app._iconid_state}}"></div>
</td>
<td>{{app.id}}</td>
<td>{{app.version}}</td>
<td>{{app.origin}}</td>
<td>{{app.desc}}</td>
</tr>
</table>
</div>
</div>
<tr ng-repeat="app in ctrl.tableData"
ng-click="selectCallback($event, app)"
ng-class="{selected: app === sel}"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{app._iconid_state}}"></div>
</td>
<td>{{app.id}}</td>
<td>{{app.version}}</td>
<td>{{app.origin}}</td>
<td>{{app.desc}}</td>
</tr>
</tbody>
</table>
</div>
......
......@@ -71,7 +71,6 @@
d3.select('#app-deactivate').on('click', function () { appAction('deactivate'); });
tbs.buildTable({
self: this,
scope: $scope,
tag: 'app',
selCb: selCb
......
......@@ -17,7 +17,7 @@
<!-- Cluster partial HTML -->
<div id="ov-cluster">
<div class="tabular-header">
<h2>Cluster Nodes ({{ctrl.tableData.length}} total)</h2>
<h2>Cluster Nodes ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh active"
icon icon-size="36" icon-id="refresh"
......@@ -25,37 +25,42 @@
</div>
</div>
<table class="summary-list"
onos-fixed-header
onos-sortable-header
sort-callback="sortCallback(requestParams)">
<thead>
<tr>
<th colId="_iconid_state" class="table-icon" sortable></th>
<th colId="id" sortable>ID </th>
<th colId="ip" sortable>IP Address </th>
<th colId="tcp" sortable>TCP Port </th>
<th colId="updated" sortable>Last Updated </th>
</tr>
</thead>
<div class="summary-list" onos-fixed-header>
<tbody>
<tr ng-hide="ctrl.tableData.length">
<td class="nodata" colspan="5">
No Cluster Nodes found
</td>
</tr>
<div class="table-header"
onos-sortable-header sort-callback="sortCallback(requestParams)">
<table>
<tr>
<td colId="_iconid_state" class="table-icon" sortable></td>
<td colId="id" sortable>ID </td>
<td colId="ip" sortable>IP Address </td>
<td colId="tcp" sortable>TCP Port </td>
<td colId="updated" sortable>Last Updated </td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr ng-hide="tableData.length" class="no-data ignore-width">
<td colspan="5">
No Cluster Nodes found
</td>
</tr>
<tr ng-repeat="node in tableData"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{node._iconid_state}}"></div>
</td>
<td>{{node.id}}</td>
<td>{{node.ip}}</td>
<td>{{node.tcp}}</td>
<td>{{node.updated}}</td>
</tr>
</table>
</div>
</div>
<tr ng-repeat="node in ctrl.tableData"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{node._iconid_state}}"></div>
</td>
<td>{{node.id}}</td>
<td>{{node.ip}}</td>
<td>{{node.tcp}}</td>
<td>{{node.updated}}</td>
</tr>
</tbody>
</table>
</div>
......
......@@ -27,7 +27,6 @@
function ($log, $scope, ts, tbs) {
tbs.buildTable({
self: this,
scope: $scope,
tag: 'cluster'
});
......
<!-- Device partial HTML -->
<div id="ov-device">
<div class="tabular-header">
<h2>Devices ({{ctrl.tableData.length}} total)</h2>
<h2>Devices ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh active"
icon icon-size="36" icon-id="refresh"
......@@ -9,49 +9,54 @@
</div>
</div>
<table class="summary-list"
onos-fixed-header
onos-sortable-header
sort-callback="sortCallback(requestParams)">
<thead>
<tr>
<th colId="available" class="table-icon" sortable></th>
<th colId="type" class="table-icon" sortable></th>
<th colId="id" sortable>Device ID </th>
<th colId="masterid" sortable>Master Instance </th>
<th colId="num_ports" sortable>Ports </th>
<th colId="mfr" sortable>Vendor </th>
<th colId="hw" sortable>H/W Version </th>
<th colId="sw" sortable>S/W Version </th>
<th colId="protocol" sortable>Protocol </th>
</tr>
</thead>
<div class="summary-list" onos-fixed-header>
<tbody>
<tr ng-hide="ctrl.tableData.length">
<td class="nodata" colspan="9">
No Devices found
</td>
</tr>
<div class="table-header"
onos-sortable-header sort-callback="sortCallback(requestParams)">
<table>
<tr>
<td colId="available" class="table-icon" sortable></td>
<td colId="type" class="table-icon" sortable></td>
<td colId="id" sortable>Device ID </td>
<td colId="masterid" sortable>Master Instance </td>
<td colId="num_ports" sortable>Ports </td>
<td colId="mfr" sortable>Vendor </td>
<td colId="hw" sortable>H/W Version </td>
<td colId="sw" sortable>S/W Version </td>
<td colId="protocol" sortable>Protocol </td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr ng-hide="tableData.length" class="no-data ignore-width">
<td colspan="9">
No Devices found
</td>
</tr>
<tr ng-repeat="dev in tableData"
ng-click="selectCallback($event, dev)"
ng-class="{selected: dev === sel}"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{dev._iconid_available}}"></div>
</td>
<td class="table-icon">
<div icon icon-id="{{dev._iconid_type}}"></div>
</td>
<td>{{dev.id}}</td>
<td>{{dev.masterid}}</td>
<td>{{dev.num_ports}}</td>
<td>{{dev.mfr}}</td>
<td>{{dev.hw}}</td>
<td>{{dev.sw}}</td>
<td>{{dev.protocol}}</td>
</tr>
</table>
</div>
</div>
<tr ng-repeat="dev in ctrl.tableData"
ng-click="selectCallback($event, dev)"
ng-class="{selected: dev === sel}"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{dev._iconid_available}}"></div>
</td>
<td class="table-icon">
<div icon icon-id="{{dev._iconid_type}}"></div>
</td>
<td>{{dev.id}}</td>
<td>{{dev.masterid}}</td>
<td>{{dev.num_ports}}</td>
<td>{{dev.mfr}}</td>
<td>{{dev.hw}}</td>
<td>{{dev.sw}}</td>
<td>{{dev.protocol}}</td>
</tr>
</tbody>
</table>
</div>
......
......@@ -25,8 +25,7 @@
var $log, $scope, fs, mast, ps, wss, is, bns, ns, ttip;
// internal state
var self,
detailsPanel,
var detailsPanel,
pStartY, pHeight,
top, bottom, iconDiv,
wSize, selRow;
......@@ -183,8 +182,8 @@
}
function respDetailsCb(data) {
self.panelData = data.details;
populateDetails(self.panelData);
$scope.panelData = data.details;
populateDetails($scope.panelData);
detailsPanel.show();
}
......@@ -219,9 +218,8 @@
bns = _bns_;
ns = _ns_;
ttip = _ttip_;
self = this;
var handlers = {};
self.panelData = [];
$scope.panelData = [];
pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height')
+ mast.mastHeight() + topPdg;
wSize = fs.windowSize(pStartY);
......@@ -238,7 +236,6 @@
}
tbs.buildTable({
self: self,
scope: $scope,
tag: 'device',
selCb: selCb
......
......@@ -50,4 +50,5 @@
#ov-flow td.selector,
#ov-flow td.treatment {
padding-left: 36px;
opacity: 0.65;
}
\ No newline at end of file
......
......@@ -2,8 +2,8 @@
<div id="ov-flow">
<div class="tabular-header">
<h2>
Flows for Device {{ctrl.devId || "(No device selected)"}}
({{ctrl.tableData.length}} total)
Flows for Device {{devId || "(No device selected)"}}
({{tableData.length}} total)
</h2>
<div class="ctrl-btns">
<div class="refresh active"
......@@ -12,46 +12,52 @@
</div>
</div>
<table class="summary-list"
onos-fixed-header
onos-sortable-header
sort-callback="sortCallback(requestParams)">
<thead>
<tr>
<th colId="id" sortable>Flow ID </th>
<th colId="appId" sortable>App ID </th>
<th colId="groupId" sortable>Group ID </th>
<th colId="tableId" sortable>Table ID </th>
<th colId="priority" sortable>Priority </th>
<th colId="timeout" sortable>Timeout </th>
<th colId="permanent" sortable>Permanent </th>
<th colId="state" sortable>State </th>
</tr>
</thead>
<div class="summary-list" onos-fixed-header>
<tbody>
<tr ng-hide="ctrl.tableData.length">
<td class="nodata" colspan="8">
No Flows found
</td>
</tr>
<div class="table-header"
onos-sortable-header sort-callback="sortCallback(requestParams)">
<table>
<tr>
<td colId="id" col-width="180px" sortable>Flow ID </td>
<td colId="appId" sortable>App ID </td>
<td colId="groupId" sortable>Group ID </td>
<td colId="tableId" sortable>Table ID </td>
<td colId="priority" sortable>Priority </td>
<td colId="timeout" sortable>Timeout </td>
<td colId="permanent" sortable>Permanent </td>
<td colId="state" sortable>State </td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr ng-hide="tableData.length" class="no-data ignore-width">
<td colspan="8">
No Flows found
</td>
</tr>
<tr ng-repeat-start="flow in tableData">
<td>{{flow.id}}</td>
<td>{{flow.appId}}</td>
<td>{{flow.groupId}}</td>
<td>{{flow.tableId}}</td>
<td>{{flow.priority}}</td>
<td>{{flow.timeout}}</td>
<td>{{flow.permanent}}</td>
<td>{{flow.state}}</td>
</tr>
<tr class="ignore-width">
<td class="selector" colspan="8">{{flow.selector}}</td>
</tr>
<tr class="ignore-width"
ng-repeat-end ng-repeat-done>
<td class="treatment" colspan="8">{{flow.treatment}}</td>
</tr>
</table>
</div>
</div>
<tr ng-repeat-start="flow in ctrl.tableData">
<td>{{flow.id}}</td>
<td>{{flow.appId}}</td>
<td>{{flow.groupId}}</td>
<td>{{flow.tableId}}</td>
<td>{{flow.priority}}</td>
<td>{{flow.timeout}}</td>
<td>{{flow.permanent}}</td>
<td>{{flow.state}}</td>
</tr>
<tr>
<td class="selector" colspan="8">{{flow.selector}}</td>
</tr>
<tr ng-repeat-end ng-repeat-done>
<td class="treatment" colspan="8">{{flow.treatment}}</td>
</tr>
</tbody>
</table>
</div>
......
......@@ -30,8 +30,7 @@
'FnService', 'TableService', 'TableBuilderService',
function (_$log_, _$scope_, _$location_, _fs_, _ts_, _tbs_) {
var self = this,
params;
var params;
$log = _$log_;
$scope = _$scope_;
$location = _$location_;
......@@ -41,11 +40,10 @@
params = $location.search();
if (params.hasOwnProperty('devId')) {
self.devId = params['devId'];
$scope.devId = params['devId'];
}
tbs.buildTable({
self: self,
scope: $scope,
tag: 'flow',
query: params
......
<!-- Host partial HTML -->
<div id="ov-host">
<div class="tabular-header">
<h2>Hosts ({{ctrl.tableData.length}} total)</h2>
<h2>Hosts ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh active"
icon icon-size="36" icon-id="refresh"
......@@ -9,39 +9,44 @@
</div>
</div>
<table class="summary-list"
onos-fixed-header
onos-sortable-header
sort-callback="sortCallback(requestParams)">
<thead>
<tr>
<th colId="type" class="table-icon" sortable></th>
<th colId="id" sortable>Host ID </th>
<th colId="mac" sortable>MAC Address </th>
<th colId="vlan" sortable>VLAN ID </th>
<th colId="ips" sortable>IP Addresses </th>
<th colId="location" sortable>Location </th>
</tr>
</thead>
<div class="summary-list" onos-fixed-header>
<tbody>
<tr ng-hide="ctrl.tableData.length">
<td class="nodata" colspan="6">
No Hosts found
</td>
</tr>
<div class="table-header"
onos-sortable-header sort-callback="sortCallback(requestParams)">
<table>
<tr>
<td colId="type" class="table-icon" sortable></td>
<td colId="id" sortable>Host ID </td>
<td colId="mac" sortable>MAC Address </td>
<td colId="vlan" sortable>VLAN ID </td>
<td colId="ips" sortable>IP Addresses </td>
<td colId="location" sortable>Location </td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr ng-hide="tableData.length" class="no-data ignore-width">
<td colspan="6">
No Hosts found
</td>
</tr>
<tr ng-repeat="host in tableData"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{host._iconid_type}}"></div>
</td>
<td>{{host.id}}</td>
<td>{{host.mac}}</td>
<td>{{host.vlan}}</td>
<td>{{host.ips}}</td>
<td>{{host.location}}</td>
</tr>
</table>
</div>
</div>
<tr ng-repeat="host in ctrl.tableData"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{host._iconid_type}}"></div>
</td>
<td>{{host.id}}</td>
<td>{{host.mac}}</td>
<td>{{host.vlan}}</td>
<td>{{host.ips}}</td>
<td>{{host.location}}</td>
</tr>
</tbody>
</table>
</div>
......
......@@ -27,7 +27,6 @@
function ($log, $scope, ts, tbs) {
tbs.buildTable({
self: this,
scope: $scope,
tag: 'host'
});
......@@ -37,7 +36,7 @@
ts.resetSortIcons();
$scope.sortCallback();
};
$log.log('OvHostCtrl has been created');
}]);
}());
......
......@@ -50,4 +50,5 @@
#ov-intent td.resources,
#ov-intent td.details {
padding-left: 36px;
opacity: 0.65;
}
......
......@@ -17,45 +17,51 @@
<!-- Intent partial HTML -->
<div id="ov-intent">
<div class="tabular-header">
<h2>Intents ({{ctrl.tableData.length}} total)</h2>
<h2>Intents ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh active"
icon icon-size="36" icon-id="refresh"
ng-click="refresh()"></div>
</div>
</div>
<table class="summary-list"
onos-fixed-header
onos-sortable-header
sort-callback="sortCallback(requestParams)">
<thead>
<tr>
<th colId="appId" sortable>Application ID </th>
<th colId="key" sortable>Key </th>
<th colId="type" sortable>Type </th>
<th colId="priority" sortable>Priority </th>
</tr>
</thead>
<tbody>
<tr ng-hide="ctrl.tableData.length">
<td class="nodata" colspan="4">
No Intents found
</td>
</tr>
<div class="summary-list" onos-fixed-header>
<div class="table-header"
onos-sortable-header sort-callback="sortCallback(requestParams)">
<table>
<tr>
<td colId="appId" sortable>Application ID </td>
<td colId="key" sortable>Key </td>
<td colId="type" sortable>Type </td>
<td colId="priority" sortable>Priority </td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr ng-hide="tableData.length" class="no-data ignore-width">
<td colspan="4">
No Intents found
</td>
</tr>
<tr ng-repeat-start="intent in tableData">
<td>{{intent.appId}}</td>
<td>{{intent.key}}</td>
<td>{{intent.type}}</td>
<td>{{intent.priority}}</td>
</tr>
<tr>
<td class="resources" colspan="4">{{intent.resources}}</td>
</tr>
<tr ng-repeat-end ng-repeat-done>
<td class="details" colspan="4">{{intent.details}}</td>
</tr>
</table>
</div>
</div>
<tr ng-repeat-start="intent in ctrl.tableData">
<td>{{intent.appId}}</td>
<td>{{intent.key}}</td>
<td>{{intent.type}}</td>
<td>{{intent.priority}}</td>
</tr>
<tr>
<td class="resources" colspan="4">{{intent.resources}}</td>
</tr>
<tr ng-repeat-end ng-repeat-done>
<td class="details" colspan="4">{{intent.details}}</td>
</tr>
</tbody>
</table>
</div>
......
......@@ -27,7 +27,6 @@
function ($log, $scope, ts, tbs) {
tbs.buildTable({
self: this,
scope: $scope,
tag: 'intent'
});
......
......@@ -17,7 +17,7 @@
<!-- Link partial HTML -->
<div id="ov-link">
<div class="tabular-header">
<h2>Links ({{ctrl.tableData.length}} total)</h2>
<h2>Links ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh active"
icon icon-size="36" icon-id="refresh"
......@@ -25,39 +25,44 @@
</div>
</div>
<table class="summary-list"
onos-fixed-header
onos-sortable-header
sort-callback="sortCallback(requestParams)">
<thead>
<tr>
<th colId="_iconid_state" class="table-icon" sortable></th>
<th colId="one" sortable>Port 1 </th>
<th colId="two" sortable>Port 2 </th>
<th colId="type" sortable>Type </th>
<th colId="direction" sortable>Direction </th>
<th colId="durable" sortable>Durable </th>
</tr>
</thead>
<div class="summary-list" onos-fixed-header>
<tbody>
<tr ng-hide="ctrl.tableData.length">
<td class="nodata" colspan="6">
No Links found
</td>
</tr>
<div class="table-header"
onos-sortable-header sort-callback="sortCallback(requestParams)">
<table>
<tr>
<td colId="_iconid_state" class="table-icon" sortable></td>
<td colId="one" sortable>Port 1 </td>
<td colId="two" sortable>Port 2 </td>
<td colId="type" sortable>Type </td>
<td colId="direction" sortable>Direction </td>
<td colId="durable" sortable>Durable </td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr ng-hide="tableData.length" class="no-data ignore-width">
<td colspan="6">
No Links found
</td>
</tr>
<tr ng-repeat="link in tableData"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{link._iconid_state}}"></div>
</td>
<td>{{link.one}}</td>
<td>{{link.two}}</td>
<td>{{link.type}}</td>
<td>{{link.direction}}</td>
<td>{{link.durable}}</td>
</tr>
</table>
</div>
</div>
<tr ng-repeat="link in ctrl.tableData"
ng-repeat-done>
<td class="table-icon">
<div icon icon-id="{{link._iconid_state}}"></div>
</td>
<td>{{link.one}}</td>
<td>{{link.two}}</td>
<td>{{link.type}}</td>
<td>{{link.direction}}</td>
<td>{{link.durable}}</td>
</tr>
</tbody>
</table>
</div>
......
......@@ -27,7 +27,6 @@
function ($log, $scope, ts, tbs) {
tbs.buildTable({
self: this,
scope: $scope,
tag: 'link'
});
......@@ -37,7 +36,7 @@
ts.resetSortIcons();
$scope.sortCallback();
};
$log.log('OvLinkCtrl has been created');
}]);
}());
......
......@@ -20,71 +20,59 @@
describe('factory: fw/widget/table.js', function () {
var $log, $compile, $rootScope,
fs, ts, mast, is,
scope, compiled,
table, thead, tbody, mockHeader,
mockH2Height = 20,
mockMastHeight = 20,
mockHeaderHeight = mockH2Height + mockMastHeight,
tableIconTdSize = 100,
numTestElems = 4;
scope,
containerDiv,
headerDiv, bodyDiv,
header, body,
mockHeader,
mockHeaderHeight = 40;
var onosFixedHeaderTags =
'<table onos-fixed-header>' +
'<thead style="height:27px;">' +
'<tr>' +
'<th></th>' +
'<th>Device ID </th>' +
'<th col-width="100px">H/W Version </th>' +
'<th>S/W Version </th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<tr>' +
'<td colspan="4">' +
'No Devices found' +
'</td>' +
'</tr>' +
'<tr>' +
'<td class="table-icon">' +
'<div icon icon-id="{{dev._iconid_available}}">' +
'</div>' +
'</td>' +
'<td>Some ID</td>' +
'<td>Some HW</td>' +
'<td>Some Software</td>' +
'</tr>' +
'</tbody>' +
'</table>',
'<div class="summary-list" onos-fixed-header>' +
'<div class="table-header">' +
'<table>' +
'<tr>' +
'<td colId="type" class="table-icon"></td>' +
'<td colId="id">Host ID </td>' +
'<td colId="mac" sortable>MAC Address </td>' +
'<td colId="location" col-width="110px">Location </td>' +
'</tr>' +
'</table>' +
'</div>' +
'<div class="table-body">' +
'<table>' +
'<tr class="ignore-width">' +
'<td class="not-picked"></td>' +
'</tr>' +
'<tr>' +
'<td class="table-icon">Some Icon</td>' +
'<td>Some ID</td>' +
'<td>Some MAC Address</td>' +
'<td>Some Location</td>' +
'</tr>' +
'</table>' +
'</div>' +
'</div>',
onosSortableHeaderTags =
'<table onos-sortable-header ' +
'<div onos-sortable-header ' +
'sort-callback="sortCallback(requestParams)">' +
'<thead>' +
'<tr>' +
'<th colId="available"></th>' +
'<th colId="id" sortable>Device ID </th>' +
'<th colId="hw" sortable>H/W Version </th>' +
'<th colId="sw" sortable>S/W Version </th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<tr>' +
'<td>' +
'<div icon icon-id="{{dev._iconid_available}}">' +
'</div>' +
'</td>' +
'<td>Some ID</td>' +
'<td>Some HW</td>' +
'<td>Some Software</td>' +
'</tr>' +
'</tbody>' +
'</table>';
'<table>' +
'<tr>' +
'<td colId="type"></td>' +
'<td colId="id" sortable>Host ID </td>' +
'<td colId="mac" sortable>MAC Address </td>' +
'<td colId="location" sortable>Location </td>' +
'</tr>' +
'</table>' +
'</div>';
beforeEach(module('onosWidget', 'onosUtil', 'onosMast', 'onosSvg'));
var mockWindow = {
innerWidth: 400,
innerHeight: 200,
innerWidth: 600,
innerHeight: 400,
navigator: {
userAgent: 'defaultUA'
},
......@@ -111,23 +99,44 @@ describe('factory: fw/widget/table.js', function () {
beforeEach(function () {
scope = $rootScope.$new();
scope.tableData = [];
});
// Note: dummy header so that d3 doesn't trip up.
// $compile has to be used on the directive tag element, so it can't
// be included in the tag strings declared above.
beforeEach(function () {
mockHeader = d3.select('body')
.append('h2')
.classed('tabular-header', true)
.style('height', mockHeaderHeight + 'px')
.html('Some Header');
.style({
height: mockHeaderHeight + 'px',
margin: 0,
padding: 0
})
.text('Some Header');
});
afterEach(function () {
table = null;
thead = null;
tbody = null;
containerDiv = undefined;
headerDiv = undefined;
bodyDiv = undefined;
header = undefined;
body = undefined;
mockHeader.remove();
});
function populateTableData() {
scope.tableData = [
{
type: 'endstation',
id: '1234',
mac: '00:00:03',
location: 'USA'
}
];
}
it('should define TableBuilderService', function () {
expect(ts).toBeDefined();
});
......@@ -138,83 +147,119 @@ describe('factory: fw/widget/table.js', function () {
])).toBeTruthy();
});
function compileTable() {
compiled = $compile(table);
function compile(elem) {
var compiled = $compile(elem);
compiled(scope);
scope.$digest();
}
function verifyGivenTags(dirName) {
expect(table).toBeDefined();
expect(table.attr(dirName)).toBe('');
function selectTables() {
expect(containerDiv.find('div').length).toBe(2);
headerDiv = angular.element(containerDiv[0].querySelector('.table-header'));
expect(headerDiv.length).toBe(1);
bodyDiv = angular.element(containerDiv[0].querySelector('.table-body'));
expect(bodyDiv.length).toBe(1);
header = headerDiv.find('table');
expect(header.length).toBe(1);
body = bodyDiv.find('table');
expect(body.length).toBe(1);
}
thead = table.find('thead');
expect(thead).toBeDefined();
tbody = table.find('tbody');
expect(tbody).toBeDefined();
function verifyGivenTags(dirName, div) {
expect(div).toBeDefined();
expect(div.attr(dirName)).toBe('');
}
function verifyCssDisplay() {
var padding = 21, // bottom table constant 12 + mastPadding(?) 9
tableHeight = fs.windowSize(mockHeaderHeight).height -
(fs.noPx(table.find('thead').css('height')) + padding);
function verifyDefaultSize() {
expect(header.css('width')).toBe('570px');
expect(body.css('width')).toBe('570px');
}
expect(thead.css('display')).toBe('block');
expect(tbody.css('display')).toBe('block');
expect(tbody.css('height')).toBe(tableHeight + 'px');
expect(tbody.css('overflow')).toBe('auto');
function verifyHeight() {
var padding = 12,
mastHeight = 36,
tableHeight = (mockWindow.innerHeight - mockHeaderHeight) -
(fs.noPx(headerDiv.css('height')) + mastHeight + padding);
// TODO: investigate why math for calculating the height works better
// in the browser window (thead height is 0 in this test?)
expect(bodyDiv.css('height')).toBe(tableHeight + 'px');
}
function verifyColWidth() {
var winWidth = fs.windowSize().width,
colWidth, thElems, tr, tdElem;
colWidth = Math.floor(winWidth / numTestElems);
thElems = thead.find('th');
angular.forEach(thElems, function (thElem, i) {
thElem = angular.element(thElems[i]);
tr = angular.element(tbody.find('tr').eq(1));
tdElem = angular.element(tr.find('td').eq(i));
var custWidth = thElem.attr('col-width');
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 {
expect(thElem.css('width')).toBe(colWidth + 'px');
expect(tdElem.css('width')).toBe(colWidth + 'px');
}
});
var hdrs = header.find('td'),
cols = body.find('td');
expect(angular.element(hdrs[0]).css('width')).toBe('33px');
expect(angular.element(hdrs[3]).css('width')).toBe('110px');
expect(angular.element(cols[1]).css('width')).toBe('33px');
expect(angular.element(cols[4]).css('width')).toBe('110px');
}
function verifyCallbacks(thElems) {
function verifyCallbacks(h) {
expect(scope.sortCallback).not.toHaveBeenCalled();
// first test header has no 'sortable' attr
thElems[0].click();
h[0].click();
expect(scope.sortCallback).not.toHaveBeenCalled();
// the other headers have 'sortable'
for(var i = 1; i < numTestElems; i += 1) {
thElems[i].click();
expect(scope.sortCallback).toHaveBeenCalled();
}
h[1].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'id',
sortDir: 'asc'
});
h[1].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'id',
sortDir: 'desc'
});
h[1].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'id',
sortDir: 'asc'
});
h[2].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'mac',
sortDir: 'asc'
});
h[2].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'mac',
sortDir: 'desc'
});
h[2].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'mac',
sortDir: 'asc'
});
h[3].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'location',
sortDir: 'asc'
});
h[3].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'location',
sortDir: 'desc'
});
h[3].click();
expect(scope.sortCallback).toHaveBeenCalledWith({
sortCol: 'location',
sortDir: 'asc'
});
}
function verifyIcons(thElems) {
var currentTh, div;
// make sure it has the correct icon after clicking
thElems[1].click();
currentTh = angular.element(thElems[1]);
div = currentTh.find('div');
function verifyIcons(h) {
var currH, div;
h[1].click();
currH = angular.element(h[1]);
div = currH.find('div');
expect(div.html()).toBe(
'<svg class="embeddedIcon" width="10" height="10" viewBox="0 0 ' +
'50 50"><g class="icon upArrow"><rect width="50" height="50" ' +
......@@ -222,8 +267,8 @@ describe('factory: fw/widget/table.js', function () {
'xlink="http://www.w3.org/1999/xlink" xlink:href="#triangleUp">' +
'</use></g></svg>'
);
thElems[1].click();
div = currentTh.find('div');
h[1].click();
div = currH.find('div');
expect(div.html()).toBe(
'<svg class="embeddedIcon" width="10" height="10" viewBox="0 0 ' +
'50 50"><g class="icon downArrow"><rect width="50" height="50" ' +
......@@ -232,14 +277,14 @@ describe('factory: fw/widget/table.js', function () {
'</use></g></svg>'
);
thElems[2].click();
div = currentTh.children();
h[2].click();
div = currH.children();
// clicked on a new element, so the previous icon should have been deleted
expect(div.html()).toBeFalsy();
// the new element should have the ascending icon
currentTh = angular.element(thElems[2]);
div = currentTh.children();
currH = angular.element(h[2]);
div = currH.children();
expect(div.html()).toBe(
'<svg class="embeddedIcon" width="10" height="10" viewBox="0 0 ' +
'50 50"><g class="icon upArrow"><rect width="50" height="50" ' +
......@@ -250,31 +295,42 @@ describe('factory: fw/widget/table.js', function () {
}
it('should affirm that onos-fixed-header is working', function () {
table = angular.element(onosFixedHeaderTags);
containerDiv = angular.element(onosFixedHeaderTags);
compile(containerDiv);
compileTable();
verifyGivenTags('onos-fixed-header');
verifyGivenTags('onos-fixed-header', containerDiv);
selectTables();
verifyDefaultSize();
populateTableData();
// table will not be fixed unless it receives the 'LastElement' event
scope.$emit('LastElement');
scope.$digest();
verifyCssDisplay();
verifyHeight();
verifyColWidth();
mockWindow.innerHeight = 300;
scope.$digest();
verifyHeight();
mockWindow.innerWidth = 500;
scope.$digest();
verifyColWidth();
});
it('should affirm that onos-sortable-header is working', function () {
var thElems;
table = angular.element(onosSortableHeaderTags);
headerDiv = angular.element(onosSortableHeaderTags);
compileTable();
verifyGivenTags('onos-sortable-header');
compile(headerDiv);
verifyGivenTags('onos-sortable-header', headerDiv);
scope.sortCallback = jasmine.createSpy('sortCallback');
thElems = thead.find('th');
verifyCallbacks(thElems);
verifyIcons(thElems);
header = headerDiv.find('td');
verifyCallbacks(header);
verifyIcons(header);
});
// Note: testing resetSortIcons isn't feasible because due to the nature of
......
......@@ -48,7 +48,6 @@ describe('factory: fw/widget/tableBuilder.js', function () {
beforeEach(function () {
mockObj = {
self: {},
scope: $rootScope.$new(),
tag: 'foo',
selCb: mockSelCb
......@@ -78,10 +77,10 @@ describe('factory: fw/widget/tableBuilder.js', function () {
});
it('should set tableData', function () {
expect(mockObj.self.tableData).not.toBeDefined();
expect(mockObj.scope.tableData).not.toBeDefined();
tbs.buildTable(mockObj);
expect(fs.isA(mockObj.self.tableData)).toBeTruthy();
expect(mockObj.self.tableData.length).toBe(0);
expect(fs.isA(mockObj.scope.tableData)).toBeTruthy();
expect(mockObj.scope.tableData.length).toBe(0);
});
it('should unbind handlers on destroyed scope', function () {
......