Yi Tseng | a87b40c | 2017-09-10 00:59:03 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2017-present Open Networking Foundation |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | /* |
| 18 | ONOS GUI -- Pipeconf View Module |
| 19 | */ |
| 20 | |
| 21 | (function () { |
| 22 | 'use strict'; |
| 23 | |
| 24 | // injected refs |
| 25 | var $log, $scope, $loc, $interval, $timeout, fs, ns, wss, ls, ps, mast, is, dps; |
| 26 | |
| 27 | // Constants |
| 28 | var pipeconfRequest = "pipeconfRequest", |
| 29 | pipeConfResponse = "pipeConfResponse", |
| 30 | noPipeconfResp = "noPipeconfResp", |
| 31 | invalidDevId = "invalidDevId", |
| 32 | pipeconf = "pipeconf", |
| 33 | pipelineModel = "pipelineModel", |
| 34 | devId = "devId", |
| 35 | topPdg = 28, |
| 36 | pName = 'pipeconf-detail-panel', |
| 37 | refreshRate = 5000; |
| 38 | |
| 39 | // For request handling |
| 40 | var handlers, |
| 41 | refreshPromise; |
| 42 | |
| 43 | // Details panel |
| 44 | var pWidth = 600, |
| 45 | pTopHeight = 111, |
| 46 | pStartY, |
| 47 | wSize, |
| 48 | pHeight, |
| 49 | detailsPanel; |
| 50 | |
| 51 | // create key bindings to handle panel |
| 52 | var keyBindings = { |
| 53 | esc: [closePanel, 'Close the details panel'], |
| 54 | _helpFormat: ['esc'], |
| 55 | }; |
| 56 | |
| 57 | function fetchPipeconfData() { |
| 58 | if ($scope.autoRefresh && wss.isConnected() && !ls.waiting()) { |
| 59 | ls.start(); |
| 60 | var requestData = { |
| 61 | devId: $scope.devId |
| 62 | }; |
| 63 | wss.sendEvent(pipeconfRequest, requestData); |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | function pipeConfRespCb(data) { |
| 68 | ls.stop(); |
| 69 | if (!data.hasOwnProperty(pipeconf)) { |
| 70 | $scope.pipeconf = null; |
| 71 | return; |
| 72 | } |
| 73 | $scope.pipeconf = data[pipeconf]; |
| 74 | $scope.pipelineModel = data[pipelineModel]; |
| 75 | $scope.$apply(); |
| 76 | } |
| 77 | |
| 78 | function noPipeconfRespCb(data) { |
| 79 | ls.stop(); |
| 80 | $scope.pipeconf = null; |
| 81 | $scope.pipelineModel = null; |
| 82 | $scope.$apply(); |
| 83 | } |
| 84 | |
| 85 | function viewDestroy() { |
| 86 | wss.unbindHandlers(handlers); |
| 87 | $interval.cancel(refreshPromise); |
| 88 | refreshPromise = null; |
| 89 | ls.stop(); |
| 90 | } |
| 91 | |
| 92 | function headerSelectCb($event, header) { |
| 93 | if ($scope.selectedId !== null && |
| 94 | $scope.selectedId.type === 'header' && |
| 95 | $scope.selectedId.name === header.name) { |
| 96 | |
| 97 | // Hide the panel when select same row |
| 98 | closePanel(); |
| 99 | return; |
| 100 | } |
| 101 | |
| 102 | $scope.selectedId = { |
| 103 | type: 'header', |
| 104 | name: header.name, |
| 105 | }; |
| 106 | |
| 107 | var subtitles = [ |
| 108 | { |
| 109 | label: 'Header Type: ', |
| 110 | value: header.type.name, |
| 111 | }, |
| 112 | { |
| 113 | label: 'Is metadata: ', |
| 114 | value: header.isMetadata, |
| 115 | }, |
| 116 | { |
| 117 | label: 'Index: ', |
| 118 | value: header.index, |
| 119 | }, |
| 120 | ]; |
| 121 | |
| 122 | var tables = [ |
| 123 | { |
| 124 | title: 'Fields', |
| 125 | headers: ['Name', 'Bit width'], |
| 126 | data: header.type.fields, |
| 127 | noDataText: 'No header fields' |
| 128 | }, |
| 129 | ]; |
| 130 | populateDetailPanel(header.name, subtitles, tables); |
| 131 | } |
| 132 | |
| 133 | function actionSelectCb($event, action) { |
| 134 | if ($scope.selectedId !== null && |
| 135 | $scope.selectedId.type === 'action' && |
| 136 | $scope.selectedId.name === action.name) { |
| 137 | |
| 138 | // Hide the panel when select same row |
| 139 | closePanel(); |
| 140 | return; |
| 141 | } |
| 142 | |
| 143 | $scope.selectedId = { |
| 144 | type: 'action', |
| 145 | name: action.name, |
| 146 | }; |
| 147 | |
| 148 | var subtitles = []; |
| 149 | var tables = [ |
| 150 | { |
| 151 | title: 'Parameters', |
| 152 | headers: ['Name', 'Bit width'], |
| 153 | data: action.params, |
| 154 | noDataText: 'No action parameters', |
| 155 | }, |
| 156 | ]; |
| 157 | |
| 158 | populateDetailPanel(action.name, subtitles, tables); |
| 159 | } |
| 160 | |
| 161 | function tableSelectCb($event, table) { |
| 162 | if ($scope.selectedId !== null && |
| 163 | $scope.selectedId.type === 'table' && |
| 164 | $scope.selectedId.name === table.name) { |
| 165 | |
| 166 | // Hide the panel when select same row |
| 167 | closePanel(); |
| 168 | return; |
| 169 | } |
| 170 | |
| 171 | $scope.selectedId = { |
| 172 | type: 'table', |
| 173 | name: table.name, |
| 174 | }; |
| 175 | |
| 176 | var subtitles = [ |
| 177 | { |
| 178 | label: 'Max Size: ', |
| 179 | value: table.maxSize, |
| 180 | }, |
| 181 | { |
| 182 | label: 'Has counters: ', |
| 183 | value: table.hasCounters, |
| 184 | }, |
| 185 | { |
| 186 | label: 'Support Aging: ', |
| 187 | value: table.supportAging, |
| 188 | }, |
| 189 | ]; |
| 190 | |
| 191 | var matchFields = table.matchFields.map(function(mp) { |
| 192 | return { |
Yi Tseng | e2a1500 | 2017-11-29 16:42:52 -0800 | [diff] [blame] | 193 | name: mp.field, |
| 194 | bitWidth: mp.bitWidth, |
Yi Tseng | a87b40c | 2017-09-10 00:59:03 -0700 | [diff] [blame] | 195 | matchType: mp.matchType, |
| 196 | } |
| 197 | }); |
| 198 | |
| 199 | var tables = [ |
| 200 | { |
| 201 | title: 'Match fields', |
| 202 | headers: ['Name', 'Bit width', 'Match type'], |
| 203 | data: matchFields, |
| 204 | noDataText: 'No match fields' |
| 205 | }, |
| 206 | { |
| 207 | title: 'Actions', |
| 208 | headers: ['Name'], |
| 209 | data: table.actions, |
| 210 | noDataText: 'No actions' |
| 211 | }, |
| 212 | ]; |
| 213 | |
| 214 | populateDetailPanel(table.name, subtitles, tables); |
| 215 | } |
| 216 | |
| 217 | function closePanel() { |
| 218 | if (detailsPanel.isVisible()) { |
| 219 | |
| 220 | detailsPanel.hide(); |
| 221 | |
| 222 | // Avoid Angular inprog error |
| 223 | $timeout(function() { |
| 224 | $scope.selectedId = null; |
| 225 | }, 0); |
| 226 | return true; |
| 227 | } |
| 228 | return false; |
| 229 | } |
| 230 | |
| 231 | function populateDetailTable(tableContainer, table) { |
| 232 | var tableTitle = table.title; |
| 233 | var tableData = table.data; |
| 234 | var tableHeaders = table.headers; |
| 235 | var noDataText = table.noDataText; |
| 236 | |
| 237 | tableContainer.append('h2').classed('detail-panel-bottom-title', true).text(tableTitle); |
| 238 | |
| 239 | var detailPanelTable = tableContainer.append('table').classed('detail-panel-table', true); |
| 240 | var headerTr = detailPanelTable.append('tr').classed('detail-panel-table-header', true); |
| 241 | |
| 242 | tableHeaders.forEach(function(h) { |
| 243 | headerTr.append('th').text(h); |
| 244 | }); |
| 245 | |
| 246 | if (tableData.length === 0) { |
| 247 | var row = detailPanelTable.append('tr').classed('detail-panel-table-row', true); |
| 248 | row.append('td') |
| 249 | .classed('detail-panel-table-col no-data', true) |
| 250 | .attr('colspan', tableHeaders.length) |
| 251 | .text(noDataText); |
| 252 | } |
| 253 | |
| 254 | tableData.forEach(function(data) { |
| 255 | var row = detailPanelTable.append('tr').classed('detail-panel-table-row', true); |
| 256 | if (fs.isS(data)) { |
| 257 | row.append('td').classed('detail-panel-table-col', true).text(data); |
| 258 | } else { |
| 259 | Object.keys(data).forEach(function(k) { |
| 260 | row.append('td').classed('detail-panel-table-col', true).text(data[k]); |
| 261 | }); |
| 262 | } |
| 263 | }); |
| 264 | |
| 265 | tableContainer.append('hr'); |
| 266 | } |
| 267 | |
| 268 | function populateDetailTables(tableContainer, tables) { |
| 269 | tables.forEach(function(table) { |
| 270 | populateDetailTable(tableContainer, table); |
| 271 | }) |
| 272 | } |
| 273 | |
| 274 | function populateDetailPanel(topTitle, topSubtitles, tables) { |
| 275 | dps.empty(); |
| 276 | dps.addContainers(); |
| 277 | dps.addCloseButton(closePanel); |
| 278 | |
| 279 | var top = dps.top(); |
| 280 | top.append('h2').classed('detail-panel-header', true).text(topTitle); |
| 281 | topSubtitles.forEach(function(st) { |
| 282 | var typeText = top.append('div').classed('top-info', true); |
| 283 | typeText.append('p').classed('label', true).text(st.label); |
| 284 | typeText.append('p').classed('value', true).text(st.value); |
| 285 | }); |
| 286 | |
| 287 | var bottom = dps.bottom(); |
| 288 | var bottomHeight = pHeight - pTopHeight - 60; |
| 289 | bottom.style('height', bottomHeight + 'px'); |
| 290 | populateDetailTables(bottom, tables); |
| 291 | |
| 292 | detailsPanel.width(pWidth); |
| 293 | detailsPanel.show(); |
| 294 | resizeDetailPanel(); |
| 295 | } |
| 296 | |
| 297 | function heightCalc() { |
| 298 | pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height') |
| 299 | + mast.mastHeight() + topPdg; |
| 300 | wSize = fs.windowSize(pStartY); |
| 301 | pHeight = wSize.height - 20; |
| 302 | } |
| 303 | |
| 304 | function resizeDetailPanel() { |
| 305 | if (detailsPanel.isVisible()) { |
| 306 | heightCalc(); |
| 307 | var bottomHeight = pHeight - pTopHeight - 60; |
| 308 | d3.select('.bottom').style('height', bottomHeight + 'px'); |
| 309 | detailsPanel.height(pHeight); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | angular.module('ovPipeconf', []) |
| 314 | .controller('OvPipeconfCtrl', |
| 315 | ['$log', '$scope', '$location', '$interval', '$timeout', 'FnService', 'NavService', 'WebSocketService', |
| 316 | 'LoadingService', 'PanelService', 'MastService', 'IconService', 'DetailsPanelService', |
| 317 | function (_$log_, _$scope_, _$loc_, _$interval_, _$timeout_, _fs_, |
| 318 | _ns_, _wss_, _ls_, _ps_, _mast_, _is_, _dps_) { |
| 319 | $log = _$log_; |
| 320 | $scope = _$scope_; |
| 321 | $loc = _$loc_; |
| 322 | $interval = _$interval_; |
| 323 | $timeout = _$timeout_; |
| 324 | fs = _fs_; |
| 325 | ns = _ns_; |
| 326 | wss = _wss_; |
| 327 | ls = _ls_; |
| 328 | ps = _ps_; |
| 329 | mast = _mast_; |
| 330 | is = _is_; |
| 331 | dps = _dps_; |
| 332 | |
| 333 | $scope.deviceTip = 'Show device table'; |
| 334 | $scope.flowTip = 'Show flow view for this device'; |
| 335 | $scope.portTip = 'Show port view for this device'; |
| 336 | $scope.groupTip = 'Show group view for this device'; |
| 337 | $scope.meterTip = 'Show meter view for selected device'; |
| 338 | $scope.pipeconfTip = 'Show pipeconf view for selected device'; |
| 339 | |
| 340 | var params = $loc.search(); |
| 341 | if (params.hasOwnProperty(devId)) { |
| 342 | $scope.devId = params[devId]; |
| 343 | } |
| 344 | $scope.nav = function (path) { |
| 345 | if ($scope.devId) { |
| 346 | ns.navTo(path, { devId: $scope.devId }); |
| 347 | } |
| 348 | }; |
| 349 | handlers = { |
| 350 | pipeConfResponse: pipeConfRespCb, |
| 351 | noPipeconfResp: noPipeconfRespCb, |
| 352 | invalidDevId: noPipeconfRespCb, |
| 353 | }; |
| 354 | wss.bindHandlers(handlers); |
| 355 | $scope.$on('$destroy', viewDestroy); |
| 356 | |
| 357 | $scope.autoRefresh = true; |
| 358 | fetchPipeconfData(); |
| 359 | |
| 360 | // On click callbacks, initialize select id |
| 361 | $scope.selectedId = null; |
| 362 | $scope.headerSelectCb = headerSelectCb; |
| 363 | $scope.actionSelectCb = actionSelectCb; |
| 364 | $scope.tableSelectCb = tableSelectCb; |
| 365 | |
| 366 | // Make them collapsable |
| 367 | $scope.collapsePipeconf = false; |
| 368 | $scope.collapseHeaders = false; |
| 369 | $scope.collapseActions = false; |
| 370 | $scope.collapseTables = false; |
| 371 | |
| 372 | $scope.mapToNames = function(data) { |
| 373 | return data.map(function(d) { |
| 374 | return d.name; |
| 375 | }); |
| 376 | }; |
| 377 | |
| 378 | $scope.matMatchFields = function(matchFields) { |
| 379 | return matchFields.map(function(mf) { |
Yi Tseng | e2a1500 | 2017-11-29 16:42:52 -0800 | [diff] [blame] | 380 | return mf.field; |
Yi Tseng | a87b40c | 2017-09-10 00:59:03 -0700 | [diff] [blame] | 381 | }); |
| 382 | }; |
| 383 | |
| 384 | refreshPromise = $interval(function() { |
| 385 | fetchPipeconfData(); |
| 386 | }, refreshRate); |
| 387 | |
| 388 | $log.log('OvPipeconfCtrl has been created'); |
| 389 | }]) |
| 390 | .directive('autoHeight', ['$window', 'FnService', |
| 391 | function($window, fs) { |
| 392 | return function(scope, element) { |
| 393 | var autoHeightElem = d3.select(element[0]); |
| 394 | |
| 395 | scope.$watchCollection(function() { |
| 396 | return { |
| 397 | h: $window.innerHeight |
| 398 | }; |
| 399 | }, function() { |
| 400 | var wsz = fs.windowSize(140, 0); |
| 401 | autoHeightElem.style('height', wsz.height + 'px'); |
| 402 | }); |
| 403 | }; |
| 404 | } |
| 405 | ]) |
| 406 | .directive('pipeconfViewDetailPanel', ['$rootScope', '$window', '$timeout', 'KeyService', |
| 407 | function($rootScope, $window, $timeout, ks) { |
| 408 | function createDetailsPanel() { |
| 409 | detailsPanel = dps.create(pName, { |
| 410 | width: wSize.width, |
| 411 | margin: 0, |
| 412 | hideMargin: 0, |
| 413 | scope: $scope, |
| 414 | keyBindings: keyBindings, |
| 415 | nameChangeRequest: null, |
| 416 | }); |
| 417 | $scope.hidePanel = function () { detailsPanel.hide(); }; |
| 418 | detailsPanel.hide(); |
| 419 | } |
| 420 | |
| 421 | function initPanel() { |
| 422 | heightCalc(); |
| 423 | createDetailsPanel(); |
| 424 | } |
| 425 | |
| 426 | return function(scope) { |
| 427 | var unbindWatch; |
| 428 | // Safari has a bug where it renders the fixed-layout table wrong |
| 429 | // if you ask for the window's size too early |
| 430 | if (scope.onos.browser === 'safari') { |
| 431 | $timeout(initPanel); |
| 432 | } else { |
| 433 | initPanel(); |
| 434 | } |
| 435 | |
| 436 | // if the panelData changes |
| 437 | scope.$watch('panelData', function () { |
| 438 | if (!fs.isEmptyObject(scope.panelData)) { |
| 439 | // populateDetails(scope.panelData); |
| 440 | detailsPanel.show(); |
| 441 | } |
| 442 | }); |
| 443 | |
| 444 | // if the window size changes |
| 445 | unbindWatch = $rootScope.$watchCollection( |
| 446 | function () { |
| 447 | return { |
| 448 | h: $window.innerHeight, |
| 449 | w: $window.innerWidth, |
| 450 | }; |
| 451 | }, function () { |
| 452 | resizeDetailPanel(); |
| 453 | } |
| 454 | ); |
| 455 | |
| 456 | scope.$on('$destroy', function () { |
| 457 | unbindWatch(); |
| 458 | ks.unbindKeys(); |
| 459 | ps.destroyPanel(pName); |
| 460 | }); |
| 461 | }; |
| 462 | } |
| 463 | ]); |
| 464 | }()); |