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
Showing
21 changed files
with
278 additions
and
255 deletions
... | @@ -16,68 +16,80 @@ | ... | @@ -16,68 +16,80 @@ |
16 | 16 | ||
17 | /* ------ for summary-list tables ------ */ | 17 | /* ------ for summary-list tables ------ */ |
18 | 18 | ||
19 | -table.summary-list { | 19 | +div.summary-list { |
20 | - margin: 0 20px 16px 12px; | 20 | + margin: 0 20px 16px 10px; |
21 | font-size: 10pt; | 21 | font-size: 10pt; |
22 | border-spacing: 0; | 22 | border-spacing: 0; |
23 | } | 23 | } |
24 | 24 | ||
25 | -table.summary-list td.nodata { | 25 | +div.summary-list table { |
26 | + border-collapse: collapse; | ||
27 | + table-layout: fixed; | ||
28 | + empty-cells: show; | ||
29 | + margin: 0; | ||
30 | +} | ||
31 | + | ||
32 | +div.summary-list div.table-body { | ||
33 | + overflow-y: scroll; | ||
34 | +} | ||
35 | + | ||
36 | +div.summary-list tr.no-data td { | ||
26 | text-align: center; | 37 | text-align: center; |
27 | font-style: italic; | 38 | font-style: italic; |
28 | } | 39 | } |
29 | 40 | ||
30 | -.light table.summary-list tr:nth-child(even) { | 41 | +.light div.summary-list tr:nth-child(even) { |
31 | background-color: #ddd; | 42 | background-color: #ddd; |
32 | } | 43 | } |
33 | -.light table.summary-list tr:nth-child(odd) { | 44 | +.light div.summary-list tr:nth-child(odd) { |
34 | background-color: #eee; | 45 | background-color: #eee; |
35 | } | 46 | } |
36 | -.dark table.summary-list tr:nth-child(even) { | 47 | +.dark div.summary-list tr:nth-child(even) { |
37 | background-color: #333; | 48 | background-color: #333; |
38 | } | 49 | } |
39 | -.dark table.summary-list tr:nth-child(odd) { | 50 | +.dark div.summary-list tr:nth-child(odd) { |
40 | background-color: #444; | 51 | background-color: #444; |
41 | } | 52 | } |
42 | 53 | ||
43 | -.light table.summary-list tr.selected { | 54 | +.light div.summary-list tr.selected { |
44 | background-color: deepskyblue; | 55 | background-color: deepskyblue; |
45 | } | 56 | } |
46 | 57 | ||
47 | -.dark table.summary-list tr.selected { | 58 | +.dark div.summary-list tr.selected { |
48 | background-color: #304860; | 59 | background-color: #304860; |
49 | } | 60 | } |
50 | 61 | ||
51 | -table.summary-list td, | 62 | +div.summary-list td { |
52 | -table.summary-list th { | ||
53 | padding: 6px; | 63 | padding: 6px; |
54 | text-align: left; | 64 | text-align: left; |
65 | + word-wrap: break-word; | ||
55 | } | 66 | } |
56 | 67 | ||
57 | -table.summary-list th { | 68 | +div.summary-list .table-header td { |
58 | letter-spacing: 0.02em; | 69 | letter-spacing: 0.02em; |
59 | cursor: pointer; | 70 | cursor: pointer; |
71 | + font-weight: bold; | ||
60 | } | 72 | } |
61 | -table.summary-list th:first-child { | 73 | +div.summary-list .table-header td:first-child { |
62 | border-radius: 8px 0 0 0; | 74 | border-radius: 8px 0 0 0; |
63 | } | 75 | } |
64 | -table.summary-list th:last-child { | 76 | +div.summary-list .table-header td:last-child { |
65 | border-radius: 0 8px 0 0; | 77 | border-radius: 0 8px 0 0; |
66 | } | 78 | } |
67 | 79 | ||
68 | -.light table.summary-list th { | 80 | +.light div.summary-list .table-header td { |
69 | background-color: #bbb; | 81 | background-color: #bbb; |
70 | } | 82 | } |
71 | -.dark table.summary-list th { | 83 | +.dark div.summary-list .table-header td { |
72 | background-color: #222; | 84 | background-color: #222; |
73 | color: #ccc; | 85 | color: #ccc; |
74 | } | 86 | } |
75 | 87 | ||
76 | /* rows are selectable */ | 88 | /* rows are selectable */ |
77 | -table.summary-list td { | 89 | +div.summary-list .table-body td { |
78 | cursor: pointer; | 90 | cursor: pointer; |
79 | } | 91 | } |
80 | 92 | ||
81 | -.dark table.summary-list td { | 93 | +.dark div.summary-list td { |
82 | color: #ccc; | 94 | color: #ccc; |
83 | } | 95 | } | ... | ... |
... | @@ -37,86 +37,68 @@ | ... | @@ -37,86 +37,68 @@ |
37 | prevCol = {}, | 37 | prevCol = {}, |
38 | sortIconAPI; | 38 | sortIconAPI; |
39 | 39 | ||
40 | - // Functions for creating a fixed header on a table (Angular Directive) | 40 | + // Functions for creating a scrolling table body with fixed table header |
41 | 41 | ||
42 | - function setElemWidth(elem, size) { | 42 | + function _width(elem, width) { |
43 | - elem.style('width', size + 'px') | 43 | + elem.style('width', width); |
44 | } | 44 | } |
45 | 45 | ||
46 | - function setColWidth(th, td, size) { | 46 | + function defaultSize(table, width) { |
47 | - setElemWidth(th, size); | 47 | + var thead = table.select('.table-header').select('table'), |
48 | - setElemWidth(td, size); | 48 | + tbody = table.select('.table-body').select('table'), |
49 | + wpx = width + 'px'; | ||
50 | + _width(thead, wpx); | ||
51 | + _width(tbody, wpx); | ||
49 | } | 52 | } |
50 | 53 | ||
51 | - // count number of headers of | 54 | + function adjustTable(table, width, height) { |
52 | - // - assigned width, | 55 | + var thead = table.select('.table-header').select('table'), |
53 | - // - icon width, | 56 | + tbodyDiv = table.select('.table-body'), |
54 | - // - and default width | 57 | + tbody = tbodyDiv.select('table'), |
55 | - // assumes assigned width is not given to icons | 58 | + cstmWidths = {}; |
56 | - // returns the width of all columns that are not icons | ||
57 | - // or have an assigned width | ||
58 | - function getDefaultWidth(headers) { | ||
59 | - var winWidth = fs.windowSize().width, | ||
60 | - iconCols = 0, | ||
61 | - regCols = 0, | ||
62 | - cstmColWidth = 0; | ||
63 | 59 | ||
64 | - headers.each(function (d, i) { | 60 | + function findCstmWidths() { |
65 | - var thElement = d3.select(this), | 61 | + var headers = thead.selectAll('td'); |
66 | - cstmWidth = thElement.attr(colWidth); | ||
67 | 62 | ||
68 | - if (cstmWidth) { | 63 | + headers.each(function (d, i) { |
69 | - cstmColWidth += fs.noPx(cstmWidth); | 64 | + var h = d3.select(this), |
70 | - } else if (thElement.classed(tableIcon)) { | 65 | + index = i.toString(); |
71 | - iconCols += 1; | 66 | + if (h.classed(tableIcon)) { |
72 | - } else { | 67 | + cstmWidths[index] = tableIconTdSize + 'px'; |
73 | - regCols += 1; | 68 | + } |
69 | + if (h.attr(colWidth)) { | ||
70 | + cstmWidths[index] = h.attr(colWidth); | ||
74 | } | 71 | } |
75 | }); | 72 | }); |
76 | - | 73 | + $log.debug('Headers with custom widths: ', cstmWidths); |
77 | - return Math.floor((winWidth - cstmColWidth - | ||
78 | - (iconCols * tableIconTdSize)) / regCols); | ||
79 | } | 74 | } |
80 | 75 | ||
81 | - function setTableWidth(t) { | 76 | + function setTdWidths(elem) { |
82 | - var tHeaders = t.selectAll('th'), | 77 | + var tds = elem.selectAll('tr:not(.ignore-width)').selectAll('td'); |
83 | - defaultColWidth = getDefaultWidth(tHeaders); | 78 | + _width(elem, width + 'px'); |
84 | - | ||
85 | - tHeaders.each(function (d, i) { | ||
86 | - var thElement = d3.select(this), | ||
87 | - tr = t.select('tr:nth-of-type(2)'), | ||
88 | - tdElement = tr.select('td:nth-of-type(' + (i + 1) + ')'), | ||
89 | - custWidth = thElement.attr(colWidth); | ||
90 | 79 | ||
91 | - if (custWidth) { | 80 | + tds.each(function (d, i) { |
92 | - setColWidth(thElement, tdElement, fs.noPx(custWidth)); | 81 | + var td = d3.select(this), |
93 | - } else if (thElement.classed(tableIcon)) { | 82 | + index = i.toString(); |
94 | - setColWidth(thElement, tdElement, tableIconTdSize); | 83 | + if (cstmWidths.hasOwnProperty(index)) { |
95 | - } else { | 84 | + _width(td, cstmWidths[index]); |
96 | - setColWidth(thElement, tdElement, defaultColWidth); | ||
97 | } | 85 | } |
98 | }); | 86 | }); |
99 | } | 87 | } |
100 | 88 | ||
101 | - // get the size of the window and then subtract the extra space at the top | 89 | + function setHeight(body) { |
102 | - // to get the height of the table | 90 | + var h = height - (mast.mastHeight() + |
103 | - function setTableHeight(thead, tbody) { | 91 | + fs.noPxStyle(d3.select('.tabular-header'), 'height') + |
104 | - var ttlHgt = fs.noPxStyle(d3.select('.tabular-header'), 'height'), | 92 | + fs.noPxStyle(thead, 'height') + pdg); |
105 | - thHgt = fs.noPxStyle(thead, 'height'), | 93 | + body.style('height', h + 'px'); |
106 | - totalHgt = ttlHgt + thHgt + pdg, | ||
107 | - tbleHgt = fs.windowSize(mast.mastHeight() + totalHgt).height; | ||
108 | - | ||
109 | - thead.style('display', 'block'); | ||
110 | - tbody.style({ | ||
111 | - display: 'block', | ||
112 | - height: tbleHgt + 'px', | ||
113 | - overflow: 'auto' | ||
114 | - }); | ||
115 | } | 94 | } |
116 | 95 | ||
117 | - function fixTable(t, th, tb) { | 96 | + findCstmWidths(); |
118 | - setTableWidth(t); | 97 | + setTdWidths(thead); |
119 | - setTableHeight(th, tb); | 98 | + setTdWidths(tbody); |
99 | + setHeight(tbodyDiv); | ||
100 | + | ||
101 | + cstmWidths = {}; | ||
120 | } | 102 | } |
121 | 103 | ||
122 | // Functions for sorting table rows by header | 104 | // Functions for sorting table rows by header |
... | @@ -163,40 +145,42 @@ | ... | @@ -163,40 +145,42 @@ |
163 | } | 145 | } |
164 | 146 | ||
165 | angular.module('onosWidget') | 147 | angular.module('onosWidget') |
166 | - .directive('onosFixedHeader', ['$window', 'FnService', 'MastService', | 148 | + .directive('onosFixedHeader', ['$log','$window', |
167 | - function (_$window_, _fs_, _mast_) { | 149 | + 'FnService', 'MastService', |
150 | + | ||
151 | + function (_$log_, _$window_, _fs_, _mast_) { | ||
168 | return function (scope, element) { | 152 | return function (scope, element) { |
153 | + $log = _$log_; | ||
169 | $window = _$window_; | 154 | $window = _$window_; |
170 | fs = _fs_; | 155 | fs = _fs_; |
171 | mast = _mast_; | 156 | mast = _mast_; |
172 | 157 | ||
173 | var w = angular.element($window), | 158 | var w = angular.element($window), |
174 | table = d3.select(element[0]), | 159 | table = d3.select(element[0]), |
175 | - thead = table.select('thead'), | ||
176 | - tbody = table.select('tbody'), | ||
177 | canAdjust = false; | 160 | canAdjust = false; |
178 | 161 | ||
179 | scope.$watch(function () { | 162 | scope.$watch(function () { |
180 | return { | 163 | return { |
181 | - h: window.innerHeight, | 164 | + h: $window.innerHeight, |
182 | - w: window.innerWidth | 165 | + w: $window.innerWidth |
183 | }; | 166 | }; |
184 | - }, function (newVal) { | 167 | + }, function () { |
185 | - var wsz = fs.windowSize(0, 30); | 168 | + var wsz = fs.windowSize(0, 30), |
186 | - scope.windowHeight = newVal.h; | 169 | + wWidth = wsz.width, |
187 | - scope.windowWidth = newVal.w; | 170 | + wHeight = wsz.height; |
188 | 171 | ||
189 | - // default table size in case no data elements | 172 | + if (!scope.tableData.length) { |
190 | - table.style('width', wsz.width + 'px'); | 173 | + defaultSize(table, wWidth); |
174 | + } | ||
191 | 175 | ||
192 | scope.$on('LastElement', function () { | 176 | scope.$on('LastElement', function () { |
193 | // only adjust the table once it's completely loaded | 177 | // only adjust the table once it's completely loaded |
194 | - fixTable(table, thead, tbody); | 178 | + adjustTable(table, wWidth, wHeight); |
195 | canAdjust = true; | 179 | canAdjust = true; |
196 | }); | 180 | }); |
197 | 181 | ||
198 | if (canAdjust) { | 182 | if (canAdjust) { |
199 | - fixTable(table, thead, tbody); | 183 | + adjustTable(table, wWidth, wHeight); |
200 | } | 184 | } |
201 | }, true); | 185 | }, true); |
202 | 186 | ||
... | @@ -215,16 +199,16 @@ | ... | @@ -215,16 +199,16 @@ |
215 | link: function (scope, element) { | 199 | link: function (scope, element) { |
216 | $log = _$log_; | 200 | $log = _$log_; |
217 | is = _is_; | 201 | is = _is_; |
218 | - var table = d3.select(element[0]); | 202 | + var header = d3.select(element[0]); |
219 | sortIconAPI = is.sortIcons(); | 203 | sortIconAPI = is.sortIcons(); |
220 | 204 | ||
221 | - // when a header is clicked, change its icon tag | 205 | + // when a header is clicked, change its sort direction |
222 | // and get sorting order to send to the server. | 206 | // and get sorting order to send to the server. |
223 | - table.selectAll('th').on('click', function () { | 207 | + header.selectAll('td').on('click', function () { |
224 | - var thElem = d3.select(this); | 208 | + var col = d3.select(this); |
225 | 209 | ||
226 | - if (thElem.attr('sortable') === '') { | 210 | + if (col.attr('sortable') === '') { |
227 | - updateSortDirection(thElem); | 211 | + updateSortDirection(col); |
228 | scope.ctrlCallback({ | 212 | scope.ctrlCallback({ |
229 | requestParams: sortRequestParams() | 213 | requestParams: sortRequestParams() |
230 | }); | 214 | }); |
... | @@ -234,9 +218,9 @@ | ... | @@ -234,9 +218,9 @@ |
234 | }; | 218 | }; |
235 | }]) | 219 | }]) |
236 | 220 | ||
237 | - .factory('TableService', ['$log', 'IconService', | 221 | + .factory('TableService', ['IconService', |
238 | 222 | ||
239 | - function ($log, is) { | 223 | + function (is) { |
240 | sortIconAPI = is.sortIcons(); | 224 | sortIconAPI = is.sortIcons(); |
241 | 225 | ||
242 | return { | 226 | return { | ... | ... |
... | @@ -25,7 +25,6 @@ | ... | @@ -25,7 +25,6 @@ |
25 | 25 | ||
26 | // example params to buildTable: | 26 | // example params to buildTable: |
27 | // { | 27 | // { |
28 | - // self: this, <- controller object | ||
29 | // scope: $scope, <- controller scope | 28 | // scope: $scope, <- controller scope |
30 | // tag: 'device', <- table identifier | 29 | // tag: 'device', <- table identifier |
31 | // selCb: selCb <- row selection callback (optional) | 30 | // selCb: selCb <- row selection callback (optional) |
... | @@ -43,10 +42,10 @@ | ... | @@ -43,10 +42,10 @@ |
43 | resp = o.tag + 'DataResponse', | 42 | resp = o.tag + 'DataResponse', |
44 | onSel = fs.isF(o.selCb); | 43 | onSel = fs.isF(o.selCb); |
45 | 44 | ||
46 | - o.self.tableData = []; | 45 | + o.scope.tableData = []; |
47 | 46 | ||
48 | function respCb(data) { | 47 | function respCb(data) { |
49 | - o.self.tableData = data[root]; | 48 | + o.scope.tableData = data[root]; |
50 | o.scope.$apply(); | 49 | o.scope.$apply(); |
51 | } | 50 | } |
52 | 51 | ... | ... |
1 | <!-- app partial HTML --> | 1 | <!-- app partial HTML --> |
2 | <div id="ov-app"> | 2 | <div id="ov-app"> |
3 | <div class="tabular-header"> | 3 | <div class="tabular-header"> |
4 | - <h2>Applications ({{ctrl.tableData.length}} total)</h2> | 4 | + <h2>Applications ({{tableData.length}} total)</h2> |
5 | <div class="ctrl-btns"> | 5 | <div class="ctrl-btns"> |
6 | <div class="refresh active" | 6 | <div class="refresh active" |
7 | icon icon-size="36" icon-id="refresh" | 7 | icon icon-size="36" icon-id="refresh" |
... | @@ -19,28 +19,30 @@ | ... | @@ -19,28 +19,30 @@ |
19 | </form> | 19 | </form> |
20 | </div> | 20 | </div> |
21 | 21 | ||
22 | - <table class="summary-list" | 22 | + <div class="summary-list" onos-fixed-header> |
23 | - onos-fixed-header | 23 | + |
24 | - onos-sortable-header | 24 | + <div class="table-header" |
25 | - sort-callback="sortCallback(requestParams)"> | 25 | + onos-sortable-header sort-callback="sortCallback(requestParams)"> |
26 | - <thead> | 26 | + <table> |
27 | <tr> | 27 | <tr> |
28 | - <th colId="state" class="table-icon" sortable></th> | 28 | + <td colId="state" class="table-icon" sortable></td> |
29 | - <th colId="id" sortable>App ID </th> | 29 | + <td colId="id" sortable>App ID </td> |
30 | - <th colId="version" sortable>Version </th> | 30 | + <td colId="version" sortable>Version </td> |
31 | - <th colId="origin" sortable>Origin </th> | 31 | + <td colId="origin" sortable>Origin </td> |
32 | - <th colId="desc" col-width="640px">Description </th> | 32 | + <td colId="desc" col-width="475px">Description </td> |
33 | </tr> | 33 | </tr> |
34 | - </thead> | 34 | + </table> |
35 | + </div> | ||
35 | 36 | ||
36 | - <tbody> | 37 | + <div class="table-body"> |
37 | - <tr ng-hide="ctrl.tableData.length"> | 38 | + <table> |
38 | - <td class="nodata" colspan="5"> | 39 | + <tr ng-hide="tableData.length" class="no-data ignore-width"> |
40 | + <td colspan="5"> | ||
39 | No Applications found | 41 | No Applications found |
40 | </td> | 42 | </td> |
41 | </tr> | 43 | </tr> |
42 | 44 | ||
43 | - <tr ng-repeat="app in ctrl.tableData" | 45 | + <tr ng-repeat="app in tableData" |
44 | ng-click="selectCallback($event, app)" | 46 | ng-click="selectCallback($event, app)" |
45 | ng-class="{selected: app === sel}" | 47 | ng-class="{selected: app === sel}" |
46 | ng-repeat-done> | 48 | ng-repeat-done> |
... | @@ -52,6 +54,9 @@ | ... | @@ -52,6 +54,9 @@ |
52 | <td>{{app.origin}}</td> | 54 | <td>{{app.origin}}</td> |
53 | <td>{{app.desc}}</td> | 55 | <td>{{app.desc}}</td> |
54 | </tr> | 56 | </tr> |
55 | - </tbody> | ||
56 | </table> | 57 | </table> |
58 | + </div> | ||
59 | + | ||
60 | + </div> | ||
61 | + | ||
57 | </div> | 62 | </div> | ... | ... |
... | @@ -71,7 +71,6 @@ | ... | @@ -71,7 +71,6 @@ |
71 | d3.select('#app-deactivate').on('click', function () { appAction('deactivate'); }); | 71 | d3.select('#app-deactivate').on('click', function () { appAction('deactivate'); }); |
72 | 72 | ||
73 | tbs.buildTable({ | 73 | tbs.buildTable({ |
74 | - self: this, | ||
75 | scope: $scope, | 74 | scope: $scope, |
76 | tag: 'app', | 75 | tag: 'app', |
77 | selCb: selCb | 76 | selCb: selCb | ... | ... |
... | @@ -17,7 +17,7 @@ | ... | @@ -17,7 +17,7 @@ |
17 | <!-- Cluster partial HTML --> | 17 | <!-- Cluster partial HTML --> |
18 | <div id="ov-cluster"> | 18 | <div id="ov-cluster"> |
19 | <div class="tabular-header"> | 19 | <div class="tabular-header"> |
20 | - <h2>Cluster Nodes ({{ctrl.tableData.length}} total)</h2> | 20 | + <h2>Cluster Nodes ({{tableData.length}} total)</h2> |
21 | <div class="ctrl-btns"> | 21 | <div class="ctrl-btns"> |
22 | <div class="refresh active" | 22 | <div class="refresh active" |
23 | icon icon-size="36" icon-id="refresh" | 23 | icon icon-size="36" icon-id="refresh" |
... | @@ -25,28 +25,30 @@ | ... | @@ -25,28 +25,30 @@ |
25 | </div> | 25 | </div> |
26 | </div> | 26 | </div> |
27 | 27 | ||
28 | - <table class="summary-list" | 28 | + <div class="summary-list" onos-fixed-header> |
29 | - onos-fixed-header | 29 | + |
30 | - onos-sortable-header | 30 | + <div class="table-header" |
31 | - sort-callback="sortCallback(requestParams)"> | 31 | + onos-sortable-header sort-callback="sortCallback(requestParams)"> |
32 | - <thead> | 32 | + <table> |
33 | <tr> | 33 | <tr> |
34 | - <th colId="_iconid_state" class="table-icon" sortable></th> | 34 | + <td colId="_iconid_state" class="table-icon" sortable></td> |
35 | - <th colId="id" sortable>ID </th> | 35 | + <td colId="id" sortable>ID </td> |
36 | - <th colId="ip" sortable>IP Address </th> | 36 | + <td colId="ip" sortable>IP Address </td> |
37 | - <th colId="tcp" sortable>TCP Port </th> | 37 | + <td colId="tcp" sortable>TCP Port </td> |
38 | - <th colId="updated" sortable>Last Updated </th> | 38 | + <td colId="updated" sortable>Last Updated </td> |
39 | </tr> | 39 | </tr> |
40 | - </thead> | 40 | + </table> |
41 | + </div> | ||
41 | 42 | ||
42 | - <tbody> | 43 | + <div class="table-body"> |
43 | - <tr ng-hide="ctrl.tableData.length"> | 44 | + <table> |
44 | - <td class="nodata" colspan="5"> | 45 | + <tr ng-hide="tableData.length" class="no-data ignore-width"> |
46 | + <td colspan="5"> | ||
45 | No Cluster Nodes found | 47 | No Cluster Nodes found |
46 | </td> | 48 | </td> |
47 | </tr> | 49 | </tr> |
48 | 50 | ||
49 | - <tr ng-repeat="node in ctrl.tableData" | 51 | + <tr ng-repeat="node in tableData" |
50 | ng-repeat-done> | 52 | ng-repeat-done> |
51 | <td class="table-icon"> | 53 | <td class="table-icon"> |
52 | <div icon icon-id="{{node._iconid_state}}"></div> | 54 | <div icon icon-id="{{node._iconid_state}}"></div> |
... | @@ -56,6 +58,9 @@ | ... | @@ -56,6 +58,9 @@ |
56 | <td>{{node.tcp}}</td> | 58 | <td>{{node.tcp}}</td> |
57 | <td>{{node.updated}}</td> | 59 | <td>{{node.updated}}</td> |
58 | </tr> | 60 | </tr> |
59 | - </tbody> | ||
60 | </table> | 61 | </table> |
62 | + </div> | ||
63 | + | ||
64 | + </div> | ||
65 | + | ||
61 | </div> | 66 | </div> | ... | ... |
1 | <!-- Device partial HTML --> | 1 | <!-- Device partial HTML --> |
2 | <div id="ov-device"> | 2 | <div id="ov-device"> |
3 | <div class="tabular-header"> | 3 | <div class="tabular-header"> |
4 | - <h2>Devices ({{ctrl.tableData.length}} total)</h2> | 4 | + <h2>Devices ({{tableData.length}} total)</h2> |
5 | <div class="ctrl-btns"> | 5 | <div class="ctrl-btns"> |
6 | <div class="refresh active" | 6 | <div class="refresh active" |
7 | icon icon-size="36" icon-id="refresh" | 7 | icon icon-size="36" icon-id="refresh" |
... | @@ -9,32 +9,34 @@ | ... | @@ -9,32 +9,34 @@ |
9 | </div> | 9 | </div> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | - <table class="summary-list" | 12 | + <div class="summary-list" onos-fixed-header> |
13 | - onos-fixed-header | 13 | + |
14 | - onos-sortable-header | 14 | + <div class="table-header" |
15 | - sort-callback="sortCallback(requestParams)"> | 15 | + onos-sortable-header sort-callback="sortCallback(requestParams)"> |
16 | - <thead> | 16 | + <table> |
17 | <tr> | 17 | <tr> |
18 | - <th colId="available" class="table-icon" sortable></th> | 18 | + <td colId="available" class="table-icon" sortable></td> |
19 | - <th colId="type" class="table-icon" sortable></th> | 19 | + <td colId="type" class="table-icon" sortable></td> |
20 | - <th colId="id" sortable>Device ID </th> | 20 | + <td colId="id" sortable>Device ID </td> |
21 | - <th colId="masterid" sortable>Master Instance </th> | 21 | + <td colId="masterid" sortable>Master Instance </td> |
22 | - <th colId="num_ports" sortable>Ports </th> | 22 | + <td colId="num_ports" sortable>Ports </td> |
23 | - <th colId="mfr" sortable>Vendor </th> | 23 | + <td colId="mfr" sortable>Vendor </td> |
24 | - <th colId="hw" sortable>H/W Version </th> | 24 | + <td colId="hw" sortable>H/W Version </td> |
25 | - <th colId="sw" sortable>S/W Version </th> | 25 | + <td colId="sw" sortable>S/W Version </td> |
26 | - <th colId="protocol" sortable>Protocol </th> | 26 | + <td colId="protocol" sortable>Protocol </td> |
27 | </tr> | 27 | </tr> |
28 | - </thead> | 28 | + </table> |
29 | + </div> | ||
29 | 30 | ||
30 | - <tbody> | 31 | + <div class="table-body"> |
31 | - <tr ng-hide="ctrl.tableData.length"> | 32 | + <table> |
32 | - <td class="nodata" colspan="9"> | 33 | + <tr ng-hide="tableData.length" class="no-data ignore-width"> |
34 | + <td colspan="9"> | ||
33 | No Devices found | 35 | No Devices found |
34 | </td> | 36 | </td> |
35 | </tr> | 37 | </tr> |
36 | 38 | ||
37 | - <tr ng-repeat="dev in ctrl.tableData" | 39 | + <tr ng-repeat="dev in tableData" |
38 | ng-click="selectCallback($event, dev)" | 40 | ng-click="selectCallback($event, dev)" |
39 | ng-class="{selected: dev === sel}" | 41 | ng-class="{selected: dev === sel}" |
40 | ng-repeat-done> | 42 | ng-repeat-done> |
... | @@ -52,6 +54,9 @@ | ... | @@ -52,6 +54,9 @@ |
52 | <td>{{dev.sw}}</td> | 54 | <td>{{dev.sw}}</td> |
53 | <td>{{dev.protocol}}</td> | 55 | <td>{{dev.protocol}}</td> |
54 | </tr> | 56 | </tr> |
55 | - </tbody> | ||
56 | </table> | 57 | </table> |
58 | + </div> | ||
59 | + | ||
60 | + </div> | ||
61 | + | ||
57 | </div> | 62 | </div> | ... | ... |
... | @@ -25,8 +25,7 @@ | ... | @@ -25,8 +25,7 @@ |
25 | var $log, $scope, fs, mast, ps, wss, is, bns, ns, ttip; | 25 | var $log, $scope, fs, mast, ps, wss, is, bns, ns, ttip; |
26 | 26 | ||
27 | // internal state | 27 | // internal state |
28 | - var self, | 28 | + var detailsPanel, |
29 | - detailsPanel, | ||
30 | pStartY, pHeight, | 29 | pStartY, pHeight, |
31 | top, bottom, iconDiv, | 30 | top, bottom, iconDiv, |
32 | wSize, selRow; | 31 | wSize, selRow; |
... | @@ -183,8 +182,8 @@ | ... | @@ -183,8 +182,8 @@ |
183 | } | 182 | } |
184 | 183 | ||
185 | function respDetailsCb(data) { | 184 | function respDetailsCb(data) { |
186 | - self.panelData = data.details; | 185 | + $scope.panelData = data.details; |
187 | - populateDetails(self.panelData); | 186 | + populateDetails($scope.panelData); |
188 | detailsPanel.show(); | 187 | detailsPanel.show(); |
189 | } | 188 | } |
190 | 189 | ||
... | @@ -219,9 +218,8 @@ | ... | @@ -219,9 +218,8 @@ |
219 | bns = _bns_; | 218 | bns = _bns_; |
220 | ns = _ns_; | 219 | ns = _ns_; |
221 | ttip = _ttip_; | 220 | ttip = _ttip_; |
222 | - self = this; | ||
223 | var handlers = {}; | 221 | var handlers = {}; |
224 | - self.panelData = []; | 222 | + $scope.panelData = []; |
225 | pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height') | 223 | pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height') |
226 | + mast.mastHeight() + topPdg; | 224 | + mast.mastHeight() + topPdg; |
227 | wSize = fs.windowSize(pStartY); | 225 | wSize = fs.windowSize(pStartY); |
... | @@ -238,7 +236,6 @@ | ... | @@ -238,7 +236,6 @@ |
238 | } | 236 | } |
239 | 237 | ||
240 | tbs.buildTable({ | 238 | tbs.buildTable({ |
241 | - self: self, | ||
242 | scope: $scope, | 239 | scope: $scope, |
243 | tag: 'device', | 240 | tag: 'device', |
244 | selCb: selCb | 241 | selCb: selCb | ... | ... |
... | @@ -2,8 +2,8 @@ | ... | @@ -2,8 +2,8 @@ |
2 | <div id="ov-flow"> | 2 | <div id="ov-flow"> |
3 | <div class="tabular-header"> | 3 | <div class="tabular-header"> |
4 | <h2> | 4 | <h2> |
5 | - Flows for Device {{ctrl.devId || "(No device selected)"}} | 5 | + Flows for Device {{devId || "(No device selected)"}} |
6 | - ({{ctrl.tableData.length}} total) | 6 | + ({{tableData.length}} total) |
7 | </h2> | 7 | </h2> |
8 | <div class="ctrl-btns"> | 8 | <div class="ctrl-btns"> |
9 | <div class="refresh active" | 9 | <div class="refresh active" |
... | @@ -12,31 +12,33 @@ | ... | @@ -12,31 +12,33 @@ |
12 | </div> | 12 | </div> |
13 | </div> | 13 | </div> |
14 | 14 | ||
15 | - <table class="summary-list" | 15 | + <div class="summary-list" onos-fixed-header> |
16 | - onos-fixed-header | 16 | + |
17 | - onos-sortable-header | 17 | + <div class="table-header" |
18 | - sort-callback="sortCallback(requestParams)"> | 18 | + onos-sortable-header sort-callback="sortCallback(requestParams)"> |
19 | - <thead> | 19 | + <table> |
20 | <tr> | 20 | <tr> |
21 | - <th colId="id" sortable>Flow ID </th> | 21 | + <td colId="id" col-width="180px" sortable>Flow ID </td> |
22 | - <th colId="appId" sortable>App ID </th> | 22 | + <td colId="appId" sortable>App ID </td> |
23 | - <th colId="groupId" sortable>Group ID </th> | 23 | + <td colId="groupId" sortable>Group ID </td> |
24 | - <th colId="tableId" sortable>Table ID </th> | 24 | + <td colId="tableId" sortable>Table ID </td> |
25 | - <th colId="priority" sortable>Priority </th> | 25 | + <td colId="priority" sortable>Priority </td> |
26 | - <th colId="timeout" sortable>Timeout </th> | 26 | + <td colId="timeout" sortable>Timeout </td> |
27 | - <th colId="permanent" sortable>Permanent </th> | 27 | + <td colId="permanent" sortable>Permanent </td> |
28 | - <th colId="state" sortable>State </th> | 28 | + <td colId="state" sortable>State </td> |
29 | </tr> | 29 | </tr> |
30 | - </thead> | 30 | + </table> |
31 | + </div> | ||
31 | 32 | ||
32 | - <tbody> | 33 | + <div class="table-body"> |
33 | - <tr ng-hide="ctrl.tableData.length"> | 34 | + <table> |
34 | - <td class="nodata" colspan="8"> | 35 | + <tr ng-hide="tableData.length" class="no-data ignore-width"> |
36 | + <td colspan="8"> | ||
35 | No Flows found | 37 | No Flows found |
36 | </td> | 38 | </td> |
37 | </tr> | 39 | </tr> |
38 | 40 | ||
39 | - <tr ng-repeat-start="flow in ctrl.tableData"> | 41 | + <tr ng-repeat-start="flow in tableData"> |
40 | <td>{{flow.id}}</td> | 42 | <td>{{flow.id}}</td> |
41 | <td>{{flow.appId}}</td> | 43 | <td>{{flow.appId}}</td> |
42 | <td>{{flow.groupId}}</td> | 44 | <td>{{flow.groupId}}</td> |
... | @@ -46,12 +48,16 @@ | ... | @@ -46,12 +48,16 @@ |
46 | <td>{{flow.permanent}}</td> | 48 | <td>{{flow.permanent}}</td> |
47 | <td>{{flow.state}}</td> | 49 | <td>{{flow.state}}</td> |
48 | </tr> | 50 | </tr> |
49 | - <tr> | 51 | + <tr class="ignore-width"> |
50 | <td class="selector" colspan="8">{{flow.selector}}</td> | 52 | <td class="selector" colspan="8">{{flow.selector}}</td> |
51 | </tr> | 53 | </tr> |
52 | - <tr ng-repeat-end ng-repeat-done> | 54 | + <tr class="ignore-width" |
55 | + ng-repeat-end ng-repeat-done> | ||
53 | <td class="treatment" colspan="8">{{flow.treatment}}</td> | 56 | <td class="treatment" colspan="8">{{flow.treatment}}</td> |
54 | </tr> | 57 | </tr> |
55 | - </tbody> | ||
56 | </table> | 58 | </table> |
59 | + </div> | ||
60 | + | ||
61 | + </div> | ||
62 | + | ||
57 | </div> | 63 | </div> | ... | ... |
... | @@ -30,8 +30,7 @@ | ... | @@ -30,8 +30,7 @@ |
30 | 'FnService', 'TableService', 'TableBuilderService', | 30 | 'FnService', 'TableService', 'TableBuilderService', |
31 | 31 | ||
32 | function (_$log_, _$scope_, _$location_, _fs_, _ts_, _tbs_) { | 32 | function (_$log_, _$scope_, _$location_, _fs_, _ts_, _tbs_) { |
33 | - var self = this, | 33 | + var params; |
34 | - params; | ||
35 | $log = _$log_; | 34 | $log = _$log_; |
36 | $scope = _$scope_; | 35 | $scope = _$scope_; |
37 | $location = _$location_; | 36 | $location = _$location_; |
... | @@ -41,11 +40,10 @@ | ... | @@ -41,11 +40,10 @@ |
41 | 40 | ||
42 | params = $location.search(); | 41 | params = $location.search(); |
43 | if (params.hasOwnProperty('devId')) { | 42 | if (params.hasOwnProperty('devId')) { |
44 | - self.devId = params['devId']; | 43 | + $scope.devId = params['devId']; |
45 | } | 44 | } |
46 | 45 | ||
47 | tbs.buildTable({ | 46 | tbs.buildTable({ |
48 | - self: self, | ||
49 | scope: $scope, | 47 | scope: $scope, |
50 | tag: 'flow', | 48 | tag: 'flow', |
51 | query: params | 49 | query: params | ... | ... |
1 | <!-- Host partial HTML --> | 1 | <!-- Host partial HTML --> |
2 | <div id="ov-host"> | 2 | <div id="ov-host"> |
3 | <div class="tabular-header"> | 3 | <div class="tabular-header"> |
4 | - <h2>Hosts ({{ctrl.tableData.length}} total)</h2> | 4 | + <h2>Hosts ({{tableData.length}} total)</h2> |
5 | <div class="ctrl-btns"> | 5 | <div class="ctrl-btns"> |
6 | <div class="refresh active" | 6 | <div class="refresh active" |
7 | icon icon-size="36" icon-id="refresh" | 7 | icon icon-size="36" icon-id="refresh" |
... | @@ -9,29 +9,31 @@ | ... | @@ -9,29 +9,31 @@ |
9 | </div> | 9 | </div> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | - <table class="summary-list" | 12 | + <div class="summary-list" onos-fixed-header> |
13 | - onos-fixed-header | 13 | + |
14 | - onos-sortable-header | 14 | + <div class="table-header" |
15 | - sort-callback="sortCallback(requestParams)"> | 15 | + onos-sortable-header sort-callback="sortCallback(requestParams)"> |
16 | - <thead> | 16 | + <table> |
17 | <tr> | 17 | <tr> |
18 | - <th colId="type" class="table-icon" sortable></th> | 18 | + <td colId="type" class="table-icon" sortable></td> |
19 | - <th colId="id" sortable>Host ID </th> | 19 | + <td colId="id" sortable>Host ID </td> |
20 | - <th colId="mac" sortable>MAC Address </th> | 20 | + <td colId="mac" sortable>MAC Address </td> |
21 | - <th colId="vlan" sortable>VLAN ID </th> | 21 | + <td colId="vlan" sortable>VLAN ID </td> |
22 | - <th colId="ips" sortable>IP Addresses </th> | 22 | + <td colId="ips" sortable>IP Addresses </td> |
23 | - <th colId="location" sortable>Location </th> | 23 | + <td colId="location" sortable>Location </td> |
24 | </tr> | 24 | </tr> |
25 | - </thead> | 25 | + </table> |
26 | + </div> | ||
26 | 27 | ||
27 | - <tbody> | 28 | + <div class="table-body"> |
28 | - <tr ng-hide="ctrl.tableData.length"> | 29 | + <table> |
29 | - <td class="nodata" colspan="6"> | 30 | + <tr ng-hide="tableData.length" class="no-data ignore-width"> |
31 | + <td colspan="6"> | ||
30 | No Hosts found | 32 | No Hosts found |
31 | </td> | 33 | </td> |
32 | </tr> | 34 | </tr> |
33 | 35 | ||
34 | - <tr ng-repeat="host in ctrl.tableData" | 36 | + <tr ng-repeat="host in tableData" |
35 | ng-repeat-done> | 37 | ng-repeat-done> |
36 | <td class="table-icon"> | 38 | <td class="table-icon"> |
37 | <div icon icon-id="{{host._iconid_type}}"></div> | 39 | <div icon icon-id="{{host._iconid_type}}"></div> |
... | @@ -42,6 +44,9 @@ | ... | @@ -42,6 +44,9 @@ |
42 | <td>{{host.ips}}</td> | 44 | <td>{{host.ips}}</td> |
43 | <td>{{host.location}}</td> | 45 | <td>{{host.location}}</td> |
44 | </tr> | 46 | </tr> |
45 | - </tbody> | ||
46 | </table> | 47 | </table> |
48 | + </div> | ||
49 | + | ||
50 | + </div> | ||
51 | + | ||
47 | </div> | 52 | </div> | ... | ... |
... | @@ -17,34 +17,37 @@ | ... | @@ -17,34 +17,37 @@ |
17 | <!-- Intent partial HTML --> | 17 | <!-- Intent partial HTML --> |
18 | <div id="ov-intent"> | 18 | <div id="ov-intent"> |
19 | <div class="tabular-header"> | 19 | <div class="tabular-header"> |
20 | - <h2>Intents ({{ctrl.tableData.length}} total)</h2> | 20 | + <h2>Intents ({{tableData.length}} total)</h2> |
21 | <div class="ctrl-btns"> | 21 | <div class="ctrl-btns"> |
22 | <div class="refresh active" | 22 | <div class="refresh active" |
23 | icon icon-size="36" icon-id="refresh" | 23 | icon icon-size="36" icon-id="refresh" |
24 | ng-click="refresh()"></div> | 24 | ng-click="refresh()"></div> |
25 | </div> | 25 | </div> |
26 | </div> | 26 | </div> |
27 | - <table class="summary-list" | 27 | + |
28 | - onos-fixed-header | 28 | + <div class="summary-list" onos-fixed-header> |
29 | - onos-sortable-header | 29 | + |
30 | - sort-callback="sortCallback(requestParams)"> | 30 | + <div class="table-header" |
31 | - <thead> | 31 | + onos-sortable-header sort-callback="sortCallback(requestParams)"> |
32 | + <table> | ||
32 | <tr> | 33 | <tr> |
33 | - <th colId="appId" sortable>Application ID </th> | 34 | + <td colId="appId" sortable>Application ID </td> |
34 | - <th colId="key" sortable>Key </th> | 35 | + <td colId="key" sortable>Key </td> |
35 | - <th colId="type" sortable>Type </th> | 36 | + <td colId="type" sortable>Type </td> |
36 | - <th colId="priority" sortable>Priority </th> | 37 | + <td colId="priority" sortable>Priority </td> |
37 | </tr> | 38 | </tr> |
38 | - </thead> | 39 | + </table> |
40 | + </div> | ||
39 | 41 | ||
40 | - <tbody> | 42 | + <div class="table-body"> |
41 | - <tr ng-hide="ctrl.tableData.length"> | 43 | + <table> |
42 | - <td class="nodata" colspan="4"> | 44 | + <tr ng-hide="tableData.length" class="no-data ignore-width"> |
45 | + <td colspan="4"> | ||
43 | No Intents found | 46 | No Intents found |
44 | </td> | 47 | </td> |
45 | </tr> | 48 | </tr> |
46 | 49 | ||
47 | - <tr ng-repeat-start="intent in ctrl.tableData"> | 50 | + <tr ng-repeat-start="intent in tableData"> |
48 | <td>{{intent.appId}}</td> | 51 | <td>{{intent.appId}}</td> |
49 | <td>{{intent.key}}</td> | 52 | <td>{{intent.key}}</td> |
50 | <td>{{intent.type}}</td> | 53 | <td>{{intent.type}}</td> |
... | @@ -56,6 +59,9 @@ | ... | @@ -56,6 +59,9 @@ |
56 | <tr ng-repeat-end ng-repeat-done> | 59 | <tr ng-repeat-end ng-repeat-done> |
57 | <td class="details" colspan="4">{{intent.details}}</td> | 60 | <td class="details" colspan="4">{{intent.details}}</td> |
58 | </tr> | 61 | </tr> |
59 | - </tbody> | ||
60 | </table> | 62 | </table> |
63 | + </div> | ||
64 | + | ||
65 | + </div> | ||
66 | + | ||
61 | </div> | 67 | </div> | ... | ... |
... | @@ -17,7 +17,7 @@ | ... | @@ -17,7 +17,7 @@ |
17 | <!-- Link partial HTML --> | 17 | <!-- Link partial HTML --> |
18 | <div id="ov-link"> | 18 | <div id="ov-link"> |
19 | <div class="tabular-header"> | 19 | <div class="tabular-header"> |
20 | - <h2>Links ({{ctrl.tableData.length}} total)</h2> | 20 | + <h2>Links ({{tableData.length}} total)</h2> |
21 | <div class="ctrl-btns"> | 21 | <div class="ctrl-btns"> |
22 | <div class="refresh active" | 22 | <div class="refresh active" |
23 | icon icon-size="36" icon-id="refresh" | 23 | icon icon-size="36" icon-id="refresh" |
... | @@ -25,29 +25,31 @@ | ... | @@ -25,29 +25,31 @@ |
25 | </div> | 25 | </div> |
26 | </div> | 26 | </div> |
27 | 27 | ||
28 | - <table class="summary-list" | 28 | + <div class="summary-list" onos-fixed-header> |
29 | - onos-fixed-header | 29 | + |
30 | - onos-sortable-header | 30 | + <div class="table-header" |
31 | - sort-callback="sortCallback(requestParams)"> | 31 | + onos-sortable-header sort-callback="sortCallback(requestParams)"> |
32 | - <thead> | 32 | + <table> |
33 | <tr> | 33 | <tr> |
34 | - <th colId="_iconid_state" class="table-icon" sortable></th> | 34 | + <td colId="_iconid_state" class="table-icon" sortable></td> |
35 | - <th colId="one" sortable>Port 1 </th> | 35 | + <td colId="one" sortable>Port 1 </td> |
36 | - <th colId="two" sortable>Port 2 </th> | 36 | + <td colId="two" sortable>Port 2 </td> |
37 | - <th colId="type" sortable>Type </th> | 37 | + <td colId="type" sortable>Type </td> |
38 | - <th colId="direction" sortable>Direction </th> | 38 | + <td colId="direction" sortable>Direction </td> |
39 | - <th colId="durable" sortable>Durable </th> | 39 | + <td colId="durable" sortable>Durable </td> |
40 | </tr> | 40 | </tr> |
41 | - </thead> | 41 | + </table> |
42 | + </div> | ||
42 | 43 | ||
43 | - <tbody> | 44 | + <div class="table-body"> |
44 | - <tr ng-hide="ctrl.tableData.length"> | 45 | + <table> |
45 | - <td class="nodata" colspan="6"> | 46 | + <tr ng-hide="tableData.length" class="no-data ignore-width"> |
47 | + <td colspan="6"> | ||
46 | No Links found | 48 | No Links found |
47 | </td> | 49 | </td> |
48 | </tr> | 50 | </tr> |
49 | 51 | ||
50 | - <tr ng-repeat="link in ctrl.tableData" | 52 | + <tr ng-repeat="link in tableData" |
51 | ng-repeat-done> | 53 | ng-repeat-done> |
52 | <td class="table-icon"> | 54 | <td class="table-icon"> |
53 | <div icon icon-id="{{link._iconid_state}}"></div> | 55 | <div icon icon-id="{{link._iconid_state}}"></div> |
... | @@ -58,6 +60,9 @@ | ... | @@ -58,6 +60,9 @@ |
58 | <td>{{link.direction}}</td> | 60 | <td>{{link.direction}}</td> |
59 | <td>{{link.durable}}</td> | 61 | <td>{{link.durable}}</td> |
60 | </tr> | 62 | </tr> |
61 | - </tbody> | ||
62 | </table> | 63 | </table> |
64 | + </div> | ||
65 | + | ||
66 | + </div> | ||
67 | + | ||
63 | </div> | 68 | </div> | ... | ... |
This diff is collapsed. Click to expand it.
... | @@ -48,7 +48,6 @@ describe('factory: fw/widget/tableBuilder.js', function () { | ... | @@ -48,7 +48,6 @@ describe('factory: fw/widget/tableBuilder.js', function () { |
48 | 48 | ||
49 | beforeEach(function () { | 49 | beforeEach(function () { |
50 | mockObj = { | 50 | mockObj = { |
51 | - self: {}, | ||
52 | scope: $rootScope.$new(), | 51 | scope: $rootScope.$new(), |
53 | tag: 'foo', | 52 | tag: 'foo', |
54 | selCb: mockSelCb | 53 | selCb: mockSelCb |
... | @@ -78,10 +77,10 @@ describe('factory: fw/widget/tableBuilder.js', function () { | ... | @@ -78,10 +77,10 @@ describe('factory: fw/widget/tableBuilder.js', function () { |
78 | }); | 77 | }); |
79 | 78 | ||
80 | it('should set tableData', function () { | 79 | it('should set tableData', function () { |
81 | - expect(mockObj.self.tableData).not.toBeDefined(); | 80 | + expect(mockObj.scope.tableData).not.toBeDefined(); |
82 | tbs.buildTable(mockObj); | 81 | tbs.buildTable(mockObj); |
83 | - expect(fs.isA(mockObj.self.tableData)).toBeTruthy(); | 82 | + expect(fs.isA(mockObj.scope.tableData)).toBeTruthy(); |
84 | - expect(mockObj.self.tableData.length).toBe(0); | 83 | + expect(mockObj.scope.tableData.length).toBe(0); |
85 | }); | 84 | }); |
86 | 85 | ||
87 | it('should unbind handlers on destroyed scope', function () { | 86 | it('should unbind handlers on destroyed scope', function () { | ... | ... |
-
Please register or login to post a comment