diff options
author | Ashlee Young <ashlee@onosfw.com> | 2015-09-09 22:15:21 -0700 |
---|---|---|
committer | Ashlee Young <ashlee@onosfw.com> | 2015-09-09 22:15:21 -0700 |
commit | 13d05bc8458758ee39cb829098241e89616717ee (patch) | |
tree | 22a4d1ce65f15952f07a3df5af4b462b4697cb3a /framework/src/onos/web/gui/src/main/webapp/app/view | |
parent | 6139282e1e93c2322076de4b91b1c85d0bc4a8b3 (diff) |
ONOS checkin based on commit tag e796610b1f721d02f9b0e213cf6f7790c10ecd60
Change-Id: Ife8810491034fe7becdba75dda20de4267bd15cd
Diffstat (limited to 'framework/src/onos/web/gui/src/main/webapp/app/view')
52 files changed, 9134 insertions, 0 deletions
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.css b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.css new file mode 100644 index 00000000..9413369a --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.css @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Host View -- CSS file + */ + +#ov-app h2 { + display: inline-block; +} + +#ov-app div.ctrl-btns { + width: 290px; +} + +#ov-app form#inputFileForm, +#ov-app input#uploadFile { + display: none; +} diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.html b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.html new file mode 100644 index 00000000..85b044fb --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.html @@ -0,0 +1,80 @@ +<!-- app partial HTML --> +<div id="ov-app"> + <div class="tabular-header"> + <h2>Applications ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + <div class="separator"></div> + + <form id="inputFileForm"> + <input id="uploadFile" + type="file" size="50" accept=".oar" + file-model="appFile"> + </form> + <div icon icon-size="36" icon-id="plus" + class="active" trigger-form + tooltip tt-msg="uploadTip"> + </div> + <div icon icon-size="36" icon-id="play" + ng-click="appAction('activate')" + tooltip tt-msg="activateTip" + ng-class="{active: ctrlBtnState.installed}"> + </div> + <div icon icon-size="36" icon-id="stop" + ng-click="appAction('deactivate')" + tooltip tt-msg="deactivateTip" + ng-class="{active: ctrlBtnState.active}"> + </div> + <div icon icon-size="36" icon-id="garbage" + ng-click="appAction('uninstall')" + tooltip tt-msg="uninstallTip" + ng-class="{active: ctrlBtnState.selection}"> + </div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <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 onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="5"> + No Applications found + </td> + </tr> + + <tr ng-repeat="app in tableData track by $index" + ng-click="selectCallback($event, app)" + ng-class="{selected: app.id === selId}" + ng-repeat-complete row-id="{{app.id}}"> + <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> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.js b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.js new file mode 100644 index 00000000..7cc081cb --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.js @@ -0,0 +1,148 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- App View Module + */ + +(function () { + 'use strict'; + + // constants + var INSTALLED = 'INSTALLED', + ACTIVE = 'ACTIVE', + APP_MGMENT_REQ = 'appManagementRequest', + FILE_UPLOAD_URL = 'applications/upload'; + + angular.module('ovApp', []) + .controller('OvAppCtrl', + ['$log', '$scope', '$http', + 'FnService', 'TableBuilderService', 'WebSocketService', 'UrlFnService', + 'KeyService', + + function ($log, $scope, $http, fs, tbs, wss, ufs, ks) { + $scope.ctrlBtnState = {}; + $scope.uploadTip = 'Upload an application (.oar file)'; + $scope.activateTip = 'Activate selected application'; + $scope.deactivateTip = 'Deactivate selected application'; + $scope.uninstallTip = 'Uninstall selected application'; + + function selCb($event, row) { + // selId comes from tableBuilder + $scope.ctrlBtnState.selection = !!$scope.selId; + refreshCtrls(); + } + + function refreshCtrls() { + var row, rowIdx; + if ($scope.ctrlBtnState.selection) { + rowIdx = fs.find($scope.selId, $scope.tableData); + row = rowIdx >= 0 ? $scope.tableData[rowIdx] : null; + + $scope.ctrlBtnState.installed = row && row.state === INSTALLED; + $scope.ctrlBtnState.active = row && row.state === ACTIVE; + } else { + $scope.ctrlBtnState.installed = false; + $scope.ctrlBtnState.active = false; + } + } + + tbs.buildTable({ + scope: $scope, + tag: 'app', + selCb: selCb, + respCb: refreshCtrls + }); + + // TODO: reexamine where keybindings should be - directive or controller? + ks.keyBindings({ + esc: [$scope.selectCallback, 'Deselect app'], + _helpFormat: ['esc'] + }); + ks.gestureNotes([ + ['click row', 'Select / deselect app'], + ['scroll down', 'See more apps'] + ]); + + $scope.appAction = function (action) { + if ($scope.ctrlBtnState.selection) { + $log.debug('Initiating ' + action + ' of ' + $scope.selId); + wss.sendEvent(APP_MGMENT_REQ, { + action: action, + name: $scope.selId, + sortCol: $scope.sortParams.sortCol, + sortDir: $scope.sortParams.sortDir + }); + } + }; + + $scope.$on('FileChanged', function () { + var formData = new FormData(); + if ($scope.appFile) { + formData.append('file', $scope.appFile); + $http.post(ufs.rsUrl(FILE_UPLOAD_URL), formData, { + transformRequest: angular.identity, + headers: { + 'Content-Type': undefined + } + }) + .finally(function () { + $scope.sortCallback($scope.sortParams); + document.getElementById('inputFileForm').reset(); + }); + } + }); + + $scope.$on('$destroy', function () { + ks.unbindKeys(); + }); + + $log.log('OvAppCtrl has been created'); + }]) + + // triggers the input form to appear when button is clicked + .directive('triggerForm', function () { + return { + restrict: 'A', + link: function (scope, elem) { + elem.bind('click', function () { + document.getElementById('uploadFile') + .dispatchEvent(new MouseEvent('click')); + }); + } + }; + }) + + // binds the model file to the scope in scope.appFile + // sends upload request to the server + .directive('fileModel', ['$parse', + function ($parse) { + return { + restrict: 'A', + link: function (scope, elem, attrs) { + var model = $parse(attrs.fileModel), + modelSetter = model.assign; + + elem.bind('change', function () { + scope.$apply(function () { + modelSetter(scope, elem[0].files[0]); + }); + scope.$emit('FileChanged'); + }); + } + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.css b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.css new file mode 100644 index 00000000..83ba8d87 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.css @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Cluster View -- CSS file + */ + +#ov-cluster h2 { + display: inline-block; +} + +#ov-cluster div.ctrl-btns { + width: 45px; +}
\ No newline at end of file diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.html b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.html new file mode 100644 index 00000000..c5048884 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.html @@ -0,0 +1,68 @@ +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Cluster partial HTML --> +<div id="ov-cluster"> + <div class="tabular-header"> + <h2>Cluster Nodes ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <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 onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="5"> + No Cluster Nodes found + </td> + </tr> + + <tr ng-repeat="node in tableData track by $index" + ng-repeat-complete row-id="{{node.id}}"> + <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> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.js b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.js new file mode 100644 index 00000000..006808c0 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.js @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Cluster View Module + */ + +(function () { + 'use strict'; + + angular.module('ovCluster', []) + .controller('OvClusterCtrl', + ['$log', '$scope', 'TableBuilderService', + + function ($log, $scope, tbs) { + tbs.buildTable({ + scope: $scope, + tag: 'cluster' + }); + + $log.log('OvClusterCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.css b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.css new file mode 100644 index 00000000..fc08f68b --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.css @@ -0,0 +1,144 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Device View -- CSS file + */ + +#ov-device h2 { + display: inline-block; +} + +#ov-device div.ctrl-btns { + width: 240px; +} + +/* More in generic panel.css */ + +#device-details-panel.floatpanel { + -moz-border-radius: 0; + border-radius: 0; + z-index: 0; +} + +.light #device-details-panel.floatpanel { + background-color: rgb(229, 234, 237); +} +.dark #device-details-panel.floatpanel { + background-color: #3A4042; +} + +#device-details-panel .container { + padding: 0 12px; +} + +#device-details-panel .close-btn { + position: absolute; + right: 10px; + top: 0; + cursor: pointer; +} +.light .close-btn svg.embeddedIcon .icon.plus .glyph { + fill: #aaa; +} +.dark .close-btn svg.embeddedIcon .icon.plus .glyph { + fill: #ccc; +} + +#device-details-panel .dev-icon { + display: inline-block; + padding: 0 6px 0 0; + vertical-align: middle; +} +.light .dev-icon svg.embeddedIcon .glyph { + fill: rgb(0, 172, 229); +} +.dark .dev-icon svg.embeddedIcon .glyph { + fill: #486D91; +} + +#device-details-panel h2 { + display: inline-block; + margin: 8px 0; +} + +#device-details-panel .top div.left { + float: left; + padding: 0 18px 0 0; +} +#device-details-panel .top div.right { + display: inline-block; +} + +#device-details-panel td.label { + font-style: italic; + padding-right: 12px; + /* works for both light and dark themes ... */ + color: #777; +} + +#device-details-panel .actionBtns div { + padding: 12px 6px; +} + +#device-details-panel .top hr { + width: 95%; + margin: 0 auto; +} + +.light #device-details-panel hr { + opacity: .5; + border-color: #FFF; +} +.dark #device-details-panel hr { + border-color: #666; +} + +#device-details-panel .bottom table { + border-spacing: 0; +} + +#device-details-panel .bottom th { + letter-spacing: 0.02em; +} + +.light #device-details-panel .bottom th { + background-color: #CCC; + /* default text color */ +} +.dark #device-details-panel .bottom th { + background-color: #131313; + color: #ccc; +} + +#device-details-panel .bottom th, +#device-details-panel .bottom td { + padding: 6px 12px; + text-align: center; +} + +.light #device-details-panel .bottom tr:nth-child(odd) { + background-color: #f9f9f9; +} +.light #device-details-panel .bottom tr:nth-child(even) { + background-color: #EBEDF2; +} +.dark #device-details-panel .bottom tr:nth-child(odd) { + background-color: #333; +} +.dark #device-details-panel .bottom tr:nth-child(even) { + background-color: #555; +} diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.html b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.html new file mode 100644 index 00000000..5d51d1d4 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.html @@ -0,0 +1,82 @@ +<!-- Device partial HTML --> +<div id="ov-device"> + <div class="tabular-header"> + <h2>Devices ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-id="refresh" icon-size="36" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + <div class="separator"></div> + + <div ng-class="{active: !!selId}" + icon icon-id="flowTable" icon-size="36" + tooltip tt-msg="flowTip" + ng-click="nav('flow')"></div> + + <div ng-class="{active: !!selId}" + icon icon-id="portTable" icon-size="36" + tooltip tt-msg="portTip" + ng-click="nav('port')"></div> + + <div ng-class="{active: !!selId}" + icon icon-id="groupTable" icon-size="36" + tooltip tt-msg="groupTip" + ng-click="nav('group')"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <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 onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="9"> + No Devices found + </td> + </tr> + + <tr ng-repeat="dev in tableData track by $index" + ng-click="selectCallback($event, dev)" + ng-class="{selected: dev.id === selId}" + ng-repeat-complete row-id="{{dev.id}}"> + <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> + + <device-details-panel></device-details-panel> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.js b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.js new file mode 100644 index 00000000..7a2dc4f9 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.js @@ -0,0 +1,319 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Device View Module + */ + +(function () { + 'use strict'; + + // injected refs + var $log, $scope, $location, fs, mast, ps, wss, is, ns; + + // internal state + var detailsPanel, + pStartY, pHeight, + top, bottom, iconDiv, + wSize; + + // constants + var topPdg = 13, + ctnrPdg = 24, + scrollSize = 17, + portsTblPdg = 50, + + pName = 'device-details-panel', + detailsReq = 'deviceDetailsRequest', + detailsResp = 'deviceDetailsResponse', + + propOrder = [ + 'type', 'masterid', 'chassisid', + 'mfr', 'hw', 'sw', 'protocol', 'serial' + ], + friendlyProps = [ + 'Type', 'Master ID', 'Chassis ID', + 'Vendor', 'H/W Version', 'S/W Version', 'Protocol', 'Serial #' + ], + portCols = [ + 'enabled', 'id', 'speed', 'type', 'elinks_dest', 'name' + ], + friendlyPortCols = [ + 'Enabled', 'ID', 'Speed', 'Type', 'Egress Links', 'Name' + ]; + + function closePanel() { + if (detailsPanel.isVisible()) { + $scope.selId = null; + detailsPanel.hide(); + } + } + + function addCloseBtn(div) { + is.loadEmbeddedIcon(div, 'plus', 30); + div.select('g').attr('transform', 'translate(25, 0) rotate(45)'); + div.on('click', closePanel); + } + + function setUpPanel() { + var container, closeBtn, tblDiv; + detailsPanel.empty(); + + container = detailsPanel.append('div').classed('container', true); + + top = container.append('div').classed('top', true); + closeBtn = top.append('div').classed('close-btn', true); + addCloseBtn(closeBtn); + iconDiv = top.append('div').classed('dev-icon', true); + top.append('h2'); + + tblDiv = top.append('div').classed('top-tables', true); + tblDiv.append('div').classed('left', true).append('table'); + tblDiv.append('div').classed('right', true).append('table'); + + top.append('hr'); + + bottom = container.append('div').classed('bottom', true); + bottom.append('h2').classed('ports-title', true).html('Ports'); + bottom.append('table'); + } + + function addProp(tbody, index, value) { + var tr = tbody.append('tr'); + + function addCell(cls, txt) { + tr.append('td').attr('class', cls).html(txt); + } + addCell('label', friendlyProps[index] + ' :'); + addCell('value', value); + } + + function populateTop(tblDiv, details) { + var leftTbl = tblDiv.select('.left') + .select('table') + .append('tbody'), + rightTbl = tblDiv.select('.right') + .select('table') + .append('tbody'); + + is.loadEmbeddedIcon(iconDiv, details._iconid_type, 40); + top.select('h2').html(details.id); + + propOrder.forEach(function (prop, i) { + // properties are split into two tables + addProp(i < 3 ? leftTbl : rightTbl, i, details[prop]); + }); + } + + function addPortRow(tbody, port) { + var tr = tbody.append('tr'); + + portCols.forEach(function (col) { + tr.append('td').html(port[col]); + }); + } + + function populateBottom(table, ports) { + var theader = table.append('thead').append('tr'), + tbody = table.append('tbody'), + tbWidth, tbHeight; + + friendlyPortCols.forEach(function (col) { + theader.append('th').html(col); + }); + ports.forEach(function (port) { + addPortRow(tbody, port); + }); + + tbWidth = fs.noPxStyle(tbody, 'width') + scrollSize; + tbHeight = pHeight + - (fs.noPxStyle(detailsPanel.el() + .select('.top'), 'height') + + fs.noPxStyle(detailsPanel.el() + .select('.ports-title'), 'height') + + portsTblPdg); + + table.style({ + height: tbHeight + 'px', + width: tbWidth + 'px', + overflow: 'auto', + display: 'block' + }); + + detailsPanel.width(tbWidth + ctnrPdg); + } + + function populateDetails(details) { + var topTbs, btmTbl, ports; + setUpPanel(); + + topTbs = top.select('.top-tables'); + btmTbl = bottom.select('table'); + ports = details.ports; + + populateTop(topTbs, details); + populateBottom(btmTbl, ports); + + detailsPanel.height(pHeight); + } + + function respDetailsCb(data) { + $scope.panelData = data.details; + $scope.$apply(); + } + + function createDetailsPane() { + detailsPanel = ps.createPanel(pName, { + width: wSize.width, + margin: 0, + hideMargin: 0 + }); + detailsPanel.el().style({ + position: 'absolute', + top: pStartY + 'px' + }); + $scope.hidePanel = function () { detailsPanel.hide(); }; + detailsPanel.hide(); + } + + angular.module('ovDevice', []) + .controller('OvDeviceCtrl', + ['$log', '$scope', '$location', 'TableBuilderService', 'FnService', + 'MastService', 'PanelService', 'WebSocketService', 'IconService', + 'NavService', + + function (_$log_, _$scope_, _$location_, + tbs, _fs_, _mast_, _ps_, _wss_, _is_, _ns_) { + $log = _$log_; + $scope = _$scope_; + $location = _$location_; + fs = _fs_; + mast = _mast_; + ps = _ps_; + wss = _wss_; + is = _is_; + ns = _ns_; + var params = $location.search(), + handlers = {}; + $scope.panelData = {}; + $scope.flowTip = 'Show flow view for selected device'; + $scope.portTip = 'Show port view for selected device'; + $scope.groupTip = 'Show group view for selected device'; + + // details panel handlers + handlers[detailsResp] = respDetailsCb; + wss.bindHandlers(handlers); + + // query for if a certain device needs to be highlighted + if (params.hasOwnProperty('devId')) { + $scope.selId = params['devId']; + wss.sendEvent(detailsReq, { id: $scope.selId }); + } + + function selCb($event, row) { + if ($scope.selId) { + wss.sendEvent(detailsReq, { id: row.id }); + } else { + $scope.hidePanel(); + } + $log.debug('Got a click on:', row); + } + + tbs.buildTable({ + scope: $scope, + tag: 'device', + selCb: selCb + }); + + $scope.nav = function (path) { + if ($scope.selId) { + ns.navTo(path, { devId: $scope.selId }); + } + }; + + $scope.$on('$destroy', function () { + wss.unbindHandlers(handlers); + }); + + $log.log('OvDeviceCtrl has been created'); + }]) + + .directive('deviceDetailsPanel', + ['$rootScope', '$window', '$timeout', 'KeyService', + function ($rootScope, $window, $timeout, ks) { + return function (scope) { + var unbindWatch; + + function heightCalc() { + pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height') + + mast.mastHeight() + topPdg; + wSize = fs.windowSize(pStartY); + pHeight = wSize.height; + } + + function initPanel() { + heightCalc(); + createDetailsPane(); + } + + // Safari has a bug where it renders the fixed-layout table wrong + // if you ask for the window's size too early + if (scope.onos.browser === 'safari') { + $timeout(initPanel); + } else { + initPanel(); + } + // create key bindings to handle panel + ks.keyBindings({ + esc: [closePanel, 'Close the details panel'], + _helpFormat: ['esc'] + }); + ks.gestureNotes([ + ['click', 'Select a row to show device details'], + ['scroll down', 'See more devices'] + ]); + + // if the panelData changes + scope.$watch('panelData', function () { + if (!fs.isEmptyObject(scope.panelData)) { + populateDetails(scope.panelData); + detailsPanel.show(); + } + }); + + // if the window size changes + unbindWatch = $rootScope.$watchCollection( + function () { + return { + h: $window.innerHeight, + w: $window.innerWidth + }; + }, function () { + if (!fs.isEmptyObject(scope.panelData)) { + heightCalc(); + populateDetails(scope.panelData); + } + } + ); + + scope.$on('$destroy', function () { + unbindWatch(); + ks.unbindKeys(); + ps.destroyPanel(pName); + }); + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.css b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.css new file mode 100644 index 00000000..4aa96210 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.css @@ -0,0 +1,86 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Flow View -- CSS file + */ + +#ov-flow h2 { + display: inline-block; +} + +#ov-flow div.ctrl-btns { + width: 240px; +} + +.light #ov-flow .current-view use { + fill: white; +} +.dark #ov-flow .current-view use { + fill: #304860; +} + +.light #ov-flow .current-view rect { + fill: deepskyblue; +} +.dark #ov-flow .current-view rect { + fill: #eee; +} + +.light #ov-flow tr:nth-child(6n + 1), +.light #ov-flow tr:nth-child(6n + 2), +.light #ov-flow tr:nth-child(6n + 3) { + background-color: #eee; +} +.light #ov-flow tr:nth-child(6n + 4), +.light #ov-flow tr:nth-child(6n + 5), +.light #ov-flow tr:nth-child(6n) { + background-color: #ddd; +} +.dark #ov-flow tr:nth-child(6n + 1), +.dark #ov-flow tr:nth-child(6n + 2), +.dark #ov-flow tr:nth-child(6n + 3) { + background-color: #444; +} +.dark #ov-flow tr:nth-child(6n + 4), +.dark #ov-flow tr:nth-child(6n + 5), +.dark #ov-flow tr:nth-child(6n) { + background-color: #333; +} + +/* highlighted color */ +.light #ov-flow tr:nth-child(6n + 1).data-change, +.light #ov-flow tr:nth-child(6n + 2).data-change, +.light #ov-flow tr:nth-child(6n + 3).data-change, +.light #ov-flow tr:nth-child(6n + 4).data-change, +.light #ov-flow tr:nth-child(6n + 5).data-change, +.light #ov-flow tr:nth-child(6n).data-change { + background-color: #FDFFDC; +} +.dark #ov-flow tr:nth-child(6n + 1).data-change, +.dark #ov-flow tr:nth-child(6n + 2).data-change, +.dark #ov-flow tr:nth-child(6n + 3).data-change, +.dark #ov-flow tr:nth-child(6n + 4).data-change, +.dark #ov-flow tr:nth-child(6n + 5).data-change, +.dark #ov-flow tr:nth-child(6n).data-change { + background-color: #5A5600; +} + +#ov-flow td.selector, +#ov-flow td.treatment { + padding-left: 36px; + opacity: 0.65; +}
\ No newline at end of file diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.html b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.html new file mode 100644 index 00000000..5fce98cf --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.html @@ -0,0 +1,84 @@ +<!-- Flow partial HTML --> +<div id="ov-flow"> + <div class="tabular-header"> + <h2> + Flows for Device {{devId || "(No device selected)"}} + ({{tableData.length}} total) + </h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + + <div class="separator"></div> + + <div class="current-view" + icon icon-id="flowTable" icon-size="36"></div> + + <div class="active" + icon icon-id="portTable" icon-size="36" + tooltip tt-msg="portTip" + ng-click="nav('port')"></div> + + <div class="active" + icon icon-id="groupTable" icon-size="36" + tooltip tt-msg="groupTip" + ng-click="nav('group')"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <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> + <td colId="packets" sortable>Packets </td> + <td colId="bytes" sortable>Bytes </td> + </tr> + </table> + </div> + + <div class="table-body"> + <table onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="10"> + No Flows found + </td> + </tr> + + <tr ng-repeat-start="flow in tableData track by $index" + ng-repeat-complete row-id="{{flow.id}}"> + <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> + <td>{{flow.packets}}</td> + <td>{{flow.bytes}}</td> + </tr> + <tr row-id="{{flow.id}}"> + <td class="selector" colspan="10">{{flow.selector}}</td> + </tr> + <tr row-id="{{flow.id}}" ng-repeat-end> + <td class="treatment" colspan="10">{{flow.treatment}}</td> + </tr> + </table> + </div> + + </div> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.js b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.js new file mode 100644 index 00000000..15678d5e --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.js @@ -0,0 +1,62 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Flow View Module + */ + +(function () { + 'use strict'; + + // injected references + var $log, $scope, $location, fs, tbs, ns; + + angular.module('ovFlow', []) + .controller('OvFlowCtrl', + ['$log', '$scope', '$location', + 'FnService', 'TableBuilderService', 'NavService', + + function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _ns_) { + var params; + $log = _$log_; + $scope = _$scope_; + $location = _$location_; + fs = _fs_; + tbs = _tbs_; + ns = _ns_; + $scope.portTip = 'Show port view for this device'; + $scope.groupTip = 'Show group view for this device'; + + params = $location.search(); + if (params.hasOwnProperty('devId')) { + $scope.devId = params['devId']; + } + + tbs.buildTable({ + scope: $scope, + tag: 'flow', + query: params + }); + + $scope.nav = function (path) { + if ($scope.devId) { + ns.navTo(path, { devId: $scope.devId }); + } + }; + + $log.log('OvFlowCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.css b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.css new file mode 100644 index 00000000..42f1c31d --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.css @@ -0,0 +1,77 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Group View -- CSS file + */ + +#ov-group h2 { + display: inline-block; +} + +#ov-group div.ctrl-btns { + width: 240px; +} + +.light #ov-group .current-view use { + fill: white; +} +.dark #ov-group .current-view use { + fill: #304860; +} + +.light #ov-group .current-view rect { + fill: deepskyblue; +} +.dark #ov-group .current-view rect { + fill: #eee; +} + +.light #ov-group tr:nth-child(4n + 1), +.light #ov-group tr:nth-child(4n + 2) { + background-color: #eee; +} +.light #ov-group tr:nth-child(4n + 3), +.light #ov-group tr:nth-child(4n) { + background-color: #ddd; +} +.dark #ov-group tr:nth-child(4n + 1), +.dark #ov-group tr:nth-child(4n + 2) { + background-color: #444; +} +.dark #ov-group tr:nth-child(4n + 3), +.dark #ov-group tr:nth-child(4n) { + background-color: #333; +} + +/* highlighted color */ +.light #ov-group tr:nth-child(4n + 1).data-change, +.light #ov-group tr:nth-child(4n + 2).data-change, +.light #ov-group tr:nth-child(4n + 3).data-change, +.light #ov-group tr:nth-child(4n).data-change { + background-color: #FDFFDC; +} +.dark #ov-group tr:nth-child(4n + 1).data-change, +.dark #ov-group tr:nth-child(4n + 2).data-change, +.dark #ov-group tr:nth-child(4n + 3).data-change, +.dark #ov-group tr:nth-child(4n).data-change { + background-color: #5A5600; +} + +#ov-group td.buckets { + padding-left: 36px; + opacity: 0.65; +} diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.html b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.html new file mode 100644 index 00000000..b963f469 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.html @@ -0,0 +1,90 @@ +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Group partial HTML --> +<div id="ov-group"> + <div class="tabular-header"> + <h2> + Groups for Device {{devId || "(No device selected)"}} + ({{tableData.length}} total) + </h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + + <div class="separator"></div> + + <div class="active" + icon icon-id="flowTable" icon-size="36" + tooltip tt-msg="flowTip" + ng-click="nav('flow')"></div> + + <div class="active" + icon icon-id="portTable" icon-size="36" + tooltip tt-msg="portTip" + ng-click="nav('port')"></div> + + <div class="current-view" + icon icon-id="groupTable" icon-size="36"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <table> + <tr> + <td colId="id" sortable>Group ID </td> + <td colId="app_id" sortable>App ID </td> + <td colId="state" sortable>State </td> + <td colId="type" sortable>Type </td> + <td colId="packets" sortable>Packets </td> + <td colId="bytes" sortable>Bytes </td> + </tr> + </table> + </div> + + <div class="table-body"> + <table onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="6"> + No Groups found + </td> + </tr> + + <tr ng-repeat-start="group in tableData track by $index" + ng-repeat-complete row-id="{{group.id}}"> + <td>{{group.id}}</td> + <td>{{group.app_id}}</td> + <td>{{group.state}}</td> + <td>{{group.type}}</td> + <td>{{group.packets}}</td> + <td>{{group.bytes}}</td> + </tr> + <tr row-id="{{group.id}}" ng-repeat-end> + <td class="buckets" colspan="6" + ng-bind-html="group.buckets"></td> + </tr> + </table> + </div> + + </div> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.js b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.js new file mode 100644 index 00000000..2510190b --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.js @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Group View Module + */ + +(function () { + 'use strict'; + + // injected references + var $log, $scope, $location, fs, tbs, ns; + + angular.module('ovGroup', []) + .controller('OvGroupCtrl', + ['$log', '$scope', '$location', '$sce', + 'FnService', 'TableBuilderService', 'NavService', + + function (_$log_, _$scope_, _$location_, $sce, _fs_, _tbs_, _ns_) { + var params; + $log = _$log_; + $scope = _$scope_; + $location = _$location_; + fs = _fs_; + tbs = _tbs_; + ns = _ns_; + $scope.flowTip = 'Show flow view for this device'; + $scope.portTip = 'Show port view for this device'; + + params = $location.search(); + if (params.hasOwnProperty('devId')) { + $scope.devId = params['devId']; + } + + tbs.buildTable({ + scope: $scope, + tag: 'group', + query: params + }); + + $scope.$watch('tableData', function () { + if (!fs.isEmptyObject($scope.tableData)) { + $scope.tableData.forEach(function (group) { + group.buckets = $sce.trustAsHtml(group.buckets); + }); + } + }); + + $scope.nav = function (path) { + if ($scope.devId) { + ns.navTo(path, { devId: $scope.devId }); + } + }; + + $log.log('OvGroupCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.css b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.css new file mode 100644 index 00000000..60672759 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.css @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Host View -- CSS file + */ + +#ov-host h2 { + display: inline-block; +} + +#ov-host div.ctrl-btns { + width: 45px; +}
\ No newline at end of file diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.html b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.html new file mode 100644 index 00000000..19a7ee1d --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.html @@ -0,0 +1,54 @@ +<!-- Host partial HTML --> +<div id="ov-host"> + <div class="tabular-header"> + <h2>Hosts ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <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 onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="6"> + No Hosts found + </td> + </tr> + + <tr ng-repeat="host in tableData track by $index" + ng-repeat-complete row-id="{{host.id}}"> + <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> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.js b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.js new file mode 100644 index 00000000..1ccca401 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.js @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Host View Module + */ + +(function () { + 'use strict'; + + angular.module('ovHost', []) + .controller('OvHostCtrl', + ['$log', '$scope', 'TableBuilderService', + + function ($log, $scope, tbs) { + tbs.buildTable({ + scope: $scope, + tag: 'host' + }); + + $log.log('OvHostCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.css b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.css new file mode 100644 index 00000000..ed9cd48d --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.css @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Intent View -- CSS file + */ + +#ov-intent h2 { + display: inline-block; +} + +#ov-intent div.ctrl-btns { + width: 45px; +} + +.light #ov-intent tr:nth-child(6n + 1), +.light #ov-intent tr:nth-child(6n + 2), +.light #ov-intent tr:nth-child(6n + 3) { + background-color: #eee; +} +.light #ov-intent tr:nth-child(6n + 4), +.light #ov-intent tr:nth-child(6n + 5), +.light #ov-intent tr:nth-child(6n) { + background-color: #ddd; +} +.dark #ov-intent tr:nth-child(6n + 1), +.dark #ov-intent tr:nth-child(6n + 2), +.dark #ov-intent tr:nth-child(6n + 3) { + background-color: #444; +} +.dark #ov-intent tr:nth-child(6n + 4), +.dark #ov-intent tr:nth-child(6n + 5), +.dark #ov-intent tr:nth-child(6n) { + background-color: #333; +} + +.light #ov-intent tr:nth-child(6n + 1).data-change, +.light #ov-intent tr:nth-child(6n + 2).data-change, +.light #ov-intent tr:nth-child(6n + 3).data-change, +.light #ov-intent tr:nth-child(6n + 4).data-change, +.light #ov-intent tr:nth-child(6n + 5).data-change, +.light #ov-intent tr:nth-child(6n).data-change { + background-color: #FDFFDC; +} +.dark #ov-intent tr:nth-child(6n + 1).data-change, +.dark #ov-intent tr:nth-child(6n + 2).data-change, +.dark #ov-intent tr:nth-child(6n + 3).data-change, +.dark #ov-intent tr:nth-child(6n + 4).data-change, +.dark #ov-intent tr:nth-child(6n + 5).data-change, +.dark #ov-intent tr:nth-child(6n).data-change { + background-color: #5A5600; +} + +#ov-intent td.resources, +#ov-intent td.details { + padding-left: 36px; + opacity: 0.65; +} diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.html b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.html new file mode 100644 index 00000000..4883beed --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.html @@ -0,0 +1,72 @@ +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Intent partial HTML --> +<div id="ov-intent"> + <div class="tabular-header"> + <h2>Intents ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <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> + <td colId="state" sortable>State </td> + </tr> + </table> + </div> + + <div class="table-body"> + <table onos-flash-changes id-prop="key"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="5"> + No Intents found + </td> + </tr> + + <tr ng-repeat-start="intent in tableData track by $index" + ng-repeat-complete row-id="{{intent.key}}"> + <td>{{intent.appId}}</td> + <td>{{intent.key}}</td> + <td>{{intent.type}}</td> + <td>{{intent.priority}}</td> + <td>{{intent.state}}</td> + </tr> + <tr row-id="{{intent.key}}"> + <td class="resources" colspan="5">{{intent.resources}}</td> + </tr> + <tr row-id="{{intent.key}}" ng-repeat-end> + <td class="details" colspan="5">{{intent.details}}</td> + </tr> + </table> + </div> + + </div> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.js b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.js new file mode 100644 index 00000000..5810f347 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.js @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Intent View Module + */ + +(function () { + 'use strict'; + + angular.module('ovIntent', []) + .controller('OvIntentCtrl', + ['$log', '$scope', 'TableBuilderService', + + function ($log, $scope, tbs) { + tbs.buildTable({ + scope: $scope, + tag: 'intent' + }); + + $log.log('OvIntentCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.css b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.css new file mode 100644 index 00000000..4f049cdb --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.css @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Link View -- CSS file + */ + +#ov-link h2 { + display: inline-block; +} + +#ov-link div.ctrl-btns { + width: 45px; +}
\ No newline at end of file diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.html b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.html new file mode 100644 index 00000000..371d6b13 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.html @@ -0,0 +1,70 @@ +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Link partial HTML --> +<div id="ov-link"> + <div class="tabular-header"> + <h2>Links ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <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 onos-flash-changes id-prop="one"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="6"> + No Links found + </td> + </tr> + + <tr ng-repeat="link in tableData track by $index" + ng-repeat-complete row-id="{{link.one}}"> + <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 ng-bind-html="link.direction"></td> + <td>{{link.durable}}</td> + </tr> + </table> + </div> + + </div> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.js b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.js new file mode 100644 index 00000000..fcf2a83e --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.js @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Host View Module + */ + +(function () { + 'use strict'; + + angular.module('ovLink', []) + .controller('OvLinkCtrl', + ['$log', '$scope', '$sce', 'FnService', 'TableBuilderService', + + function ($log, $scope, $sce, fs, tbs) { + tbs.buildTable({ + scope: $scope, + tag: 'link' + }); + + $scope.$watch('tableData', function () { + if (!fs.isEmptyObject($scope.tableData)) { + $scope.tableData.forEach(function (link) { + link.direction = $sce.trustAsHtml(link.direction); + }); + } + }); + + $log.log('OvLinkCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.css b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.css new file mode 100644 index 00000000..7fcfee3f --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.css @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Port View -- CSS file + */ + +#ov-port h2 { + display: inline-block; +} + +#ov-port div.ctrl-btns { + width: 240px; +} + +.light #ov-port .current-view use { + fill: white; +} +.dark #ov-port .current-view use { + fill: #304860; +} + +.light #ov-port .current-view rect { + fill: deepskyblue; +} +.dark #ov-port .current-view rect { + fill: #eee; +} + +#ov-port td { + text-align: right; +} + +#ov-port tr.no-data td { + text-align: center; +} diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.html b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.html new file mode 100644 index 00000000..8eecb9d6 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.html @@ -0,0 +1,90 @@ +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Port partial HTML --> +<div id="ov-port"> + <div class="tabular-header"> + <h2> + Port Statistics for Device {{devId || "(No device selected)"}} + ({{tableData.length}} Ports total) + </h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + + <div class="separator"></div> + + <div class="active" + icon icon-id="flowTable" icon-size="36" + tooltip tt-msg="flowTip" + ng-click="nav('flow')"></div> + + <div class="current-view" + icon icon-id="portTable" icon-size="36"></div> + + <div class="active" + icon icon-id="groupTable" icon-size="36" + tooltip tt-msg="groupTip" + ng-click="nav('group')"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <table> + <tr> + <td colId="id" col-width="60px" sortable>Port ID </td> + <td colId="pkt_rx" sortable>Pkts Received </td> + <td colId="pkt_tx" sortable>Pkts Sent </td> + <td colId="bytes_rx" sortable>Bytes Received </td> + <td colId="bytes_tx" sortable>Bytes Sent </td> + <td colId="pkt_rx_drp" sortable>Pkts Received Dropped </td> + <td colId="pkt_tx_drp" sortable>Pkts Sent Dropped </td> + <td colId="duration" sortable>Duration (sec) </td> + </tr> + </table> + </div> + + <div class="table-body"> + <table onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="8"> + No Ports found + </td> + </tr> + + <tr ng-repeat="port in tableData track by $index" + ng-repeat-complete row-id="{{port.id}}"> + <td>{{port.id}}</td> + <td>{{port.pkt_rx}}</td> + <td>{{port.pkt_tx}}</td> + <td>{{port.bytes_rx}}</td> + <td>{{port.bytes_tx}}</td> + <td>{{port.pkt_rx_drp}}</td> + <td>{{port.pkt_tx_drp}}</td> + <td>{{port.duration}}</td> + </tr> + </table> + </div> + + </div> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.js b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.js new file mode 100644 index 00000000..a157c5be --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.js @@ -0,0 +1,62 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Port View Module + */ + +(function () { + 'use strict'; + + // injected references + var $log, $scope, $location, fs, tbs, ns; + + angular.module('ovPort', []) + .controller('OvPortCtrl', + ['$log', '$scope', '$location', + 'FnService', 'TableBuilderService', 'NavService', + + function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _ns_) { + var params; + $log = _$log_; + $scope = _$scope_; + $location = _$location_; + fs = _fs_; + tbs = _tbs_; + ns = _ns_; + $scope.flowTip = 'Show flow view for this device'; + $scope.groupTip = 'Show group view for this device'; + + params = $location.search(); + if (params.hasOwnProperty('devId')) { + $scope.devId = params['devId']; + } + + tbs.buildTable({ + scope: $scope, + tag: 'port', + query: params + }); + + $scope.nav = function (path) { + if ($scope.devId) { + ns.navTo(path, { devId: $scope.devId }); + } + }; + + $log.log('OvPortCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.css b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.css new file mode 100644 index 00000000..e748c5ac --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.css @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Settings View -- CSS file + */ + +#ov-settings h2 { + display: inline-block; +} + +#ov-settings div.ctrl-btns { + width: 45px; +} diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.html b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.html new file mode 100644 index 00000000..ee069d37 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.html @@ -0,0 +1,52 @@ +<!-- settings partial HTML --> +<div id="ov-settings"> + <div class="tabular-header"> + <h2>Component Settings ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <table> + <tr> + <td colId="component" sortable col-width="200px">Component </td> + <td colId="id" sortable>Property </td> + <td colId="type" sortable col-width="70px">Type </td> + <td colId="value" sortable>Value </td> + <td colId="defValue" sortable>Default </td> + <td colId="desc" col-width="400px">Description </td> + </tr> + </table> + </div> + + <div class="table-body"> + <table onos-flash-changes id-prop="id"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="6"> + No Settings found + </td> + </tr> + + <tr ng-repeat="prop in tableData track by $index" + ng-repeat-complete row-id="{{prop.id}}"> + <td>{{prop.component}}</td> + <td>{{prop.id}}</td> + <td>{{prop.type}}</td> + <td>{{prop.value}}</td> + <td>{{prop.defValue}}</td> + <td>{{prop.desc}}</td> + </tr> + </table> + </div> + + </div> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.js b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.js new file mode 100644 index 00000000..cec0e70e --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.js @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Component Settings View Module + */ + +(function () { + 'use strict'; + + angular.module('ovSettings', []) + .controller('OvSettingsCtrl', + ['$log', '$scope', 'TableBuilderService', + + function ($log, $scope, tbs) { + tbs.buildTable({ + scope: $scope, + tag: 'setting' + }); + + $log.log('OvSettingsCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt new file mode 100644 index 00000000..61089bf6 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt @@ -0,0 +1,3 @@ +# ONOS Topology View + +Code and resources for implementing the client-side for the Topology View. diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css new file mode 100644 index 00000000..f4b089a0 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css @@ -0,0 +1,729 @@ +/* + * Copyright 2014,2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology View -- CSS file + */ + +/* --- Base SVG Layer --- */ + +#ov-topo svg { + /* prevents the little cut/copy/paste square that would appear on iPad */ + -webkit-user-select: none; +} +.light #ov-topo svg { + background-color: #fff; +} +.dark #ov-topo svg { + background-color: #2b2b2b; +} + + +/* --- "No Devices" Layer --- */ + +#ov-topo svg #topo-noDevsLayer { + visibility: hidden; +} + +.light #ov-topo svg .noDevsBird { + fill: #ecd; +} +.dark #ov-topo svg .noDevsBird { + fill: #683434; +} + +#ov-topo svg #topo-noDevsLayer text { + font-size: 60pt; + font-style: italic; +} +.light #ov-topo svg #topo-noDevsLayer text { + fill: #dde; +} +.dark #ov-topo svg #topo-noDevsLayer text { + fill: #3b3b4f; +} + + +/* --- Topo Map --- */ + +#ov-topo svg #topo-map { + stroke-width: 2px; + fill: transparent; +} + +.light #ov-topo svg #topo-map { + stroke: #ddd; +} +.dark #ov-topo svg #topo-map { + stroke: #444; +} + + +/* --- Topo Summary Panel --- */ + +#topo-p-summary { + /* Base css from panel.css */ +} + +/* --- Topo Detail Panel --- */ + +#topo-p-detail { + /* Base css from panel.css */ + top: 320px; +} +html[data-platform='iPad'] #topo-p-detail { + top: 336px; +} + +#topo-p-detail .actionBtns .actionBtn { + display: inline-block; +} +#topo-p-detail .actionBtns .actionBtn svg { + width: 30px; + height: 30px; +} + +/* --- general topo-panel styling --- */ + +.topo-p div.header div.icon { + vertical-align: middle; + display: inline-block; +} +.topo-p div.body { + overflow-y: scroll; +} + +.topo-p div.body::-webkit-scrollbar { + display: none; +} + +.topo-p svg { + display: inline-block; + width: 42px; + height: 42px; +} + +.light .topo-p svg .glyph { + fill: #222; +} + +.dark .topo-p svg .glyph.overlay { + fill: #222; +} + +.dark .topo-p svg .glyph { + fill: #ddd; +} +.light .topo-p svg .glyph.overlay { + fill: #fff; +} + + +.topo-p h2 { + padding: 0 4px; + margin: 0; + word-wrap: break-word; + display: inline-block; + width: 210px; + vertical-align: middle; +} +.light .topo-p h2 { + color: black; +} +.dark .topo-p h2 { + color: #ddd; +} + +.topo-p h3 { + padding: 0 4px; + margin: 0; + word-wrap: break-word; + top: 20px; + left: 50px; +} +.light .topo-p h3 { + color: black; +} +.dark .topo-p h3 { + color: #ddd; +} + +.topo-p p, table { + padding: 4px; + margin: 0; +} + +.topo-p td { + word-wrap: break-word; +} +.topo-p td.label { + font-style: italic; + padding-right: 12px; + /* works for both light and dark themes ... */ + color: #777; +} +.topo-p td.value { +} + +.topo-p hr { + height: 1px; + border: 0; +} +.light .topo-p hr { + background-color: #ccc; + color: #ccc; +} +.dark .topo-p hr { + background-color: #888; + color: #888; +} + +/* --- Topo Instance Panel --- */ + +#topo-p-instance { + height: 100px; +} + +#topo-p-instance div.onosInst { + display: inline-block; + width: 170px; + height: 85px; + cursor: pointer; +} + +#topo-p-instance svg rect { + stroke-width: 3.5; +} +#topo-p-instance .online svg rect { + opacity: 1; +} +.light #topo-p-instance svg rect { + fill: #ccc; + stroke: #aaa; +} +.light #topo-p-instance .online svg rect { + fill: #9cf; + stroke: #555; +} +.dark #topo-p-instance svg rect { + fill: #666; + stroke: #222; +} +.dark #topo-p-instance .online svg rect { + fill: #9cf; + stroke: #999; +} + + +#topo-p-instance svg .glyph { + fill: #888; + fill-rule: evenodd; +} +#topo-p-instance .online svg .glyph { + fill: #000; +} + +#topo-p-instance svg .badgeIcon { + fill-rule: evenodd; + opacity: 0.4; +} +.light #topo-p-instance svg .badgeIcon { + fill: #777; +} +.dark #topo-p-instance svg .badgeIcon { + fill: #555; +} + +#topo-p-instance .online svg .badgeIcon { + opacity: 1.0; +} +.light #topo-p-instance .online svg .badgeIcon { + fill: #fff; +} +.dark #topo-p-instance .online svg .badgeIcon { + fill: #fff; +} + +#topo-p-instance svg text { + text-anchor: middle; + opacity: 0.3; +} +#topo-p-instance .online svg text { + opacity: 1.0; +} +.light #topo-p-instance svg text { + fill: #444; +} +.light #topo-p-instance .online svg text { + fill: #eee; +} +.dark #topo-p-instance svg text { + fill: #aaa; +} +.dark #topo-p-instance .online svg text { + fill: #ccc; +} + +#topo-p-instance svg text.instTitle { + font-size: 11pt; + font-weight: bold; +} +#topo-p-instance svg text.instLabel { + font-size: 9pt; + font-style: italic; +} + +#topo-p-instance .onosInst.mastership { + opacity: 0.3; +} +#topo-p-instance .onosInst.mastership.affinity { + opacity: 1.0; +} +.light #topo-p-instance .onosInst.mastership.affinity svg rect { + filter: url(#blue-glow); +} +.dark #topo-p-instance .onosInst.mastership.affinity svg rect { + filter: url(#yellow-glow); +} +.light.firefox #topo-p-instance .onosInst.mastership.affinity svg rect { + filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow"); +} +.dark.firefox #topo-p-instance .onosInst.mastership.affinity svg rect { + filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow"); +} + + +/* --- Toolbar --- */ + +#toolbar-topo-tbar .tbar-row.right { + width: 100%; +} + +#toolbar-topo-tbar .tbar-row-text { + height: 21px; + text-align: right; + padding: 8px 60px 0 0; + font-style: italic; +} + + +/* --- Topo Nodes --- */ + +#ov-topo svg .suppressed { + opacity: 0.5 !important; +} + +#ov-topo svg .suppressedmax { + opacity: 0.2 !important; +} + +#ov-topo svg .node { + cursor: pointer; +} + +.light #ov-topo svg .node.selected rect, +.light #ov-topo svg .node.selected circle { + fill: #f90; + filter: url(#blue-glow); +} +.dark #ov-topo svg .node.selected rect, +.dark #ov-topo svg .node.selected circle { + fill: #f90; + filter: url(#yellow-glow); +} +.light.firefox #ov-topo svg .node.selected rect, +.light.firefox #ov-topo svg .node.selected circle { + filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow"); +} +.dark.firefox #ov-topo svg .node.selected rect, +.dark.firefox #ov-topo svg .node.selected circle { + filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow"); +} + +#ov-topo svg .node text { + pointer-events: none; +} + +/* Device Nodes */ + +#ov-topo svg .node.device { +} + +#ov-topo svg .node.device rect { + stroke-width: 1.5; +} + +#ov-topo svg .node.device.fixed rect { + stroke-width: 1.5; +} +.light #ov-topo svg .node.device.fixed rect { + stroke: #aaa; +} +.dark #ov-topo svg .node.device.fixed rect { + stroke: #999; +} + +/* note: device is offline without the 'online' class */ +.light #ov-topo svg .node.device { + fill: #777; +} +.dark #ov-topo svg .node.device { + fill: #555; +} + +.light #ov-topo svg .node.device rect { + stroke: #666; +} +.light #ov-topo svg .node.device rect { + stroke: #999; +} + +.light #ov-topo svg .node.device.online { + fill: #6e7fa3; +} +.dark #ov-topo svg .node.device.online { + fill: #4E5C7F; +} + +/* note: device is offline without the 'online' class */ +#ov-topo svg .node.device text { + fill: #bbb; + font: 10pt sans-serif; +} + +#ov-topo svg .node.device.online text { + fill: white; +} + +#ov-topo svg .node.device .svgIcon rect { + fill: #aaa; +} +#ov-topo svg .node.device .svgIcon use { + fill: #777; +} +#ov-topo svg .node.device.selected .svgIcon rect { + fill: #f90; +} +#ov-topo svg .node.device.online .svgIcon rect { + fill: #ccc; +} +#ov-topo svg .node.device.online .svgIcon use { + fill: #000; +} +#ov-topo svg .node.device.online.selected .svgIcon rect { + fill: #f90; +} + + +/* Host Nodes */ + +#ov-topo svg .node.host { +} + +#ov-topo svg .node.host text { + stroke: none; + font: 9pt sans-serif; +} +.light #ov-topo svg .node.host text { + fill: #846; +} +.dark #ov-topo svg .node.host text { + fill: #BB809D; +} + +.light svg .node.host circle { + stroke: #000; + fill: #edb; +} +.dark svg .node.host circle { + stroke: #eee; + fill: #B2A180; +} + +.light svg .node.host .svgIcon { + fill: #444; +} +.dark svg .node.host .svgIcon { + fill: #222; +} + +/* --- Topo Links --- */ + +#ov-topo svg .link { + opacity: .9; +} + +#ov-topo svg .link.selected, +#ov-topo svg .link.enhanced { + stroke-width: 4.5px; +} +.light #ov-topo svg .link.selected, +.light #ov-topo svg .link.enhanced { + filter: url(#blue-glow); +} +.dark #ov-topo svg .link.selected, +.dark #ov-topo svg .link.enhanced { + filter: url(#yellow-glow); +} +.light.firefox #ov-topo svg .link.selected, +.light.firefox #ov-topo svg .link.enhanced { + filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow"); +} +.dark.firefox #ov-topo svg .link.selected, +.dark.firefox #ov-topo svg .link.enhanced { + filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow"); + +} + +#ov-topo svg .link.inactive { + opacity: .5; + stroke-dasharray: 8 4; +} + +#ov-topo svg .link.secondary { + stroke-width: 3px; +} +.light #ov-topo svg .link.secondary { + stroke: rgba(0,153,51,0.5); +} +.dark #ov-topo svg .link.secondary { + stroke: rgba(121,231,158,0.5); +} + +/* Port traffic color visualization for Kbps, Mbps, and Gbps */ + +.light #ov-topo svg .link.secondary.port-traffic-Kbps { + stroke: rgb(0,153,51); + stroke-width: 5.0; +} +.dark #ov-topo svg .link.secondary.port-traffic-Kbps { + stroke: rgb(98, 153, 118); + stroke-width: 5.0; +} + +.light #ov-topo svg .link.secondary.port-traffic-Mbps { + stroke: rgb(128,145,27); + stroke-width: 6.5; +} +.dark #ov-topo svg .link.secondary.port-traffic-Mbps { + stroke: rgb(91, 109, 54); + stroke-width: 6.5; +} + +.light #ov-topo svg .link.secondary.port-traffic-Gbps { + stroke: rgb(255, 137, 3); + stroke-width: 8.0; +} +.dark #ov-topo svg .link.secondary.port-traffic-Gbps { + stroke: rgb(174, 119, 55); + stroke-width: 8.0; +} + +.light #ov-topo svg .link.secondary.port-traffic-Gbps-choked { + stroke: rgb(183, 30, 21); + stroke-width: 8.0; +} +.dark #ov-topo svg .link.secondary.port-traffic-Gbps-choked { + stroke: rgb(127, 40, 39); + stroke-width: 8.0; +} + + +#ov-topo svg .link.animated { + stroke-dasharray: 8 5; + animation: ants 5s infinite linear; + /* below line will be added via Javascript based on path */ + /*animation-direction: reverse;*/ +} +@keyframes ants { + from { + stroke-dashoffset: 0; + } + to { + stroke-dashoffset: 400; + } +} + +#ov-topo svg .link.primary { + stroke-width: 4px; +} +.light #ov-topo svg .link.primary { + stroke: #ffA300; +} +.dark #ov-topo svg .link.primary { + stroke: #D58E0F; +} + +#ov-topo svg .link.secondary.optical { + stroke-width: 4px; +} +.light #ov-topo svg .link.secondary.optical { + stroke: rgba(128,64,255,0.5); +} +.dark #ov-topo svg .link.secondary.optical { + stroke: rgba(164,139,215,0.5); +} + +#ov-topo svg .link.primary.optical { + stroke-width: 6px; +} +.light #ov-topo svg .link.primary.optical { + stroke: #74f; +} +.dark #ov-topo svg .link.primary.optical { + stroke: #7352CD; +} + +/* Link Labels */ + +#ov-topo svg .linkLabel rect { + stroke: none; +} +.light #ov-topo svg .linkLabel rect { + fill: #eee; +} +.dark #ov-topo svg .linkLabel rect { + fill: #555; +} + +#ov-topo svg .linkLabel text { + text-anchor: middle; + stroke-width: 0.1; + font-size: 9pt; +} +.light #ov-topo svg .linkLabel text { + fill: #444; +} +.dark #ov-topo svg .linkLabel text { + fill: #eee; +} + +/* Port Labels */ + +#ov-topo svg .portLabel rect { + stroke: none; +} +.light #ov-topo svg .portLabel rect { + fill: #eee; +} +.dark #ov-topo svg .portLabel rect { + fill: #222; +} + +#ov-topo svg .portLabel text { + text-anchor: middle; + stroke-width: 0.1; + font-size: 11pt; +} +.light #ov-topo svg .portLabel text { + fill: #444; +} +.dark #ov-topo svg .portLabel text { + fill: #eee; +} + +/* Number of Links Labels */ +#ov-topo line.numLinkHash { + stroke-width: 3; +} + +#ov-topo text.numLinkText { + font-size: 15pt; +} + +#ov-topo text.numLinkText { + text-anchor: middle; +} + +.light #ov-topo text.numLinkText { + fill: #444; +} +.dark #ov-topo text.numLinkText { + fill: #eee; +} + +/* ------------------------------------------------- */ +/* Sprite Layer */ + +#ov-topo svg #topo-sprites use { + stroke-width: 2; +} +#ov-topo svg #topo-sprites text { + text-anchor: middle; + font-size: 20pt; + font-style: italic; +} + +.light #ov-topo svg #topo-sprites .gold1 use { + stroke: #fda; + fill: none; +} +.dark #ov-topo svg #topo-sprites .gold1 use { + stroke: #541; + fill: none; +} +.light #ov-topo svg #topo-sprites .gold1 text { + fill: #eda; +} +.dark #ov-topo svg #topo-sprites .gold1 text { + fill: #543; +} + +.light #ov-topo svg #topo-sprites .blue1 use { + stroke: #bbd; + fill: none; +} +.dark #ov-topo svg #topo-sprites .blue1 use { + stroke: #445; + fill: none; +} +.light #ov-topo svg #topo-sprites .blue1 text { + fill: #cce; +} +.dark #ov-topo svg #topo-sprites .blue1 text { + fill: #446; +} + +.light #ov-topo svg #topo-sprites .gray1 use { + stroke: #ccc; + fill: none; +} +.dark #ov-topo svg #topo-sprites .gray1 use { + stroke: #333; + fill: none; +} +.light #ov-topo svg #topo-sprites .gray1 text { + fill: #ddd; +} +.dark #ov-topo svg #topo-sprites .gray1 text { + fill: #444; +} + +/* fills */ +.light #ov-topo svg #topo-sprites use.fill-gray2 { + fill: #eee; +} +.dark #ov-topo svg #topo-sprites use.fill-gray2 { + fill: #444; +} + +.light #ov-topo svg #topo-sprites use.fill-blue2 { + fill: #bce; +} +.dark #ov-topo svg #topo-sprites use.fill-blue2 { + fill: #447; +} + diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html new file mode 100644 index 00000000..8a64abb5 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html @@ -0,0 +1,7 @@ +<!-- Topology View partial HTML --> +<div id="ov-topo"> + <svg viewBox="0 0 1000 1000" + resize offset-height="56" offset-width="12" + notifier="notifyResize()"> + </svg> +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js new file mode 100644 index 00000000..21894100 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js @@ -0,0 +1,529 @@ +/* + * Copyright 2014,2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology View Module + */ + +(function () { + 'use strict'; + + var moduleDependencies = [ + 'ngCookies', + 'onosUtil', + 'onosSvg', + 'onosRemote' + ]; + + // references to injected services etc. + var $scope, $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps, + tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, ttip, tov; + + // DOM elements + var ovtopo, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer; + + // Internal state + var zoomer, actionMap; + + // --- Short Cut Keys ------------------------------------------------ + + function setUpKeys(overlayKeys) { + // key bindings need to be made after the services have been injected + // thus, deferred to here... + actionMap = { + I: [toggleInstances, 'Toggle ONOS instances panel'], + O: [toggleSummary, 'Toggle ONOS summary panel'], + D: [toggleUseDetailsFlag, 'Disable / enable details panel'], + + H: [toggleHosts, 'Toggle host visibility'], + M: [toggleOffline, 'Toggle offline visibility'], + P: [togglePorts, 'Toggle Port Highlighting'], + dash: [tfs.showBadLinks, 'Show bad links'], + B: [toggleMap, 'Toggle background map'], + S: [toggleSprites, 'Toggle sprite layer'], + + //X: [toggleNodeLock, 'Lock / unlock node positions'], + Z: [tos.toggleOblique, 'Toggle oblique view (Experimental)'], + N: [fltr.clickAction, 'Cycle node layers'], + L: [tfs.cycleDeviceLabels, 'Cycle device labels'], + U: [tfs.unpin, 'Unpin node (hover mouse over)'], + R: [resetZoom, 'Reset pan / zoom'], + dot: [ttbs.toggleToolbar, 'Toggle Toolbar'], + + E: [equalizeMasters, 'Equalize mastership roles'], + + esc: handleEscape, + + _keyListener: ttbs.keyListener, + + _helpFormat: [ + ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B', 'S' ], + ['X', 'Z', 'N', 'L', 'U', 'R', '-', 'E', '-', 'dot'], + [] // this column reserved for overlay actions + ] + }; + + if (fs.isO(overlayKeys)) { + mergeKeys(overlayKeys); + } + + ks.keyBindings(actionMap); + + ks.gestureNotes([ + ['click', 'Select the item and show details'], + ['shift-click', 'Toggle selection state'], + ['drag', 'Reposition (and pin) device / host'], + ['cmd-scroll', 'Zoom in / out'], + ['cmd-drag', 'Pan'] + ]); + } + + // when a topology overlay is activated, we need to bind their keystrokes + // and include them in the quick-help panel + function mergeKeys(extra) { + var _hf = actionMap._helpFormat[2]; + extra._keyOrder.forEach(function (k) { + var d = extra[k], + cb = d && d.cb, + tt = d && d.tt; + // NOTE: ignore keys that are already defined + if (d && !actionMap[k]) { + actionMap[k] = [cb, tt]; + _hf.push(k); + } + }); + } + + // --- Keystroke functions ------------------------------------------- + + function toggleInstances(x) { + updatePrefsState('insts', tis.toggle(x)); + tfs.updateDeviceColors(); + } + + function toggleSummary(x) { + updatePrefsState('summary', tps.toggleSummary(x)); + } + + function toggleUseDetailsFlag(x) { + updatePrefsState('detail', tps.toggleUseDetailsFlag(x)); + } + + function toggleHosts(x) { + updatePrefsState('hosts', tfs.toggleHosts(x)); + } + + function toggleOffline(x) { + updatePrefsState('offdev', tfs.toggleOffline(x)); + } + + function togglePorts(x) { + updatePrefsState('porthl', tfs.togglePorts(x)); + } + + function _togSvgLayer(x, G, tag, what) { + var on = (x === 'keyev') ? !sus.visible(G) : !!x, + verb = on ? 'Show' : 'Hide'; + sus.visible(G, on); + updatePrefsState(tag, on); + flash.flash(verb + ' ' + what); + } + + function toggleMap(x) { + _togSvgLayer(x, mapG, 'bg', 'background map'); + } + + function toggleSprites(x) { + _togSvgLayer(x, spriteG, 'spr', 'sprite layer'); + } + + function resetZoom() { + zoomer.reset(); + flash.flash('Pan and zoom reset'); + } + + function equalizeMasters() { + wss.sendEvent('equalizeMasters'); + flash.flash('Equalizing master roles'); + } + + function handleEscape() { + if (tis.showMaster()) { + // if an instance is selected, cancel the affinity mapping + tis.cancelAffinity() + + } else if (tov.hooks.escape()) { + // else if the overlay consumed the ESC event... + // (work already done) + + } else if (tss.deselectAll()) { + // else if we have node selections, deselect them all + // (work already done) + + } else if (tls.deselectLink()) { + // else if we have a link selected, deselect it + // (work already done) + + } else if (tis.isVisible()) { + // else if the Instance Panel is visible, hide it + tis.hide(); + tfs.updateDeviceColors(); + + } else if (tps.summaryVisible()) { + // else if the Summary Panel is visible, hide it + tps.hideSummaryPanel(); + } + } + + // --- Toolbar Functions --------------------------------------------- + + function notValid(what) { + $log.warn('topo.js getActionEntry(): Not a valid ' + what); + } + + function getActionEntry(key) { + var entry; + + if (!key) { + notValid('key'); + return null; + } + + entry = actionMap[key]; + + if (!entry) { + notValid('actionMap entry'); + return null; + } + return fs.isA(entry) || [entry, '']; + } + + function setUpToolbar() { + ttbs.init({ + getActionEntry: getActionEntry, + setUpKeys: setUpKeys + }); + ttbs.createToolbar(); + } + + // --- Glyphs, Icons, and the like ----------------------------------- + + function setUpDefs() { + defs = svg.append('defs'); + gs.loadDefs(defs); + sus.loadGlowDefs(defs); + } + + + // --- Pan and Zoom -------------------------------------------------- + + // zoom enabled predicate. ev is a D3 source event. + function zoomEnabled(ev) { + return fs.isMobile() || (ev.metaKey || ev.altKey); + } + + function zoomCallback() { + var sc = zoomer.scale(), + tr = zoomer.translate(); + + ps.setPrefs('topo_zoom', {tx:tr[0], ty:tr[1], sc:sc}); + + // keep the map lines constant width while zooming + mapG.style('stroke-width', (2.0 / sc) + 'px'); + } + + function setUpZoom() { + zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer'); + zoomer = zs.createZoomer({ + svg: svg, + zoomLayer: zoomLayer, + zoomEnabled: zoomEnabled, + zoomCallback: zoomCallback + }); + } + + + // callback invoked when the SVG view has been resized.. + function svgResized(s) { + tfs.newDim([s.width, s.height]); + } + + // --- Background Map ------------------------------------------------ + + function setUpNoDevs() { + var g, box; + noDevsLayer = svg.append('g').attr({ + id: 'topo-noDevsLayer', + transform: sus.translate(500,500) + }); + // Note, SVG viewbox is '0 0 1000 1000', defined in topo.html. + // We are translating this layer to have its origin at the center + + g = noDevsLayer.append('g'); + gs.addGlyph(g, 'bird', 100).attr('class', 'noDevsBird'); + g.append('text').text('No devices are connected') + .attr({ x: 120, y: 80}); + + box = g.node().getBBox(); + box.x -= box.width/2; + box.y -= box.height/2; + g.attr('transform', sus.translate(box.x, box.y)); + + showNoDevs(true); + } + + function showNoDevs(b) { + sus.visible(noDevsLayer, b); + } + + + var countryFilters = { + world: function (c) { + return c.properties.continent !== 'Antarctica'; + }, + + // NOTE: for "usa" we are using our hand-crafted topojson file + + s_america: function (c) { + return c.properties.continent === 'South America'; + }, + + japan: function (c) { + return c.properties.geounit === 'Japan'; + }, + + europe: function (c) { + return c.properties.continent === 'Europe'; + }, + + italy: function (c) { + return c.properties.geounit === 'Italy'; + }, + + uk: function (c) { + // technically, Ireland is not part of the United Kingdom, + // but the map looks weird without it showing. + return c.properties.adm0_a3 === 'GBR' || + c.properties.adm0_a3 === 'IRL'; + }, + + s_korea: function (c) { + return c.properties.adm0_a3 === 'KOR'; + }, + + australia: function (c) { + return c.properties.adm0_a3 === 'AUS'; + } + }; + + + function setUpMap($loc) { + var s1 = $loc.search().mapid, + s2 = ps.getPrefs('topo_mapid'), + mapId = s1 || (s2 && s2.id) || 'usa', + promise, + cfilter, + opts; + + mapG = zoomLayer.append('g').attr('id', 'topo-map'); + if (mapId === 'usa') { + promise = ms.loadMapInto(mapG, '*continental_us'); + } else { + ps.setPrefs('topo_mapid', {id:mapId}); + cfilter = countryFilters[mapId] || countryFilters.world; + opts = { countryFilter: cfilter }; + promise = ms.loadMapRegionInto(mapG, opts); + } + return promise; + } + + function opacifyMap(b) { + mapG.transition() + .duration(1000) + .attr('opacity', b ? 1 : 0); + } + + function setUpSprites($loc, tspr) { + var s1 = $loc.search().sprites, + s2 = ps.getPrefs('topo_sprites'), + sprId = s1 || (s2 && s2.id); + + spriteG = zoomLayer.append ('g').attr('id', 'topo-sprites'); + if (sprId) { + ps.setPrefs('topo_sprites', {id:sprId}); + tspr.loadSprites(spriteG, defs, sprId); + } + } + + // --- User Preferemces ---------------------------------------------- + + var prefsState = {}; + + function updatePrefsState(what, b) { + prefsState[what] = b ? 1 : 0; + ps.setPrefs('topo_prefs', prefsState); + } + + + function restoreConfigFromPrefs() { + // NOTE: toolbar will have set this for us.. + prefsState = ps.asNumbers(ps.getPrefs('topo_prefs')); + + $log.debug('TOPO- Prefs State:', prefsState); + + flash.enable(false); + toggleInstances(prefsState.insts); + toggleSummary(prefsState.summary); + toggleUseDetailsFlag(prefsState.detail); + toggleHosts(prefsState.hosts); + toggleOffline(prefsState.offdev); + togglePorts(prefsState.porthl); + toggleMap(prefsState.bg); + toggleSprites(prefsState.spr); + flash.enable(true); + } + + + // somewhat hackish, because summary update cannot happen until we + // have opened the websocket to the server; hence this extra function + // invoked after tes.start() + function restoreSummaryFromPrefs() { + prefsState = ps.asNumbers(ps.getPrefs('topo_prefs')); + $log.debug('TOPO- Prefs SUMMARY State:', prefsState.summary); + + flash.enable(false); + toggleSummary(prefsState.summary); + flash.enable(true); + } + + + // --- Controller Definition ----------------------------------------- + + angular.module('ovTopo', moduleDependencies) + .controller('OvTopoCtrl', ['$scope', '$log', '$location', '$timeout', + '$cookies', 'FnService', 'MastService', 'KeyService', 'ZoomService', + 'GlyphService', 'MapService', 'SvgUtilService', 'FlashService', + 'WebSocketService', 'PrefsService', + 'TopoEventService', 'TopoForceService', 'TopoPanelService', + 'TopoInstService', 'TopoSelectService', 'TopoLinkService', + 'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService', + 'TopoToolbarService', 'TopoSpriteService', 'TooltipService', + 'TopoOverlayService', + + function (_$scope_, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_, + _zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _tes_, _tfs_, + _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_, _ttbs_, tspr, + _ttip_, _tov_) { + var projection, + dim, + uplink = { + // provides function calls back into this space + showNoDevs: showNoDevs, + projection: function () { return projection; }, + zoomLayer: function () { return zoomLayer; }, + zoomer: function () { return zoomer; }, + opacifyMap: opacifyMap + }; + + $scope = _$scope_; + $log = _$log_; + $cookies = _$cookies_; + fs = _fs_; + ks = _ks_; + zs = _zs_; + gs = _gs_; + ms = _ms_; + sus = _sus_; + flash = _flash_; + wss = _wss_; + ps = _ps_; + tes = _tes_; + tfs = _tfs_; + // TODO: consider funnelling actions through TopoForceService... + // rather than injecting references to these 'sub-modules', + // just so we can invoke functions on them. + tps = _tps_; + tis = _tis_; + tss = _tss_; + tls = _tls_; + tts = _tts_; + tos = _tos_; + fltr = _fltr_; + ttbs = _ttbs_; + ttip = _ttip_; + tov = _tov_; + + $scope.notifyResize = function () { + svgResized(fs.windowSize(mast.mastHeight())); + }; + + // Cleanup on destroyed scope.. + $scope.$on('$destroy', function () { + $log.log('OvTopoCtrl is saying Buh-Bye!'); + tes.stop(); + ks.unbindKeys(); + tps.destroyPanels(); + tis.destroyInst(); + tfs.destroyForce(); + ttbs.destroyToolbar(); + }); + + // svg layer and initialization of components + ovtopo = d3.select('#ov-topo'); + svg = ovtopo.select('svg'); + // set the svg size to match that of the window, less the masthead + svg.attr(fs.windowSize(mast.mastHeight())); + dim = [svg.attr('width'), svg.attr('height')]; + + setUpKeys(); + setUpToolbar(); + setUpDefs(); + setUpZoom(); + setUpNoDevs(); + setUpMap($loc).then( + function (proj) { + var z = ps.getPrefs('topo_zoom') || {tx:0, ty:0, sc:1}; + zoomer.panZoom([z.tx, z.ty], z.sc); + $log.debug('** Zoom restored:', z); + + projection = proj; + $log.debug('** We installed the projection:', proj); + flash.enable(false); + toggleMap(prefsState.bg); + flash.enable(true); + + // now we have the map projection, we are ready for + // the server to send us device/host data... + tes.start(); + // need to do the following so we immediately get + // the summary panel data back from the server + restoreSummaryFromPrefs(); + } + ); + setUpSprites($loc, tspr); + + forceG = zoomLayer.append('g').attr('id', 'topo-force'); + tfs.initForce(svg, forceG, uplink, dim); + tis.initInst({ showMastership: tfs.showMastership }); + tps.initPanels(); + + // temporary solution for persisting user settings + restoreConfigFromPrefs(); + + $log.debug('registered overlays...', tov.list()); + $log.log('OvTopoCtrl has been created'); + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js new file mode 100644 index 00000000..d29748b1 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js @@ -0,0 +1,576 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology D3 Module. + Functions for manipulating the D3 visualizations of the Topology + */ + +(function () { + 'use strict'; + + // injected refs + var $log, fs, sus, is, ts; + + // api to topoForce + var api; + /* + node() // get ref to D3 selection of nodes + link() // get ref to D3 selection of links + linkLabel() // get ref to D3 selection of link labels + instVisible() // true if instances panel is visible + posNode() // position node + showHosts() // true if hosts are to be shown + restyleLinkElement() // update link styles based on backing data + updateLinkLabelModel() // update backing data for link labels + */ + + // configuration + var devCfg = { + xoff: -20, + yoff: -18 + }, + labelConfig = { + imgPad: 16, + padLR: 4, + padTB: 3, + marginLR: 3, + marginTB: 2, + port: { + gap: 3, + width: 18, + height: 14 + } + }, + icfg; + + // internal state + var deviceLabelIndex = 0, + hostLabelIndex = 0; + + + var dCol = { + black: '#000', + paleblue: '#acf', + offwhite: '#ddd', + darkgrey: '#444', + midgrey: '#888', + lightgrey: '#bbb', + orange: '#f90' + }; + + // note: these are the device icon colors without affinity + var dColTheme = { + light: { + rfill: dCol.offwhite, + online: { + glyph: dCol.darkgrey, + rect: dCol.paleblue + }, + offline: { + glyph: dCol.midgrey, + rect: dCol.lightgrey + } + }, + dark: { + rfill: dCol.midgrey, + online: { + glyph: dCol.darkgrey, + rect: dCol.paleblue + }, + offline: { + glyph: dCol.midgrey, + rect: dCol.darkgrey + } + } + }; + + function devBaseColor(d) { + var o = d.online ? 'online' : 'offline'; + return dColTheme[ts.theme()][o]; + } + + function setDeviceColor(d) { + var o = d.online, + s = d.el.classed('selected'), + c = devBaseColor(d), + a = instColor(d.master, o), + icon = d.el.select('g.deviceIcon'), + g, r; + + if (s) { + g = c.glyph; + r = dCol.orange; + } else if (api.instVisible()) { + g = o ? a : c.glyph; + r = o ? c.rfill : a; + } else { + g = c.glyph; + r = c.rect; + } + + icon.select('use').style('fill', g); + icon.select('rect').style('fill', r); + } + + function instColor(id, online) { + return sus.cat7().getColor(id, !online, ts.theme()); + } + + // ==== + + function incDevLabIndex() { + deviceLabelIndex = (deviceLabelIndex+1) % 3; + switch(deviceLabelIndex) { + case 0: return 'Hide device labels'; + case 1: return 'Show friendly device labels'; + case 2: return 'Show device ID labels'; + } + } + + // Returns the newly computed bounding box of the rectangle + function adjustRectToFitText(n) { + var text = n.select('text'), + box = text.node().getBBox(), + lab = labelConfig; + + text.attr('text-anchor', 'middle') + .attr('y', '-0.8em') + .attr('x', lab.imgPad/2); + + // translate the bbox so that it is centered on [x,y] + box.x = -box.width / 2; + box.y = -box.height / 2; + + // add padding + box.x -= (lab.padLR + lab.imgPad/2); + box.width += lab.padLR * 2 + lab.imgPad; + box.y -= lab.padTB; + box.height += lab.padTB * 2; + + return box; + } + + function hostLabel(d) { + var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0; + return d.labels[idx]; + } + function deviceLabel(d) { + var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0; + return d.labels[idx]; + } + function trimLabel(label) { + return (label && label.trim()) || ''; + } + + function emptyBox() { + return { + x: -2, + y: -2, + width: 4, + height: 4 + }; + } + + + function updateDeviceLabel(d) { + var label = trimLabel(deviceLabel(d)), + noLabel = !label, + node = d.el, + dim = icfg.device.dim, + box, dx, dy; + + node.select('text') + .text(label) + .style('opacity', 0) + .transition() + .style('opacity', 1); + + if (noLabel) { + box = emptyBox(); + dx = -dim/2; + dy = -dim/2; + } else { + box = adjustRectToFitText(node); + dx = box.x + devCfg.xoff; + dy = box.y + devCfg.yoff; + } + + node.select('rect') + .transition() + .attr(box); + + node.select('g.deviceIcon') + .transition() + .attr('transform', sus.translate(dx, dy)); + } + + function updateHostLabel(d) { + var label = trimLabel(hostLabel(d)); + d.el.select('text').text(label); + } + + function updateDeviceColors(d) { + if (d) { + setDeviceColor(d); + } else { + api.node().filter('.device').each(function (d) { + setDeviceColor(d); + }); + } + } + + + // ========================== + // updateNodes - subfunctions + + function deviceExisting(d) { + var node = d.el; + node.classed('online', d.online); + updateDeviceLabel(d); + api.posNode(d, true); + } + + function hostExisting(d) { + updateHostLabel(d); + api.posNode(d, true); + } + + function deviceEnter(d) { + var node = d3.select(this), + glyphId = d.type || 'unknown', + label = trimLabel(deviceLabel(d)), + //devCfg = deviceIconConfig, + noLabel = !label, + box, dx, dy, icon; + + d.el = node; + + node.append('rect').attr({ rx: 5, ry: 5 }); + node.append('text').text(label).attr('dy', '1.1em'); + box = adjustRectToFitText(node); + node.select('rect').attr(box); + + icon = is.addDeviceIcon(node, glyphId); + + if (noLabel) { + dx = -icon.dim/2; + dy = -icon.dim/2; + } else { + box = adjustRectToFitText(node); + dx = box.x + devCfg.xoff; + dy = box.y + devCfg.yoff; + } + + icon.attr('transform', sus.translate(dx, dy)); + } + + function hostEnter(d) { + var node = d3.select(this), + gid = d.type || 'unknown', + rad = icfg.host.radius, + r = d.type ? rad.withGlyph : rad.noGlyph, + textDy = r + 10; + + d.el = node; + sus.visible(node, api.showHosts()); + + is.addHostIcon(node, r, gid); + + node.append('text') + .text(hostLabel) + .attr('dy', textDy) + .attr('text-anchor', 'middle'); + } + + function hostExit(d) { + var node = d.el; + node.select('use') + .style('opacity', 0.5) + .transition() + .duration(800) + .style('opacity', 0); + + node.select('text') + .style('opacity', 0.5) + .transition() + .duration(800) + .style('opacity', 0); + + node.select('circle') + .style('stroke-fill', '#555') + .style('fill', '#888') + .style('opacity', 0.5) + .transition() + .duration(1500) + .attr('r', 0); + } + + function deviceExit(d) { + var node = d.el; + node.select('use') + .style('opacity', 0.5) + .transition() + .duration(800) + .style('opacity', 0); + + node.selectAll('rect') + .style('stroke-fill', '#555') + .style('fill', '#888') + .style('opacity', 0.5); + } + + + // ========================== + // updateLinks - subfunctions + + function linkEntering(d) { + var link = d3.select(this); + d.el = link; + api.restyleLinkElement(d); + if (d.type() === 'hostLink') { + sus.visible(link, api.showHosts()); + } + } + + var linkLabelOffset = '0.3em'; + + function applyLinkLabels() { + var entering; + + api.updateLinkLabelModel(); + + // for elements already existing, we need to update the text + // and adjust the rectangle size to fit + api.linkLabel().each(function (d) { + var el = d3.select(this), + rect = el.select('rect'), + text = el.select('text'); + text.text(d.label); + rect.attr(rectAroundText(el)); + }); + + entering = api.linkLabel().enter().append('g') + .classed('linkLabel', true) + .attr('id', function (d) { return d.id; }); + + entering.each(function (d) { + var el = d3.select(this), + rect, + text; + + if (d.ldata.type() === 'hostLink') { + el.classed('hostLinkLabel', true); + sus.visible(el, api.showHosts()); + } + + d.el = el; + rect = el.append('rect'); + text = el.append('text').text(d.label); + rect.attr(rectAroundText(el)); + text.attr('dy', linkLabelOffset); + + el.attr('transform', transformLabel(d.ldata.position)); + }); + + // Remove any labels that are no longer required. + api.linkLabel().exit().remove(); + } + + function rectAroundText(el) { + var text = el.select('text'), + box = text.node().getBBox(); + + // translate the bbox so that it is centered on [x,y] + box.x = -box.width / 2; + box.y = -box.height / 2; + + // add padding + box.x -= 1; + box.width += 2; + return box; + } + + function transformLabel(p) { + var dx = p.x2 - p.x1, + dy = p.y2 - p.y1, + xMid = dx/2 + p.x1, + yMid = dy/2 + p.y1; + return sus.translate(xMid, yMid); + } + + function applyPortLabels(data, portLabelG) { + var entering = portLabelG.selectAll('.portLabel') + .data(data).enter().append('g') + .classed('portLabel', true) + .attr('id', function (d) { return d.id; }); + + entering.each(function (d) { + var el = d3.select(this), + rect = el.append('rect'), + text = el.append('text').text(d.num); + + rect.attr(rectAroundText(el)); + text.attr('dy', linkLabelOffset); + el.attr('transform', sus.translate(d.x, d.y)); + }); + } + + function labelPoint(linkPos) { + var lengthUpLine = 1 / 3, + dx = linkPos.x2 - linkPos.x1, + dy = linkPos.y2 - linkPos.y1, + movedX = dx * lengthUpLine, + movedY = dy * lengthUpLine; + + return { + x: movedX, + y: movedY + }; + } + + function calcGroupPos(linkPos) { + var moved = labelPoint(linkPos); + return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y); + } + + // calculates where on the link that the hash line for 5+ label appears + function hashAttrs(linkPos) { + var hashLength = 25, + halfLength = hashLength / 2, + dx = linkPos.x2 - linkPos.x1, + dy = linkPos.y2 - linkPos.y1, + length = Math.sqrt((dx * dx) + (dy * dy)), + moveAmtX = (dx / length) * halfLength, + moveAmtY = (dy / length) * halfLength, + mid = labelPoint(linkPos), + angle = Math.atan(dy / dx) + 45; + + return { + x1: mid.x - moveAmtX, + y1: mid.y - moveAmtY, + x2: mid.x + moveAmtX, + y2: mid.y + moveAmtY, + stroke: api.linkConfig()[ts.theme()].baseColor, + transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')' + }; + } + + function textLabelPos(linkPos) { + var point = labelPoint(linkPos), + dist = 20; + return { + x: point.x + dist, + y: point.y + dist + }; + } + + function applyNumLinkLabels(data, lblsG) { + var labels = lblsG.selectAll('g.numLinkLabel') + .data(data, function (d) { return 'pair-' + d.id; }), + entering; + + // update existing labels + labels.each(function (d) { + var el = d3.select(this); + + el.attr({ + transform: function (d) { return calcGroupPos(d.linkCoords); } + }); + el.select('line') + .attr(hashAttrs(d.linkCoords)); + el.select('text') + .attr(textLabelPos(d.linkCoords)) + .text(d.num); + }); + + // add new labels + entering = labels + .enter() + .append('g') + .attr({ + transform: function (d) { return calcGroupPos(d.linkCoords); }, + id: function (d) { return 'pair-' + d.id; } + }) + .classed('numLinkLabel', true); + + entering.each(function (d) { + var el = d3.select(this); + + el.append('line') + .classed('numLinkHash', true) + .attr(hashAttrs(d.linkCoords)); + el.append('text') + .classed('numLinkText', true) + .attr(textLabelPos(d.linkCoords)) + .text(d.num); + }); + + // remove old labels + labels.exit().remove(); + } + + // ========================== + // Module definition + + angular.module('ovTopo') + .factory('TopoD3Service', + ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService', + + function (_$log_, _fs_, _sus_, _is_, _ts_) { + $log = _$log_; + fs = _fs_; + sus = _sus_; + is = _is_; + ts = _ts_; + + icfg = is.iconConfig(); + + function initD3(_api_) { + api = _api_; + } + + function destroyD3() { } + + return { + initD3: initD3, + destroyD3: destroyD3, + + incDevLabIndex: incDevLabIndex, + adjustRectToFitText: adjustRectToFitText, + hostLabel: hostLabel, + deviceLabel: deviceLabel, + trimLabel: trimLabel, + + updateDeviceLabel: updateDeviceLabel, + updateHostLabel: updateHostLabel, + updateDeviceColors: updateDeviceColors, + + deviceExisting: deviceExisting, + hostExisting: hostExisting, + deviceEnter: deviceEnter, + hostEnter: hostEnter, + hostExit: hostExit, + deviceExit: deviceExit, + + linkEntering: linkEntering, + applyLinkLabels: applyLinkLabels, + transformLabel: transformLabel, + applyPortLabels: applyPortLabels, + applyNumLinkLabels: applyNumLinkLabels + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js new file mode 100644 index 00000000..5fd38bf6 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js @@ -0,0 +1,131 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Event Module. + + Defines the conduit between the client and the server: + - provides a clean API for sending events to the server + - dispatches incoming events from the server to the appropriate sub-module + + */ + +(function () { + 'use strict'; + + // injected refs + var $log, $interval, wss, tps, tis, tfs, tss, tov, tspr; + + // internal state + var handlerMap, + openListener, + heartbeatTimer; + + var heartbeatPeriod = 9000; // 9 seconds + + // ========================== + + function createHandlerMap() { + handlerMap = { + showSummary: tps, + + showDetails: tss, + + showHighlights: tov, + + addInstance: tis, + updateInstance: tis, + removeInstance: tis, + + addDevice: tfs, + updateDevice: tfs, + removeDevice: tfs, + addHost: tfs, + updateHost: tfs, + removeHost: tfs, + addLink: tfs, + updateLink: tfs, + removeLink: tfs, + + spriteListResponse: tspr, + spriteDataResponse: tspr + }; + } + + function wsOpen(host, url) { + $log.debug('TOPO: web socket open - cluster node:', host, 'URL:', url); + // Request batch of initial data from the new server + wss.sendEvent('topoStart'); + } + + function cancelHeartbeat() { + if (heartbeatTimer) { + $interval.cancel(heartbeatTimer); + } + heartbeatTimer = null; + } + + function scheduleHeartbeat() { + cancelHeartbeat(); + heartbeatTimer = $interval(function () { + wss.sendEvent('topoHeartbeat'); + }, heartbeatPeriod); + } + + + angular.module('ovTopo') + .factory('TopoEventService', + ['$log', '$interval', 'WebSocketService', + 'TopoPanelService', 'TopoInstService', 'TopoForceService', + 'TopoSelectService', 'TopoOverlayService', 'TopoSpriteService', + + function (_$log_, _$interval_, _wss_, + _tps_, _tis_, _tfs_, _tss_, _tov_, _tspr_) { + $log = _$log_; + $interval = _$interval_; + wss = _wss_; + tps = _tps_; + tis = _tis_; + tfs = _tfs_; + tss = _tss_; + tov = _tov_; + tspr = _tspr_; + + createHandlerMap(); + + function start() { + openListener = wss.addOpenListener(wsOpen); + wss.bindHandlers(handlerMap); + wss.sendEvent('topoStart'); + scheduleHeartbeat(); + $log.debug('topo comms started'); + } + + function stop() { + cancelHeartbeat(); + wss.sendEvent('topoStop'); + wss.unbindHandlers(handlerMap); + wss.removeOpenListener(openListener); + openListener = null; + $log.debug('topo comms stopped'); + } + + return { + start: start, + stop: stop + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js new file mode 100644 index 00000000..f9b96ae8 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js @@ -0,0 +1,149 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology layer filtering Module. + Provides functionality to visually differentiate between the packet and + optical layers of the topology. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, fs, flash, tps, tts; + + // api to topoForce + var api; + /* + node() // get ref to D3 selection of nodes + link() // get ref to D3 selection of links + */ + + var smax = 'suppressedmax'; + + // which "layer" a particular item "belongs to" + var layerLookup = { + host: { + endstation: 'pkt', // default, if host event does not define type + router: 'pkt', + bgpSpeaker: 'pkt' + }, + device: { + switch: 'pkt', + roadm: 'opt' + }, + link: { + hostLink: 'pkt', + direct: 'pkt', + indirect: '', + tunnel: '', + optical: 'opt' + } + }, + // order of layer cycling in button + dispatch = [ + { + type: 'all', + action: function () { suppressLayers(false); }, + msg: 'All Layers Shown' + }, + { + type: 'pkt', + action: function () { showLayer('pkt'); }, + msg: 'Packet Layer Shown' + }, + { + type: 'opt', + action: function () { showLayer('opt'); }, + msg: 'Optical Layer Shown' + } + ], + layer = 0; + + function clickAction() { + layer = (layer + 1) % dispatch.length; + dispatch[layer].action(); + flash.flash(dispatch[layer].msg); + } + + function selected() { + return dispatch[layer].type; + } + + function inLayer(d, layer) { + var type = d.class === 'link' ? d.type() : d.type, + look = layerLookup[d.class], + lyr = look && look[type]; + return lyr === layer; + } + + function unsuppressLayer(which) { + api.node().each(function (d) { + var node = d.el; + if (inLayer(d, which)) { + node.classed(smax, false); + } + }); + + api.link().each(function (d) { + var link = d.el; + if (inLayer(d, which)) { + link.classed(smax, false); + } + }); + } + + function suppressLayers(b) { + api.node().classed(smax, b); + api.link().classed(smax, b); + } + + function showLayer(which) { + suppressLayers(true); + unsuppressLayer(which); + } + + // === ----------------------------------------------------- + // === MODULE DEFINITION === + + angular.module('ovTopo') + .factory('TopoFilterService', + ['$log', 'FnService', + 'FlashService', + 'TopoPanelService', + 'TopoTrafficService', + + function (_$log_, _fs_, _flash_, _tps_, _tts_) { + $log = _$log_; + fs = _fs_; + flash = _flash_; + tps = _tps_; + tts = _tts_; + + function initFilter(_api_) { + api = _api_; + } + + return { + initFilter: initFilter, + + clickAction: clickAction, + selected: selected, + inLayer: inLayer + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js new file mode 100644 index 00000000..dbe8f9f5 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js @@ -0,0 +1,1134 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Force Module. + Visualization of the topology in an SVG layer, using a D3 Force Layout. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, $timeout, fs, sus, ts, flash, wss, tov, + tis, tms, td3, tss, tts, tos, fltr, tls, uplink, svg; + + // configuration + var linkConfig = { + light: { + baseColor: '#666', + inColor: '#66f', + outColor: '#f00' + }, + dark: { + baseColor: '#aaa', + inColor: '#66f', + outColor: '#f66' + }, + inWidth: 12, + outWidth: 10 + }; + + // internal state + var settings, // merged default settings and options + force, // force layout object + drag, // drag behavior handler + network = { + nodes: [], + links: [], + linksByDevice: {}, + lookup: {}, + revLinkToKey: {} + }, + lu, // shorthand for lookup + rlk, // shorthand for revLinktoKey + showHosts = false, // whether hosts are displayed + showOffline = true, // whether offline devices are displayed + nodeLock = false, // whether nodes can be dragged or not (locked) + fTimer, // timer for delayed force layout + fNodesTimer, // timer for delayed nodes update + fLinksTimer, // timer for delayed links update + dim, // the dimensions of the force layout [w,h] + linkNums = []; // array of link number labels + + // SVG elements; + var linkG, linkLabelG, numLinkLblsG, portLabelG, nodeG; + + // D3 selections; + var link, linkLabel, node; + + // default settings for force layout + var defaultSettings = { + gravity: 0.4, + friction: 0.7, + charge: { + // note: key is node.class + device: -8000, + host: -5000, + _def_: -12000 + }, + linkDistance: { + // note: key is link.type + direct: 100, + optical: 120, + hostLink: 3, + _def_: 50 + }, + linkStrength: { + // note: key is link.type + // range: {0.0 ... 1.0} + //direct: 1.0, + //optical: 1.0, + //hostLink: 1.0, + _def_: 1.0 + } + }; + + + // ========================== + // === EVENT HANDLERS + + function addDevice(data) { + var id = data.id, + d; + + uplink.showNoDevs(false); + + // although this is an add device event, if we already have the + // device, treat it as an update instead.. + if (lu[id]) { + updateDevice(data); + return; + } + + d = tms.createDeviceNode(data); + network.nodes.push(d); + lu[id] = d; + updateNodes(); + fStart(); + } + + function updateDevice(data) { + var id = data.id, + d = lu[id], + wasOnline; + + if (d) { + wasOnline = d.online; + angular.extend(d, data); + if (tms.positionNode(d, true)) { + sendUpdateMeta(d); + } + updateNodes(); + if (wasOnline !== d.online) { + tms.findAttachedLinks(d.id).forEach(restyleLinkElement); + updateOfflineVisibility(d); + } + } + } + + function removeDevice(data) { + var id = data.id, + d = lu[id]; + if (d) { + removeDeviceElement(d); + } + } + + function addHost(data) { + var id = data.id, + d, lnk; + + // although this is an add host event, if we already have the + // host, treat it as an update instead.. + if (lu[id]) { + updateHost(data); + return; + } + + d = tms.createHostNode(data); + network.nodes.push(d); + lu[id] = d; + updateNodes(); + + lnk = tms.createHostLink(data); + if (lnk) { + d.linkData = lnk; // cache ref on its host + network.links.push(lnk); + lu[d.ingress] = lnk; + lu[d.egress] = lnk; + updateLinks(); + } + fStart(); + } + + function updateHost(data) { + var id = data.id, + d = lu[id]; + if (d) { + angular.extend(d, data); + if (tms.positionNode(d, true)) { + sendUpdateMeta(d); + } + updateNodes(); + } + } + + function removeHost(data) { + var id = data.id, + d = lu[id]; + if (d) { + removeHostElement(d, true); + } + } + + function addLink(data) { + var result = tms.findLink(data, 'add'), + bad = result.badLogic, + d = result.ldata; + + if (bad) { + //logicError(bad + ': ' + link.id); + return; + } + + if (d) { + // we already have a backing store link for src/dst nodes + addLinkUpdate(d, data); + return; + } + + // no backing store link yet + d = tms.createLink(data); + if (d) { + network.links.push(d); + aggregateLink(d, data); + lu[d.key] = d; + updateLinks(); + fStart(); + } + } + + function updateLink(data) { + var result = tms.findLink(data, 'update'), + bad = result.badLogic; + if (bad) { + //logicError(bad + ': ' + link.id); + return; + } + result.updateWith(link); + } + + function removeLink(data) { + var result = tms.findLink(data, 'remove'); + + if (!result.badLogic) { + result.removeRawLink(); + } + } + + // ======================== + + function nodeById(id) { + return lu[id]; + } + + function makeNodeKey(node1, node2) { + return node1 + '-' + node2; + } + + function findNodePair(key, keyRev) { + if (network.linksByDevice[key]) { + return key; + } else if (network.linksByDevice[keyRev]) { + return keyRev; + } else { + return false; + } + } + + function aggregateLink(ldata, link) { + var key = makeNodeKey(link.src, link.dst), + keyRev = makeNodeKey(link.dst, link.src), + found = findNodePair(key, keyRev); + + if (found) { + network.linksByDevice[found].push(ldata); + ldata.devicePair = found; + } else { + network.linksByDevice[key] = [ ldata ]; + ldata.devicePair = key; + } + } + + function addLinkUpdate(ldata, link) { + // add link event, but we already have the reverse link installed + ldata.fromTarget = link; + rlk[link.id] = ldata.key; + // possible solution to el being undefined in restyleLinkElement: + //_updateLinks(); + restyleLinkElement(ldata); + } + + + var widthRatio = 1.4, + linkScale = d3.scale.linear() + .domain([1, 12]) + .range([widthRatio, 12 * widthRatio]) + .clamp(true), + allLinkTypes = 'direct indirect optical tunnel'; + + function restyleLinkElement(ldata, immediate) { + // this fn's job is to look at raw links and decide what svg classes + // need to be applied to the line element in the DOM + var th = ts.theme(), + el = ldata.el, + type = ldata.type(), + lw = ldata.linkWidth(), + online = ldata.online(), + delay = immediate ? 0 : 1000; + + // FIXME: understand why el is sometimes undefined on addLink events... + // Investigated: + // el is undefined when it's a reverse link that is being added. + // updateLinks (which sets ldata.el) isn't called before this is called. + // Calling _updateLinks in addLinkUpdate fixes it, but there might be + // a more efficient way to fix it. + if (el && !el.empty()) { + el.classed('link', true); + el.classed('inactive', !online); + el.classed(allLinkTypes, false); + if (type) { + el.classed(type, true); + } + el.transition() + .duration(delay) + .attr('stroke-width', linkScale(lw)) + .attr('stroke', linkConfig[th].baseColor); + } + } + + function removeLinkElement(d) { + var idx = fs.find(d.key, network.links, 'key'), + removed; + if (idx >=0) { + // remove from links array + removed = network.links.splice(idx, 1); + // remove from lookup cache + delete lu[removed[0].key]; + updateLinks(); + fResume(); + } + } + + function removeHostElement(d, upd) { + // first, remove associated hostLink... + removeLinkElement(d.linkData); + + // remove hostLink bindings + delete lu[d.ingress]; + delete lu[d.egress]; + + // remove from lookup cache + delete lu[d.id]; + // remove from nodes array + var idx = fs.find(d.id, network.nodes); + network.nodes.splice(idx, 1); + + // remove from SVG + // NOTE: upd is false if we were called from removeDeviceElement() + if (upd) { + updateNodes(); + fResume(); + } + } + + function removeDeviceElement(d) { + var id = d.id, + idx; + // first, remove associated hosts and links.. + tms.findAttachedHosts(id).forEach(removeHostElement); + tms.findAttachedLinks(id).forEach(removeLinkElement); + + // remove from lookup cache + delete lu[id]; + // remove from nodes array + idx = fs.find(id, network.nodes); + if (idx > -1) { + network.nodes.splice(idx, 1); + } + + if (!network.nodes.length) { + uplink.showNoDevs(true); + } + + // remove from SVG + updateNodes(); + fResume(); + } + + function updateHostVisibility() { + sus.visible(nodeG.selectAll('.host'), showHosts); + sus.visible(linkG.selectAll('.hostLink'), showHosts); + sus.visible(linkLabelG.selectAll('.hostLinkLabel'), showHosts); + } + + function updateOfflineVisibility(dev) { + function updDev(d, show) { + var b; + sus.visible(d.el, show); + + tms.findAttachedLinks(d.id).forEach(function (link) { + b = show && ((link.type() !== 'hostLink') || showHosts); + sus.visible(link.el, b); + }); + tms.findAttachedHosts(d.id).forEach(function (host) { + b = show && showHosts; + sus.visible(host.el, b); + }); + } + + if (dev) { + // updating a specific device that just toggled off/on-line + updDev(dev, dev.online || showOffline); + } else { + // updating all offline devices + tms.findDevices(true).forEach(function (d) { + updDev(d, showOffline); + }); + } + } + + + function sendUpdateMeta(d, clearPos) { + var metaUi = {}, + ll; + + // if we are not clearing the position data (unpinning), + // attach the x, y, longitude, latitude... + if (!clearPos) { + ll = tms.lngLatFromCoord([d.x, d.y]); + metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]}; + } + d.metaUi = metaUi; + wss.sendEvent('updateMeta', { + id: d.id, + class: d.class, + memento: metaUi + }); + } + + + function mkSvgClass(d) { + return d.fixed ? d.svgClass + ' fixed' : d.svgClass; + } + + function vis(b) { + return b ? 'visible' : 'hidden'; + } + + function toggleHosts(x) { + var kev = (x === 'keyev'), + on = kev ? !showHosts : !!x; + + showHosts = on; + updateHostVisibility(); + flash.flash('Hosts ' + vis(on)); + return on; + } + + function toggleOffline(x) { + var kev = (x === 'keyev'), + on = kev ? !showOffline : !!x; + + showOffline = on; + updateOfflineVisibility(); + flash.flash('Offline devices ' + vis(on)); + return on; + } + + function cycleDeviceLabels() { + flash.flash(td3.incDevLabIndex()); + tms.findDevices().forEach(function (d) { + td3.updateDeviceLabel(d); + }); + } + + function unpin() { + var hov = tss.hovered(); + if (hov) { + sendUpdateMeta(hov, true); + hov.fixed = false; + hov.el.classed('fixed', false); + fResume(); + } + } + + function showMastership(masterId) { + if (!masterId) { + restoreLayerState(); + } else { + showMastershipFor(masterId); + } + } + + function restoreLayerState() { + // NOTE: this level of indirection required, for when we have + // the layer filter functionality re-implemented + suppressLayers(false); + } + + function showMastershipFor(id) { + suppressLayers(true); + node.each(function (n) { + if (n.master === id) { + n.el.classed('suppressedmax', false); + } + }); + } + + function supAmt(less) { + return less ? "suppressed" : "suppressedmax"; + } + + function suppressLayers(b, less) { + var cls = supAmt(less); + node.classed(cls, b); + link.classed(cls, b); + } + + function unsuppressNode(id, less) { + var cls = supAmt(less); + node.each(function (n) { + if (n.id === id) { + n.el.classed(cls, false); + } + }); + } + + function unsuppressLink(key, less) { + var cls = supAmt(less); + link.each(function (n) { + if (n.key === key) { + n.el.classed(cls, false); + } + }); + } + + function showBadLinks() { + var badLinks = tms.findBadLinks(); + flash.flash('Bad Links: ' + badLinks.length); + $log.debug('Bad Link List (' + badLinks.length + '):'); + badLinks.forEach(function (d) { + $log.debug('bad link: (' + d.bad + ') ' + d.key, d); + if (d.el) { + d.el.attr('stroke-width', linkScale(2.8)) + .attr('stroke', 'red'); + } + }); + // back to normal after 2 seconds... + $timeout(updateLinks, 2000); + } + + // ========================================== + + function updateNodes() { + if (fNodesTimer) { + $timeout.cancel(fNodesTimer); + } + fNodesTimer = $timeout(_updateNodes, 150); + } + + // IMPLEMENTATION NOTE: _updateNodes() should NOT stop, start, or resume + // the force layout; that needs to be determined and implemented elsewhere + function _updateNodes() { + // select all the nodes in the layout: + node = nodeG.selectAll('.node') + .data(network.nodes, function (d) { return d.id; }); + + // operate on existing nodes: + node.filter('.device').each(td3.deviceExisting); + node.filter('.host').each(td3.hostExisting); + + // operate on entering nodes: + var entering = node.enter() + .append('g') + .attr({ + id: function (d) { return sus.safeId(d.id); }, + class: mkSvgClass, + transform: function (d) { + // Need to guard against NaN here ?? + return sus.translate(d.x, d.y); + }, + opacity: 0 + }) + .call(drag) + .on('mouseover', tss.nodeMouseOver) + .on('mouseout', tss.nodeMouseOut) + .transition() + .attr('opacity', 1); + + // augment entering nodes: + entering.filter('.device').each(td3.deviceEnter); + entering.filter('.host').each(td3.hostEnter); + + // operate on both existing and new nodes: + td3.updateDeviceColors(); + + // operate on exiting nodes: + // Note that the node is removed after 2 seconds. + // Sub element animations should be shorter than 2 seconds. + var exiting = node.exit() + .transition() + .duration(2000) + .style('opacity', 0) + .remove(); + + // exiting node specifics: + exiting.filter('.host').each(td3.hostExit); + exiting.filter('.device').each(td3.deviceExit); + } + + // ========================== + + function getDefaultPos(link) { + return { + x1: link.source.x, + y1: link.source.y, + x2: link.target.x, + y2: link.target.y + }; + } + + // returns amount of adjustment along the normal for given link + function amt(numLinks, linkIdx) { + var gap = 6; + return (linkIdx - ((numLinks - 1) / 2)) * gap; + } + + function calcMovement(d, amt, flipped) { + var pos = getDefaultPos(d), + mult = flipped ? -amt : amt, + dx = pos.x2 - pos.x1, + dy = pos.y2 - pos.y1, + length = Math.sqrt((dx * dx) + (dy * dy)); + + return { + x1: pos.x1 + (mult * dy / length), + y1: pos.y1 + (mult * -dx / length), + x2: pos.x2 + (mult * dy / length), + y2: pos.y2 + (mult * -dx / length) + }; + } + + function calcPosition() { + var lines = this, + linkSrcId; + linkNums = []; + lines.each(function (d) { + if (d.type() === 'hostLink') { + d.position = getDefaultPos(d); + } + }); + + function normalizeLinkSrc(link) { + // ensure source device is consistent across set of links + // temporary measure until link modeling is refactored + if (!linkSrcId) { + linkSrcId = link.source.id; + return false; + } + + return link.source.id !== linkSrcId; + } + + angular.forEach(network.linksByDevice, function (linkArr, key) { + var numLinks = linkArr.length, + link; + + if (numLinks === 1) { + link = linkArr[0]; + link.position = getDefaultPos(link); + link.position.multiLink = false; + } else if (numLinks >= 5) { + // this code is inefficient, in the future the way links + // are modeled will be changed + angular.forEach(linkArr, function (link) { + link.position = getDefaultPos(link); + link.position.multiLink = true; + }); + linkNums.push({ + id: key, + num: numLinks, + linkCoords: linkArr[0].position + }); + } else { + linkSrcId = null; + angular.forEach(linkArr, function (link, index) { + var offsetAmt = amt(numLinks, index), + needToFlip = normalizeLinkSrc(link); + link.position = calcMovement(link, offsetAmt, needToFlip); + link.position.multiLink = false; + }); + } + }); + } + + function updateLinks() { + if (fLinksTimer) { + $timeout.cancel(fLinksTimer); + } + fLinksTimer = $timeout(_updateLinks, 150); + } + + // IMPLEMENTATION NOTE: _updateLinks() should NOT stop, start, or resume + // the force layout; that needs to be determined and implemented elsewhere + function _updateLinks() { + var th = ts.theme(); + + link = linkG.selectAll('.link') + .data(network.links, function (d) { return d.key; }); + + // operate on existing links: + link.each(function (d) { + // this is supposed to be an existing link, but we have observed + // occasions (where links are deleted and added rapidly?) where + // the DOM element has not been defined. So protect against that... + if (d.el) { + restyleLinkElement(d, true); + } + }); + + // operate on entering links: + var entering = link.enter() + .append('line') + .call(calcPosition) + .attr({ + x1: function (d) { return d.position.x1; }, + y1: function (d) { return d.position.y1; }, + x2: function (d) { return d.position.x2; }, + y2: function (d) { return d.position.y2; }, + stroke: linkConfig[th].inColor, + 'stroke-width': linkConfig.inWidth + }); + + // augment links + entering.each(td3.linkEntering); + + // operate on both existing and new links: + //link.each(...) + + // add labels for how many links are in a thick line + td3.applyNumLinkLabels(linkNums, numLinkLblsG); + + // apply or remove labels + td3.applyLinkLabels(); + + // operate on exiting links: + link.exit() + .attr('stroke-dasharray', '3 3') + .attr('stroke', linkConfig[th].outColor) + .style('opacity', 0.5) + .transition() + .duration(1500) + .attr({ + 'stroke-dasharray': '3 12', + 'stroke-width': linkConfig.outWidth + }) + .style('opacity', 0.0) + .remove(); + } + + + // ========================== + // force layout tick function + + function fResume() { + if (!tos.isOblique()) { + force.resume(); + } + } + + function fStart() { + if (!tos.isOblique()) { + if (fTimer) { + $timeout.cancel(fTimer); + } + fTimer = $timeout(function () { + $log.debug("Starting force-layout"); + force.start(); + }, 200); + } + } + + var tickStuff = { + nodeAttr: { + transform: function (d) { + var dx = isNaN(d.x) ? 0 : d.x, + dy = isNaN(d.y) ? 0 : d.y; + return sus.translate(dx, dy); + } + }, + linkAttr: { + x1: function (d) { return d.position.x1; }, + y1: function (d) { return d.position.y1; }, + x2: function (d) { return d.position.x2; }, + y2: function (d) { return d.position.y2; } + }, + linkLabelAttr: { + transform: function (d) { + var lnk = tms.findLinkById(d.key); + if (lnk) { + return td3.transformLabel(lnk.position); + } + } + } + }; + + function tick() { + // guard against null (which can happen when our view pages out)... + if (node && node.size()) { + node.attr(tickStuff.nodeAttr); + } + if (link && link.size()) { + link.call(calcPosition) + .attr(tickStuff.linkAttr); + td3.applyNumLinkLabels(linkNums, numLinkLblsG); + } + if (linkLabel && linkLabel.size()) { + linkLabel.attr(tickStuff.linkLabelAttr); + } + } + + + // ========================== + // === MOUSE GESTURE HANDLERS + + function zoomingOrPanning(ev) { + return ev.metaKey || ev.altKey; + } + + function atDragEnd(d) { + // once we've finished moving, pin the node in position + d.fixed = true; + d3.select(this).classed('fixed', true); + sendUpdateMeta(d); + tss.clickConsumed(true); + } + + // predicate that indicates when dragging is active + function dragEnabled() { + var ev = d3.event.sourceEvent; + // nodeLock means we aren't allowing nodes to be dragged... + return !nodeLock && !zoomingOrPanning(ev); + } + + // predicate that indicates when clicking is active + function clickEnabled() { + return true; + } + + // ============================================= + // function entry points for overlay module + + // TODO: find an automatic way of tracking via the "showHighlights" events + var allTrafficClasses = 'primary secondary optical animated ' + + 'port-traffic-Kbps port-traffic-Mbps port-traffic-Gbps ' + + 'port-traffic-Gbps-choked'; + + function clearLinkTrafficStyle() { + link.style('stroke-width', null) + .classed(allTrafficClasses, false); + } + + function removeLinkLabels() { + network.links.forEach(function (d) { + d.label = ''; + }); + } + + function updateLinkLabelModel() { + // create the backing data for showing labels.. + var data = []; + link.each(function (d) { + if (d.label) { + data.push({ + id: 'lab-' + d.key, + key: d.key, + label: d.label, + ldata: d + }); + } + }); + + linkLabel = linkLabelG.selectAll('.linkLabel') + .data(data, function (d) { return d.id; }); + } + + // ========================== + // Module definition + + function mkModelApi(uplink) { + return { + projection: uplink.projection, + network: network, + restyleLinkElement: restyleLinkElement, + removeLinkElement: removeLinkElement + }; + } + + function mkD3Api() { + return { + node: function () { return node; }, + link: function () { return link; }, + linkLabel: function () { return linkLabel; }, + instVisible: function () { return tis.isVisible(); }, + posNode: tms.positionNode, + showHosts: function () { return showHosts; }, + restyleLinkElement: restyleLinkElement, + updateLinkLabelModel: updateLinkLabelModel, + linkConfig: function () { return linkConfig; } + }; + } + + function mkSelectApi() { + return { + node: function () { return node; }, + zoomingOrPanning: zoomingOrPanning, + updateDeviceColors: td3.updateDeviceColors, + deselectLink: tls.deselectLink + }; + } + + function mkTrafficApi() { + return { + hovered: tss.hovered, + somethingSelected: tss.somethingSelected, + selectOrder: tss.selectOrder + }; + } + + function mkOverlayApi() { + return { + clearLinkTrafficStyle: clearLinkTrafficStyle, + removeLinkLabels: removeLinkLabels, + findLinkById: tms.findLinkById, + findNodeById: nodeById, + updateLinks: updateLinks, + updateNodes: updateNodes, + supLayers: suppressLayers, + unsupNode: unsuppressNode, + unsupLink: unsuppressLink + }; + } + + function mkObliqueApi(uplink, fltr) { + return { + force: function() { return force; }, + zoomLayer: uplink.zoomLayer, + nodeGBBox: function() { return nodeG.node().getBBox(); }, + node: function () { return node; }, + link: function () { return link; }, + linkLabel: function () { return linkLabel; }, + nodes: function () { return network.nodes; }, + tickStuff: tickStuff, + nodeLock: function (b) { + var old = nodeLock; + nodeLock = b; + return old; + }, + opacifyMap: uplink.opacifyMap, + inLayer: fltr.inLayer, + calcLinkPos: calcPosition, + applyNumLinkLabels: function () { + td3.applyNumLinkLabels(linkNums, numLinkLblsG); + } + }; + } + + function mkFilterApi() { + return { + node: function () { return node; }, + link: function () { return link; } + }; + } + + function mkLinkApi(svg, uplink) { + return { + svg: svg, + zoomer: uplink.zoomer(), + network: network, + portLabelG: function () { return portLabelG; }, + showHosts: function () { return showHosts; } + }; + } + + angular.module('ovTopo') + .factory('TopoForceService', + ['$log', '$timeout', 'FnService', 'SvgUtilService', + 'ThemeService', 'FlashService', 'WebSocketService', + 'TopoOverlayService', 'TopoInstService', 'TopoModelService', + 'TopoD3Service', 'TopoSelectService', 'TopoTrafficService', + 'TopoObliqueService', 'TopoFilterService', 'TopoLinkService', + + function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_, _tov_, + _tis_, _tms_, _td3_, _tss_, _tts_, _tos_, _fltr_, _tls_) { + $log = _$log_; + $timeout = _$timeout_; + fs = _fs_; + sus = _sus_; + ts = _ts_; + flash = _flash_; + wss = _wss_; + tov = _tov_; + tis = _tis_; + tms = _tms_; + td3 = _td3_; + tss = _tss_; + tts = _tts_; + tos = _tos_; + fltr = _fltr_; + tls = _tls_; + + var themeListener = ts.addListener(function () { + updateLinks(); + updateNodes(); + }); + + // forceG is the SVG group to display the force layout in + // uplink is the api from the main topo source file + // dim is the initial dimensions of the SVG as [w,h] + // opts are, well, optional :) + function initForce(_svg_, forceG, _uplink_, _dim_, opts) { + uplink = _uplink_; + dim = _dim_; + svg = _svg_; + + lu = network.lookup; + rlk = network.revLinkToKey; + + $log.debug('initForce().. dim = ' + dim); + + tov.setApi(mkOverlayApi(), tss); + tms.initModel(mkModelApi(uplink), dim); + td3.initD3(mkD3Api()); + tss.initSelect(mkSelectApi()); + tts.initTraffic(mkTrafficApi()); + tos.initOblique(mkObliqueApi(uplink, fltr)); + fltr.initFilter(mkFilterApi()); + tls.initLink(mkLinkApi(svg, uplink), td3); + + settings = angular.extend({}, defaultSettings, opts); + + linkG = forceG.append('g').attr('id', 'topo-links'); + linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels'); + numLinkLblsG = forceG.append('g').attr('id', 'topo-numLinkLabels'); + nodeG = forceG.append('g').attr('id', 'topo-nodes'); + portLabelG = forceG.append('g').attr('id', 'topo-portLabels'); + + link = linkG.selectAll('.link'); + linkLabel = linkLabelG.selectAll('.linkLabel'); + node = nodeG.selectAll('.node'); + + force = d3.layout.force() + .size(dim) + .nodes(network.nodes) + .links(network.links) + .gravity(settings.gravity) + .friction(settings.friction) + .charge(settings.charge._def_) + .linkDistance(settings.linkDistance._def_) + .linkStrength(settings.linkStrength._def_) + .on('tick', tick); + + drag = sus.createDragBehavior(force, + tss.selectObject, atDragEnd, dragEnabled, clickEnabled); + } + + function newDim(_dim_) { + dim = _dim_; + force.size(dim); + tms.newDim(dim); + } + + function destroyForce() { + force.stop(); + + tls.destroyLink(); + tos.destroyOblique(); + tts.destroyTraffic(); + tss.destroySelect(); + td3.destroyD3(); + tms.destroyModel(); + // note: no need to destroy overlay service + ts.removeListener(themeListener); + themeListener = null; + + // clean up the DOM + svg.selectAll('g').remove(); + svg.selectAll('defs').remove(); + + // clean up internal state + network.nodes = []; + network.links = []; + network.linksByDevice = {}; + network.lookup = {}; + network.revLinkToKey = {}; + + linkNums = []; + + linkG = linkLabelG = numLinkLblsG = nodeG = portLabelG = null; + link = linkLabel = node = null; + force = drag = null; + + // clean up $timeout promises + if (fTimer) { + $timeout.cancel(fTimer); + } + if (fNodesTimer) { + $timeout.cancel(fNodesTimer); + } + if (fLinksTimer) { + $timeout.cancel(fLinksTimer); + } + } + + return { + initForce: initForce, + newDim: newDim, + destroyForce: destroyForce, + + updateDeviceColors: td3.updateDeviceColors, + toggleHosts: toggleHosts, + togglePorts: tls.togglePorts, + toggleOffline: toggleOffline, + cycleDeviceLabels: cycleDeviceLabels, + unpin: unpin, + showMastership: showMastership, + showBadLinks: showBadLinks, + + addDevice: addDevice, + updateDevice: updateDevice, + removeDevice: removeDevice, + addHost: addHost, + updateHost: updateHost, + removeHost: removeHost, + addLink: addLink, + updateLink: updateLink, + removeLink: removeLink + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js new file mode 100644 index 00000000..7e929977 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js @@ -0,0 +1,373 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Instances Module. + Defines modeling of ONOS instances. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, ps, sus, gs, ts, fs, flash; + + // api from topo + var api; + /* + showMastership( id ) + */ + + // configuration + var instCfg = { + rectPad: 8, + nodeOx: 9, + nodeOy: 9, + nodeDim: 40, + birdOx: 19, + birdOy: 21, + birdDim: 21, + uiDy: 45, + titleDy: 30, + textYOff: 20, + textYSpc: 15 + }, + showLogicErrors = true, + idIns = 'topo-p-instance', + instOpts = { + edge: 'left', + width: 20 + }; + + // internal state + var onosInstances, + onosOrder, + oiShowMaster, + oiBox, + themeListener; + + + // ========================== + + function addInstance(data) { + var id = data.id; + + if (onosInstances[id]) { + updateInstance(data); + return; + } + onosInstances[id] = data; + onosOrder.push(data); + updateInstances(); + } + + function updateInstance(data) { + var id = data.id, + d = onosInstances[id]; + if (d) { + angular.extend(d, data); + updateInstances(); + } else { + logicError('updateInstance: lookup fail: ID = "' + id + '"'); + } + } + + function removeInstance(data) { + var id = data.id, + d = onosInstances[id]; + if (d) { + var idx = fs.find(id, onosOrder); + if (idx >= 0) { + onosOrder.splice(idx, 1); + } + delete onosInstances[id]; + updateInstances(); + } else { + logicError('removeInstance lookup fail. ID = "' + id + '"'); + } + } + + // ========================== + + function computeDim(self) { + var css = window.getComputedStyle(self); + return { + w: sus.stripPx(css.width), + h: sus.stripPx(css.height) + }; + } + + function clickInst(d) { + var el = d3.select(this), + aff = el.classed('affinity'); + if (!aff) { + setAffinity(el, d); + } else { + cancelAffinity(); + } + } + + function setAffinity(el, d) { + d3.selectAll('.onosInst') + .classed('mastership', true) + .classed('affinity', false); + el.classed('affinity', true); + + // suppress all elements except nodes whose master is this instance + api.showMastership(d.id); + oiShowMaster = true; + } + + function cancelAffinity() { + d3.selectAll('.onosInst') + .classed('mastership affinity', false); + + api.showMastership(null); + oiShowMaster = false; + } + + function instRectAttr(dim) { + var pad = instCfg.rectPad; + return { + x: pad, + y: pad, + width: dim.w - pad*2, + height: dim.h - pad*2, + rx: 6 + }; + } + + function viewBox(dim) { + return '0 0 ' + dim.w + ' ' + dim.h; + } + + function attachUiBadge(svg) { + gs.addGlyph(svg, 'uiAttached', 30, true, [12, instCfg.uiDy]) + .classed('badgeIcon uiBadge', true); + } + + function instColor(id, online) { + return sus.cat7().getColor(id, !online, ts.theme()); + } + + // ============================== + + function updateInstances() { + var onoses = oiBox.el().selectAll('.onosInst') + .data(onosOrder, function (d) { return d.id; }), + instDim = {w:0,h:0}, + c = instCfg; + + function nSw(n) { + return '# Switches: ' + n; + } + + // operate on existing onos instances if necessary + onoses.each(function (d) { + var el = d3.select(this), + svg = el.select('svg'); + instDim = computeDim(this); + + // update online state + el.classed('online', d.online); + + // update ui-attached state + svg.select('use.uiBadge').remove(); + if (d.uiAttached) { + attachUiBadge(svg); + } + + function updAttr(id, value) { + svg.select('text.instLabel.'+id).text(value); + } + + updAttr('ip', d.ip); + updAttr('ns', nSw(d.switches)); + }); + + + // operate on new onos instances + var entering = onoses.enter() + .append('div') + .attr('class', 'onosInst') + .classed('online', function (d) { return d.online; }) + .on('click', clickInst); + + entering.each(function (d) { + var el = d3.select(this), + rectAttr, + svg; + instDim = computeDim(this); + rectAttr = instRectAttr(instDim); + + svg = el.append('svg').attr({ + width: instDim.w, + height: instDim.h, + viewBox: viewBox(instDim) + }); + + svg.append('rect').attr(rectAttr); + + gs.addGlyph(svg, 'bird', 28, true, [14, 14]) + .classed('badgeIcon', true); + + if (d.uiAttached) { + attachUiBadge(svg); + } + + var left = c.nodeOx + c.nodeDim, + len = rectAttr.width - left, + hlen = len / 2, + midline = hlen + left; + + // title + svg.append('text') + .attr({ + class: 'instTitle', + x: midline, + y: c.titleDy + }) + .text(d.id); + + // a couple of attributes + var ty = c.titleDy + c.textYOff; + + function addAttr(id, label) { + svg.append('text').attr({ + class: 'instLabel ' + id, + x: midline, + y: ty + }).text(label); + ty += c.textYSpc; + } + + addAttr('ip', d.ip); + addAttr('ns', nSw(d.switches)); + }); + + // operate on existing + new onoses here + // set the affinity colors... + onoses.each(function (d) { + var el = d3.select(this), + rect = el.select('svg').select('rect'), + col = instColor(d.id, d.online); + rect.style('fill', col); + }); + + // adjust the panel size appropriately... + oiBox.width(instDim.w * onosOrder.length); + oiBox.height(instDim.h); + + // remove any outgoing instances + onoses.exit().remove(); + } + + + // ========================== + + function logicError(msg) { + if (showLogicErrors) { + $log.warn('TopoInstService: ' + msg); + } + } + + function initInst(_api_) { + api = _api_; + oiBox = ps.createPanel(idIns, instOpts); + oiBox.show(); + + onosInstances = {}; + onosOrder = []; + oiShowMaster = false; + + // we want to update the instances, each time the theme changes + themeListener = ts.addListener(updateInstances); + } + + function destroyInst() { + ts.removeListener(themeListener); + themeListener = null; + + ps.destroyPanel(idIns); + oiBox = null; + + onosInstances = {}; + onosOrder = []; + oiShowMaster = false; + } + + function showInsts() { + oiBox.show(); + } + + function hideInsts() { + oiBox.hide(); + } + + function toggleInsts(x) { + var kev = (x === 'keyev'), + on, + verb; + + if (kev) { + on = oiBox.toggle(); + } else { + on = !!x; + if (on) { + showInsts(); + } else { + hideInsts(); + } + } + verb = on ? 'Show' : 'Hide'; + flash.flash(verb + ' instances panel'); + return on; + } + + // ========================== + + angular.module('ovTopo') + .factory('TopoInstService', + ['$log', 'PanelService', 'SvgUtilService', 'GlyphService', + 'ThemeService', 'FnService', 'FlashService', + + function (_$log_, _ps_, _sus_, _gs_, _ts_, _fs_, _flash_) { + $log = _$log_; + ps = _ps_; + sus = _sus_; + gs = _gs_; + ts = _ts_; + fs = _fs_; + flash = _flash_; + + return { + initInst: initInst, + destroyInst: destroyInst, + + addInstance: addInstance, + updateInstance: updateInstance, + removeInstance: removeInstance, + + cancelAffinity: cancelAffinity, + + isVisible: function () { return oiBox.isVisible(); }, + show: showInsts, + hide: hideInsts, + toggle: toggleInsts, + showMaster: function () { return oiShowMaster; } + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js new file mode 100644 index 00000000..38f3a6c3 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js @@ -0,0 +1,338 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Link Module. + Functions for highlighting/selecting links + */ + +(function () { + 'use strict'; + + // injected refs + var $log, fs, sus, ts, flash, tss, tps; + + // internal state + var api, + td3, + network, + showPorts = true, // enable port highlighting by default + enhancedLink = null, // the link over which the mouse is hovering + selectedLink = null; // the link which is currently selected + + // SVG elements; + var svg; + + + // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ======== + + function getLogicalMousePosition(container) { + var m = d3.mouse(container), + sc = api.zoomer.scale(), + tr = api.zoomer.translate(), + mx = (m[0] - tr[0]) / sc, + my = (m[1] - tr[1]) / sc; + return {x: mx, y: my}; + } + + + function sq(x) { return x * x; } + + function mdist(p, m) { + return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y)); + } + + function prox(dist) { + return dist / api.zoomer.scale(); + } + + function computeNearestNode(mouse) { + var proximity = prox(30), + nearest = null, + minDist; + + if (network.nodes.length) { + minDist = proximity * 2; + + network.nodes.forEach(function (d) { + var dist; + + if (!api.showHosts() && d.class === 'host') { + return; // skip hidden hosts + } + + dist = mdist({x: d.x, y: d.y}, mouse); + if (dist < minDist && dist < proximity) { + minDist = dist; + nearest = d; + } + }); + } + return nearest; + } + + + function computeNearestLink(mouse) { + var proximity = prox(30), + nearest = null, + minDist; + + function pdrop(line, mouse) { + var x1 = line.x1, + y1 = line.y1, + x2 = line.x2, + y2 = line.y2, + x3 = mouse.x, + y3 = mouse.y, + k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) / + (sq(y2-y1) + sq(x2-x1)), + x4 = x3 - k * (y2-y1), + y4 = y3 + k * (x2-x1); + return {x:x4, y:y4}; + } + + function lineHit(line, p, m) { + if (p.x < line.x1 && p.x < line.x2) return false; + if (p.x > line.x1 && p.x > line.x2) return false; + if (p.y < line.y1 && p.y < line.y2) return false; + if (p.y > line.y1 && p.y > line.y2) return false; + // line intersects, but are we close enough? + return mdist(p, m) <= proximity; + } + + if (network.links.length) { + minDist = proximity * 2; + + network.links.forEach(function (d) { + if (!api.showHosts() && d.type() === 'hostLink') { + return; // skip hidden host links + } + + var line = d.position, + point = pdrop(line, mouse), + hit = lineHit(line, point, mouse), + dist; + + if (hit) { + dist = mdist(point, mouse); + if (dist < minDist) { + minDist = dist; + nearest = d; + } + } + }); + } + return nearest; + } + + function enhanceLink(ldata) { + // if the new link is same as old link, do nothing + if (enhancedLink && ldata && enhancedLink.key === ldata.key) return; + + // first, unenhance the currently enhanced link + if (enhancedLink) { + unenhance(enhancedLink); + } + enhancedLink = ldata; + if (enhancedLink) { + enhance(enhancedLink); + } + } + + function unenhance(d) { + // guard against link element not set + if (d.el) { + d.el.classed('enhanced', false); + } + api.portLabelG().selectAll('.portLabel').remove(); + } + + function enhance(d) { + var data = [], + point; + + // guard against link element not set + if (!d.el) return; + + d.el.classed('enhanced', true); + + // Define port label data objects. + // NOTE: src port is absent in the case of host-links. + + point = locatePortLabel(d); + angular.extend(point, { + id: 'topo-port-tgt', + num: d.tgtPort + }); + data.push(point); + + if (d.srcPort) { + point = locatePortLabel(d, 1); + angular.extend(point, { + id: 'topo-port-src', + num: d.srcPort + }); + data.push(point); + } + + td3.applyPortLabels(data, api.portLabelG()); + } + + function locatePortLabel(link, src) { + var offset = 32, + pos = link.position, + nearX = src ? pos.x1 : pos.x2, + nearY = src ? pos.y1 : pos.y2, + farX = src ? pos.x2 : pos.x1, + farY = src ? pos.y2 : pos.y1; + + function dist(x, y) { return Math.sqrt(x*x + y*y); } + + var dx = farX - nearX, + dy = farY - nearY, + k = offset / dist(dx, dy); + + return {x: k * dx + nearX, y: k * dy + nearY}; + } + + function selectLink(ldata) { + // if the new link is same as old link, do nothing + if (selectedLink && ldata && selectedLink.key === ldata.key) return; + + // make sure no nodes are selected + tss.deselectAll(); + + // first, unenhance the currently enhanced link + if (selectedLink) { + unselLink(selectedLink); + } + selectedLink = ldata; + if (selectedLink) { + selLink(selectedLink); + } + } + + function unselLink(d) { + // guard against link element not set + if (d.el) { + d.el.classed('selected', false); + } + } + + function selLink(d) { + // guard against link element not set + if (!d.el) return; + + d.el.classed('selected', true); + + tps.displayLink(d); + tps.displaySomething(); + } + + // ====== MOUSE EVENT HANDLERS ====== + + function mouseMoveHandler() { + var mp = getLogicalMousePosition(this), + link = computeNearestLink(mp); + enhanceLink(link); + } + + function mouseClickHandler() { + var mp, link, node; + + if (!tss.clickConsumed()) { + mp = getLogicalMousePosition(this); + node = computeNearestNode(mp); + if (node) { + $log.debug('found nearest node:', node.labels[1]); + tss.selectObject(node); + } else { + link = computeNearestLink(mp); + selectLink(link); + } + } + } + + + // ====================== + + function togglePorts(x) { + var kev = (x === 'keyev'), + on = kev ? !showPorts : !!x, + what = on ? 'Enable' : 'Disable', + handler = on ? mouseMoveHandler : null; + + showPorts = on; + + if (!on) { + enhanceLink(null); + } + svg.on('mousemove', handler); + flash.flash(what + ' port highlighting'); + return on; + } + + function deselectLink() { + if (selectedLink) { + unselLink(selectedLink); + selectedLink = null; + return true; + } + return false; + } + + // ========================== + // Module definition + + angular.module('ovTopo') + .factory('TopoLinkService', + ['$log', 'FnService', 'SvgUtilService', 'ThemeService', 'FlashService', + 'TopoSelectService', 'TopoPanelService', + + function (_$log_, _fs_, _sus_, _ts_, _flash_, _tss_, _tps_) { + $log = _$log_; + fs = _fs_; + sus = _sus_; + ts = _ts_; + flash = _flash_; + tss = _tss_; + tps = _tps_; + + function initLink(_api_, _td3_) { + api = _api_; + td3 = _td3_; + svg = api.svg; + network = api.network; + if (showPorts && !fs.isMobile()) { + svg.on('mousemove', mouseMoveHandler); + } + svg.on('click', mouseClickHandler); + } + + function destroyLink() { + // unconditionally remove any event handlers + svg.on('mousemove', null); + svg.on('click', null); + } + + return { + initLink: initLink, + destroyLink: destroyLink, + togglePorts: togglePorts, + deselectLink: deselectLink + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js new file mode 100644 index 00000000..fb98fc2b --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js @@ -0,0 +1,439 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Model Module. + Auxiliary functions for the model of the topology; that is, our internal + representations of devices, hosts, links, etc. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, fs, rnd; + + // api to topoForce + var api; + /* + projection() + network {...} + restyleLinkElement( ldata ) + removeLinkElement( ldata ) + */ + + // shorthand + var lu, rlk, nodes, links, linksByDevice; + + var dim; // dimensions of layout [w,h] + + // configuration 'constants' + var defaultLinkType = 'direct', + nearDist = 15; + + + function coordFromLngLat(loc) { + var p = api.projection(); + // suspected cause of ONOS-2109 + return p ? p([loc.lng, loc.lat]) : [0, 0]; + } + + function lngLatFromCoord(coord) { + var p = api.projection(); + return p ? p.invert(coord) : [0, 0]; + } + + function positionNode(node, forUpdate) { + var meta = node.metaUi, + x = meta && meta.x, + y = meta && meta.y, + xy; + + // If we have [x,y] already, use that... + if (x && y) { + node.fixed = true; + node.px = node.x = x; + node.py = node.y = y; + return; + } + + var location = node.location, + coord; + + if (location && location.type === 'latlng') { + coord = coordFromLngLat(location); + node.fixed = true; + node.px = node.x = coord[0]; + node.py = node.y = coord[1]; + return true; + } + + // if this is a node update (not a node add).. skip randomizer + if (forUpdate) { + return; + } + + // Note: Placing incoming unpinned nodes at exactly the same point + // (center of the view) causes them to explode outwards when + // the force layout kicks in. So, we spread them out a bit + // initially, to provide a more serene layout convergence. + // Additionally, if the node is a host, we place it near + // the device it is connected to. + + function rand() { + return { + x: rnd.randDim(dim[0]), + y: rnd.randDim(dim[1]) + }; + } + + function near(node) { + return { + x: node.x + nearDist + rnd.spread(nearDist), + y: node.y + nearDist + rnd.spread(nearDist) + }; + } + + function getDevice(cp) { + var d = lu[cp.device]; + return d || rand(); + } + + xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand(); + angular.extend(node, xy); + } + + function mkSvgCls(dh, t, on) { + var ndh = 'node ' + dh, + ndht = t ? ndh + ' ' + t : ndh; + return on ? ndht + ' online' : ndht; + } + + function createDeviceNode(device) { + var node = device; + + // Augment as needed... + node.class = 'device'; + node.svgClass = mkSvgCls('device', device.type, device.online); + positionNode(node); + return node; + } + + function createHostNode(host) { + var node = host; + + // Augment as needed... + node.class = 'host'; + if (!node.type) { + node.type = 'endstation'; + } + node.svgClass = mkSvgCls('host', node.type); + positionNode(node); + return node; + } + + function createHostLink(host) { + var src = host.id, + dst = host.cp.device, + id = host.ingress, + lnk = linkEndPoints(src, dst); + + if (!lnk) { + return null; + } + + // Synthesize link ... + angular.extend(lnk, { + key: id, + class: 'link', + // NOTE: srcPort left undefined (host end of the link) + tgtPort: host.cp.port, + + type: function () { return 'hostLink'; }, + online: function () { + // hostlink target is edge switch + return lnk.target.online; + }, + linkWidth: function () { return 1; } + }); + return lnk; + } + + function createLink(link) { + var lnk = linkEndPoints(link.src, link.dst); + + if (!lnk) { + return null; + } + + angular.extend(lnk, { + key: link.id, + class: 'link', + fromSource: link, + srcPort: link.srcPort, + tgtPort: link.dstPort, + position: { + x1: 0, + y1: 0, + x2: 0, + y2: 0 + }, + + // functions to aggregate dual link state + type: function () { + var s = lnk.fromSource, + t = lnk.fromTarget; + return (s && s.type) || (t && t.type) || defaultLinkType; + }, + online: function () { + var s = lnk.fromSource, + t = lnk.fromTarget, + both = lnk.source.online && lnk.target.online; + return both && ((s && s.online) || (t && t.online)); + }, + linkWidth: function () { + var s = lnk.fromSource, + t = lnk.fromTarget, + ws = (s && s.linkWidth) || 0, + wt = (t && t.linkWidth) || 0; + return lnk.position.multiLink ? 5 : Math.max(ws, wt); + } + }); + return lnk; + } + + + function linkEndPoints(srcId, dstId) { + var srcNode = lu[srcId], + dstNode = lu[dstId], + sMiss = !srcNode ? missMsg('src', srcId) : '', + dMiss = !dstNode ? missMsg('dst', dstId) : ''; + + if (sMiss || dMiss) { + $log.error('Node(s) not on map for link:' + sMiss + dMiss); + //logicError('Node(s) not on map for link:\n' + sMiss + dMiss); + return null; + } + return { + source: srcNode, + target: dstNode + }; + } + + function missMsg(what, id) { + return '\n[' + what + '] "' + id + '" missing'; + } + + + function makeNodeKey(d, what) { + var port = what + 'Port'; + return d[what] + '/' + d[port]; + } + + function makeLinkKey(d, flipped) { + var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'), + two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst'); + return one + '-' + two; + } + + function findLinkById(id) { + // check to see if this is a reverse lookup, else default to given id + var key = rlk[id] || id; + return key && lu[key]; + } + + function findLink(linkData, op) { + var key = makeLinkKey(linkData), + keyrev = makeLinkKey(linkData, 1), + link = lu[key], + linkRev = lu[keyrev], + result = {}, + ldata = link || linkRev, + rawLink; + + if (op === 'add') { + if (link) { + // trying to add a link that we already know about + result.ldata = link; + result.badLogic = 'addLink: link already added'; + + } else if (linkRev) { + // we found the reverse of the link to be added + result.ldata = linkRev; + if (linkRev.fromTarget) { + result.badLogic = 'addLink: link already added'; + } + } + } else if (op === 'update') { + if (!ldata) { + result.badLogic = 'updateLink: link not found'; + } else { + rawLink = link ? ldata.fromSource : ldata.fromTarget; + result.updateWith = function (data) { + angular.extend(rawLink, data); + api.restyleLinkElement(ldata); + } + } + } else if (op === 'remove') { + if (!ldata) { + result.badLogic = 'removeLink: link not found'; + } else { + rawLink = link ? ldata.fromSource : ldata.fromTarget; + + if (!rawLink) { + result.badLogic = 'removeLink: link not found'; + + } else { + result.removeRawLink = function () { + // remove link out of aggregate linksByDevice list + var linksForDevPair = linksByDevice[ldata.devicePair], + rmvIdx = fs.find(ldata.key, linksForDevPair, 'key'); + if (rmvIdx >= 0) { + linksForDevPair.splice(rmvIdx, 1); + } + ldata.position.multilink = linksForDevPair.length >= 5; + + if (link) { + // remove fromSource + ldata.fromSource = null; + if (ldata.fromTarget) { + // promote target into source position + ldata.fromSource = ldata.fromTarget; + ldata.fromTarget = null; + ldata.key = keyrev; + delete lu[key]; + lu[keyrev] = ldata; + delete rlk[keyrev]; + } + } else { + // remove fromTarget + ldata.fromTarget = null; + delete rlk[keyrev]; + } + if (ldata.fromSource) { + api.restyleLinkElement(ldata); + } else { + api.removeLinkElement(ldata); + } + } + } + } + } + return result; + } + + function findDevices(offlineOnly) { + var a = []; + nodes.forEach(function (d) { + if (d.class === 'device' && !(offlineOnly && d.online)) { + a.push(d); + } + }); + return a; + } + + function findAttachedHosts(devId) { + var hosts = []; + nodes.forEach(function (d) { + if (d.class === 'host' && d.cp.device === devId) { + hosts.push(d); + } + }); + return hosts; + } + + function findAttachedLinks(devId) { + var lnks = []; + links.forEach(function (d) { + if (d.source.id === devId || d.target.id === devId) { + lnks.push(d); + } + }); + return lnks; + } + + // returns one-way links or where the internal link types differ + function findBadLinks() { + var lnks = [], + src, tgt; + links.forEach(function (d) { + // NOTE: skip edge links, which are synthesized + if (d.type() !== 'hostLink') { + delete d.bad; + src = d.fromSource; + tgt = d.fromTarget; + if (src && !tgt) { + d.bad = 'missing link'; + } else if (src.type !== tgt.type) { + d.bad = 'type mismatch'; + } + if (d.bad) { + lnks.push(d); + } + } + }); + return lnks; + } + + // ========================== + // Module definition + + angular.module('ovTopo') + .factory('TopoModelService', + ['$log', 'FnService', 'RandomService', + + function (_$log_, _fs_, _rnd_) { + $log = _$log_; + fs = _fs_; + rnd = _rnd_; + + function initModel(_api_, _dim_) { + api = _api_; + dim = _dim_; + lu = api.network.lookup; + rlk = api.network.revLinkToKey; + nodes = api.network.nodes; + links = api.network.links; + linksByDevice = api.network.linksByDevice; + } + + function newDim(_dim_) { + dim = _dim_; + } + + function destroyModel() { } + + return { + initModel: initModel, + newDim: newDim, + destroyModel: destroyModel, + + positionNode: positionNode, + createDeviceNode: createDeviceNode, + createHostNode: createHostNode, + createHostLink: createHostLink, + createLink: createLink, + coordFromLngLat: coordFromLngLat, + lngLatFromCoord: lngLatFromCoord, + findLink: findLink, + findLinkById: findLinkById, + findDevices: findDevices, + findAttachedHosts: findAttachedHosts, + findAttachedLinks: findAttachedLinks, + findBadLinks: findBadLinks + } + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js new file mode 100644 index 00000000..e84b1173 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js @@ -0,0 +1,257 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Oblique View Module. + Provides functionality to view the topology as two planes (packet & optical) + from an oblique (side-on) perspective. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, fs, sus, flash; + + // api to topoForce + var api; + /* + force() // get ref to force layout object + zoomLayer() // get ref to zoom layer + nodeGBBox() // get bounding box of node group layer + node() // get ref to D3 selection of nodes + link() // get ref to D3 selection of links + nodes() // get ref to network nodes array + tickStuff // ref to tick functions + nodeLock(b) // test-and-set nodeLock state + opacifyMap(b) // show or hide map layer + inLayer(d, layer) // return true if d in layer {'pkt'|'opt'} + calcLinkPos() // recomputes link pos based on node data + */ + + // configuration + var xsky = -.7, // x skew y factor + xsk = -35, // x skew angle + ysc = .5, // y scale + pad = 50, + time = 1500, + fill = { + pkt: 'rgba(130,130,170,0.3)', // blue-ish + opt: 'rgba(170,130,170,0.3)' // magenta-ish + }; + + // internal state + var oblique = false, + xffn = null, + plane = {}, + oldNodeLock; + + + function planeId(tag) { + return 'topo-obview-' + tag + 'Plane'; + } + + function ytfn(h, dir) { + return h * ysc * dir * 1.1; + } + + function obXform(h, dir) { + var yt = ytfn(h, dir); + return sus.scale(1, ysc) + sus.translate(0, yt) + sus.skewX(xsk); + } + + function noXform() { + return sus.skewX(0) + sus.translate(0,0) + sus.scale(1,1); + } + + function padBox(box, p) { + box.x -= p; + box.y -= p; + box.width += p*2; + box.height += p*2; + } + + function toObliqueView() { + var box = api.nodeGBBox(), + ox, oy; + + padBox(box, pad); + + ox = box.x + box.width / 2; + oy = box.y + box.height / 2; + + // remember node lock state, then lock the nodes down + oldNodeLock = api.nodeLock(true); + api.opacifyMap(false); + + insertPlanes(ox, oy); + + xffn = function (xy, dir) { + var yt = ytfn(box.height, dir), + ax = xy.x - ox, + ay = xy.y - oy, + x = ax + ay * xsky, + y = (ay + yt) * ysc; + return {x: ox + x, y: oy + y}; + }; + + showPlane('pkt', box, -1); + showPlane('opt', box, 1); + obTransitionNodes(); + } + + function toNormalView() { + xffn = null; + + hidePlane('pkt'); + hidePlane('opt'); + obTransitionNodes(); + + removePlanes(); + + // restore node lock state + api.nodeLock(oldNodeLock); + api.opacifyMap(true); + } + + function obTransitionNodes() { + // return the direction for the node + // -1 for pkt layer, 1 for optical layer + function dir(d) { + return api.inLayer(d, 'pkt') ? -1 : 1; + } + + if (xffn) { + api.nodes().forEach(function (d) { + var oldxy = {x: d.x, y: d.y}, + coords = xffn(oldxy, dir(d)); + d.oldxy = oldxy; + d.px = d.x = coords.x; + d.py = d.y = coords.y; + }); + } else { + api.nodes().forEach(function (d) { + var old = d.oldxy || {x: d.x, y: d.y}; + d.px = d.x = old.x; + d.py = d.y = old.y; + delete d.oldxy; + }); + } + + api.node().transition() + .duration(time) + .attr(api.tickStuff.nodeAttr); + api.link().transition() + .duration(time) + .call(api.calcLinkPos) + .attr(api.tickStuff.linkAttr) + .call(api.applyNumLinkLabels); + api.linkLabel().transition() + .duration(time) + .attr(api.tickStuff.linkLabelAttr); + } + + function showPlane(tag, box, dir) { + // set box origin at center.. + box.x = -box.width/2; + box.y = -box.height/2; + + plane[tag].select('rect') + .attr(box) + .attr('opacity', 0) + .transition() + .duration(time) + .attr('opacity', 1) + .attr('transform', obXform(box.height, dir)); + } + + function hidePlane(tag) { + plane[tag].select('rect') + .transition() + .duration(time) + .attr('opacity', 0) + .attr('transform', noXform()); + } + + function insertPlanes(ox, oy) { + function ins(tag) { + var id = planeId(tag), + g = api.zoomLayer().insert('g', '#topo-G') + .attr('id', id) + .attr('transform', sus.translate(ox,oy)); + g.append('rect') + .attr('fill', fill[tag]) + .attr('opacity', 0); + plane[tag] = g; + } + ins('opt'); + ins('pkt'); + } + + function removePlanes() { + function rem(tag) { + var id = planeId(tag); + api.zoomLayer().select('#'+id) + .transition() + .duration(time + 50) + .remove(); + delete plane[tag]; + } + rem('opt'); + rem('pkt'); + } + + +// === ----------------------------------------------------- +// === MODULE DEFINITION === + +angular.module('ovTopo') + .factory('TopoObliqueService', + ['$log', 'FnService', 'SvgUtilService', 'FlashService', + + function (_$log_, _fs_, _sus_, _flash_) { + $log = _$log_; + fs = _fs_; + sus = _sus_; + flash = _flash_; + + function initOblique(_api_) { + api = _api_; + } + + function destroyOblique() { } + + function toggleOblique() { + oblique = !oblique; + if (oblique) { + api.force().stop(); + flash.flash('Oblique view'); + toObliqueView(); + } else { + flash.flash('Normal view'); + toNormalView(); + } + } + + return { + initOblique: initOblique, + destroyOblique: destroyOblique, + + isOblique: function () { return oblique; }, + toggleOblique: toggleOblique + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js new file mode 100644 index 00000000..7eb45ba4 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js @@ -0,0 +1,418 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + ONOS GUI -- Topology Overlay Module. + + Provides overlay capabilities, allowing ONOS apps to provide additional + custom data/behavior for the topology view. + + */ + +(function () { + 'use strict'; + + // constants + var tos = 'TopoOverlayService: '; + + // injected refs + var $log, fs, gs, wss, ns, tss, tps, api; + + // internal state + var overlays = {}, + current = null; + + function error(fn, msg) { + $log.error(tos + fn + '(): ' + msg); + } + + function warn(fn, msg) { + $log.warn(tos + fn + '(): ' + msg); + } + + function mkGlyphId(oid, gid) { + return (gid[0] === '*') ? oid + '-' + gid.slice(1) : gid; + } + + function handleGlyphs(o) { + var gdata = fs.isO(o.glyphs), + oid = o.overlayId, + gid = o.glyphId || 'unknown', + data = {}, + note = []; + + o._glyphId = mkGlyphId(oid, gid); + + o.mkGid = function (g) { + return mkGlyphId(oid, g); + }; + o.mkId = function (s) { + return oid + '-' + s; + }; + + // process glyphs if defined + if (gdata) { + angular.forEach(gdata, function (value, key) { + var fullkey = oid + '-' + key; + data['_' + fullkey] = value.vb; + data[fullkey] = value.d; + note.push('*' + key); + }); + gs.registerGlyphs(data); + $log.debug('registered overlay glyphs:', oid, note); + } + } + + function register(overlay) { + var r = 'register', + over = fs.isO(overlay), + kb = over ? fs.isO(overlay.keyBindings) : null, + id = over ? over.overlayId : ''; + + if (!id) { + return error(r, 'not a recognized overlay'); + } + if (overlays[id]) { + return warn(r, 'already registered: "' + id + '"'); + } + overlays[id] = overlay; + handleGlyphs(overlay); + + if (kb) { + if (!fs.isA(kb._keyOrder)) { + warn(r, 'no _keyOrder array defined on keyBindings'); + } else { + kb._keyOrder.forEach(function (k) { + if (k !== '-' && !kb[k]) { + warn(r, 'no "' + k + '" property defined on keyBindings'); + } + }); + } + } + + $log.debug(tos + 'registered overlay: ' + id, overlay); + } + + // TODO: remove this redundant code....... + // NOTE: unregister needs to be called if an app is ever + // deactivated/uninstalled via the applications view +/* + function unregister(overlay) { + var u = 'unregister', + over = fs.isO(overlay), + id = over ? over.overlayId : ''; + + if (!id) { + return error(u, 'not a recognized overlay'); + } + if (!overlays[id]) { + return warn(u, 'not registered: "' + id + "'") + } + delete overlays[id]; + $log.debug(tos + 'unregistered overlay: ' + id); + } +*/ + + + // returns the list of overlay identifiers + function list() { + return d3.map(overlays).keys(); + } + + // add a radio button for each registered overlay + function augmentRbset(rset, switchFn) { + angular.forEach(overlays, function (ov) { + rset.push({ + gid: ov._glyphId, + tooltip: (ov.tooltip || '(no tooltip)'), + cb: function () { + tbSelection(ov.overlayId, switchFn); + } + }); + }); + } + + // an overlay was selected via toolbar radio button press from user + function tbSelection(id, switchFn) { + var same = current && current.overlayId === id, + payload = {}, + actions; + + function doop(op) { + var oid = current.overlayId; + $log.debug('Overlay:', op, oid); + current[op](); + payload[op] = oid; + } + + if (!same) { + current && doop('deactivate'); + current = overlays[id]; + current && doop('activate'); + actions = current && fs.isO(current.keyBindings); + switchFn(id, actions); + + wss.sendEvent('topoSelectOverlay', payload); + + // Ensure summary and details panels are updated immediately.. + wss.sendEvent('requestSummary'); + tss.updateDetail(); + } + } + + var coreButtons = { + showDeviceView: { + gid: 'switch', + tt: 'Show Device View', + path: 'device' + }, + showFlowView: { + gid: 'flowTable', + tt: 'Show Flow View for this Device', + path: 'flow' + }, + showPortView: { + gid: 'portTable', + tt: 'Show Port View for this Device', + path: 'port' + }, + showGroupView: { + gid: 'groupTable', + tt: 'Show Group View for this Device', + path: 'group' + } + }; + + // retrieves a button definition from the current overlay and generates + // a button descriptor to be added to the panel, with the data baked in + function _getButtonDef(id, data) { + var btns = current && current.buttons, + b = btns && btns[id], + cb = fs.isF(b.cb), + f = cb ? function () { cb(data); } : function () {}; + + return b ? { + id: current.mkId(id), + gid: current.mkGid(b.gid), + tt: b.tt, + cb: f + } : null; + } + + // install core buttons, and include any additional from the current overlay + function installButtons(buttons, data, devId) { + buttons.forEach(function (id) { + var btn = coreButtons[id], + gid = btn && btn.gid, + tt = btn && btn.tt, + path = btn && btn.path; + + if (btn) { + tps.addAction({ + id: 'core-' + id, + gid: gid, + tt: tt, + cb: function () { ns.navTo(path, {devId: devId }); } + }); + } else if (btn = _getButtonDef(id, data)) { + tps.addAction(btn); + } + }); + } + + function addDetailButton(id) { + var b = _getButtonDef(id); + if (b) { + tps.addAction({ + id: current.mkId(id), + gid: current.mkGid(b.gid), + cb: b.cb, + tt: b.tt + }); + } + } + + + // === ----------------------------------------------------- + // Hooks for overlays + + function _hook(x) { + var h = current && current.hooks; + return h && fs.isF(h[x]); + } + + function escapeHook() { + var eh = _hook('escape'); + return eh ? eh() : false; + } + + function emptySelectHook() { + var cb = _hook('empty'); + cb && cb(); + } + + function singleSelectHook(data) { + var cb = _hook('single'); + cb && cb(data); + } + + function multiSelectHook(selectOrder) { + var cb = _hook('multi'); + cb && cb(selectOrder); + } + + function mouseOverHook(what) { + var cb = _hook('mouseover'); + cb && cb(what); + } + + function mouseOutHook() { + var cb = _hook('mouseout'); + cb && cb(); + } + + // === ----------------------------------------------------- + // Event (from server) Handlers + + function setApi(_api_, _tss_) { + api = _api_; + tss = _tss_; + } + + function showHighlights(data) { + var less; + + /* + API to topoForce + clearLinkTrafficStyle() + removeLinkLabels() + findLinkById( id ) + findNodeById( id ) + updateLinks() + updateNodes() + supLayers( bool, [less] ) + unsupNode( id, [less] ) + unsupLink( key, [less] ) + */ + + // TODO: clear node highlighting + api.clearLinkTrafficStyle(); + api.removeLinkLabels(); + + // handle element suppression + if (data.subdue) { + less = data.subdue === 'min'; + api.supLayers(true, less); + + } else { + api.supLayers(false); + api.supLayers(false, true); + } + + data.hosts.forEach(function (host) { + var hdata = api.findNodeById(host.id); + if (hdata && !hdata.el.empty()) { + if (!host.subdue) { + api.unsupNode(hdata.id, less); + } + // TODO: further highlighting? + } + }); + + data.devices.forEach(function (device) { + var ddata = api.findNodeById(device.id); + if (ddata && !ddata.el.empty()) { + if (!device.subdue) { + api.unsupNode(ddata.id, less); + } + // TODO: further highlighting? + } + }); + + data.links.forEach(function (link) { + var ldata = api.findLinkById(link.id), + lab = link.label, + units, portcls, magnitude; + + if (ldata && !ldata.el.empty()) { + if (!link.subdue) { + api.unsupLink(ldata.key, less); + } + ldata.el.classed(link.css, true); + ldata.label = lab; + + // TODO: this needs to be pulled out into traffic overlay + // inject additional styling for port-based traffic + if (fs.endsWith(lab, 'bps')) { + units = lab.substring(lab.length-4); + portcls = 'port-traffic-' + units; + + // for GBps + if (units.substring(0,1) === 'G') { + magnitude = fs.parseBitRate(lab); + if (magnitude >= 9) { + portcls += '-choked' + } + } + ldata.el.classed(portcls, true); + } + } + }); + + api.updateNodes(); + api.updateLinks(); + } + + // ======================================================================== + + angular.module('ovTopo') + .factory('TopoOverlayService', + ['$log', 'FnService', 'GlyphService', 'WebSocketService', 'NavService', + 'TopoPanelService', + + function (_$log_, _fs_, _gs_, _wss_, _ns_, _tps_) { + $log = _$log_; + fs = _fs_; + gs = _gs_; + wss = _wss_; + ns = _ns_; + tps = _tps_; + + return { + register: register, + //unregister: unregister, + setApi: setApi, + list: list, + augmentRbset: augmentRbset, + mkGlyphId: mkGlyphId, + tbSelection: tbSelection, + installButtons: installButtons, + addDetailButton: addDetailButton, + hooks: { + escape: escapeHook, + emptySelect: emptySelectHook, + singleSelect: singleSelectHook, + multiSelect: multiSelectHook, + mouseOver: mouseOverHook, + mouseOut: mouseOutHook + }, + + showHighlights: showHighlights + } + }]); + +}());
\ No newline at end of file diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js new file mode 100644 index 00000000..7db17f0d --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js @@ -0,0 +1,539 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Panel Module. + Defines functions for manipulating the summary, detail, and instance panels. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, $window, $rootScope, fs, ps, gs, flash, wss, bns, mast, ns; + + // constants + var pCls = 'topo-p', + idSum = 'topo-p-summary', + idDet = 'topo-p-detail', + panelOpts = { + width: 260 + }, + sumMax = 240, + padTop = 20, + devPath = 'device'; + + // internal state + var useDetails = true, // should we show details if we have 'em? + haveDetails = false, // do we have details that we could show? + sumFromTop, // summary panel distance from top of screen + unbindWatch; + + // panels + var summary, detail; + + // === ----------------------------------------------------- + // Panel API + function createTopoPanel(id, opts) { + var p = ps.createPanel(id, opts), + pid = id, + header, body, footer; + p.classed(pCls, true); + + function panel() { + return p; + } + + function hAppend(x) { + return header.append(x); + } + + function bAppend(x) { + return body.append(x); + } + + function fAppend(x) { + return footer.append(x); + } + + function setup() { + p.empty(); + + p.append('div').classed('header', true); + p.append('div').classed('body', true); + p.append('div').classed('footer', true); + + header = p.el().select('.header'); + body = p.el().select('.body'); + footer = p.el().select('.footer'); + } + + function destroy() { + ps.destroyPanel(pid); + } + + // fromTop is how many pixels from the top of the page the panel is + // max is the max height of the panel in pixels + // only adjusts if the body content would be 10px or larger + function adjustHeight(fromTop, max) { + var totalPHeight, avSpace, + overflow = 0, + pdg = 30; + + if (!fromTop) { + $log.warn('adjustHeight: height from top of page not given'); + return null; + } else if (!body || !p) { + $log.warn('adjustHeight: panel contents are not defined'); + return null; + } + + p.el().style('height', null); + body.style('height', null); + + totalPHeight = fromTop + p.height(); + avSpace = fs.windowSize(pdg).height; + + if (totalPHeight >= avSpace) { + overflow = totalPHeight - avSpace; + } + + function _adjustBody(height) { + if (height < 10) { + return false; + } else { + body.style('height', height + 'px'); + } + return true; + } + + if (!_adjustBody(fs.noPxStyle(body, 'height') - overflow)) { + return; + } + + if (max && p.height() > max) { + _adjustBody(fs.noPxStyle(body, 'height') - (p.height() - max)); + } + } + + return { + panel: panel, + setup: setup, + destroy: destroy, + appendHeader: hAppend, + appendBody: bAppend, + appendFooter: fAppend, + adjustHeight: adjustHeight + }; + } + + // === ----------------------------------------------------- + // Utility functions + + function addSep(tbody) { + tbody.append('tr').append('td').attr('colspan', 2).append('hr'); + } + + function addBtnFooter() { + detail.appendFooter('hr'); + detail.appendFooter('div').classed('actionBtns', true); + } + + function addProp(tbody, label, value) { + var tr = tbody.append('tr'), + lab; + if (typeof label === 'string') { + lab = label.replace(/_/g, ' '); + } else { + lab = label; + } + + function addCell(cls, txt) { + tr.append('td').attr('class', cls).html(txt); + } + addCell('label', lab + ' :'); + addCell('value', value); + } + + function listProps(tbody, data) { + data.propOrder.forEach(function (p) { + if (p === '-') { + addSep(tbody); + } else { + addProp(tbody, p, data.props[p]); + } + }); + } + + function watchWindow() { + unbindWatch = $rootScope.$watchCollection( + function () { + return { + h: $window.innerHeight, + w: $window.innerWidth + }; + }, function () { + summary.adjustHeight(sumFromTop, sumMax); + detail.adjustHeight(detail.ypos.current); + } + ); + } + + // === ----------------------------------------------------- + // Functions for populating the summary panel + + function populateSummary(data) { + summary.setup(); + + var svg = summary.appendHeader('div') + .classed('icon', true) + .append('svg'), + title = summary.appendHeader('h2'), + table = summary.appendBody('table'), + tbody = table.append('tbody'), + glyphId = data.type || 'node'; + + gs.addGlyph(svg, glyphId, 40); + + if (glyphId === 'node') { + gs.addGlyph(svg, 'bird', 24, true, [8,12]); + } + + title.text(data.title); + listProps(tbody, data); + } + + // === ----------------------------------------------------- + // Functions for populating the detail panel + + var isDevice = { + switch: 1, + roadm: 1 + }; + + function displaySingle(data) { + detail.setup(); + + var svg = detail.appendHeader('div') + .classed('icon clickable', true) + .append('svg'), + title = detail.appendHeader('h2') + .classed('clickable', true), + table = detail.appendBody('table'), + tbody = table.append('tbody'), + navFn; + + gs.addGlyph(svg, (data.type || 'unknown'), 40); + title.text(data.title); + + // only add navigation when displaying a device + if (isDevice[data.type]) { + navFn = function () { + ns.navTo(devPath, { devId: data.id }); + }; + + svg.on('click', navFn); + title.on('click', navFn); + } + + listProps(tbody, data); + addBtnFooter(); + } + + function displayMulti(ids) { + detail.setup(); + + var title = detail.appendHeader('h3'), + table = detail.appendBody('table'), + tbody = table.append('tbody'); + + title.text('Selected Nodes'); + ids.forEach(function (d, i) { + addProp(tbody, i+1, d); + }); + addBtnFooter(); + } + + function addAction(o) { + var btnDiv = d3.select('#' + idDet) + .select('.actionBtns') + .append('div') + .classed('actionBtn', true); + bns.button(btnDiv, idDet + '-' + o.id, o.gid, o.cb, o.tt); + } + + var friendlyIndex = { + device: 1, + host: 0 + }; + + function friendly(d) { + var i = friendlyIndex[d.class] || 0; + return (d.labels && d.labels[i]) || ''; + } + + function linkSummary(d) { + var o = d && d.online ? 'online' : 'offline'; + return d ? d.type + ' / ' + o : '-'; + } + + // provided to change presentation of internal type name + var linkTypePres = { + hostLink: 'edge link' + }; + + function linkType(d) { + return linkTypePres[d.type()] || d.type(); + } + + var coreOrder = [ + 'Type', '-', + 'A_type', 'A_id', 'A_label', 'A_port', '-', + 'B_type', 'B_id', 'B_label', 'B_port', '-' + ], + edgeOrder = [ + 'Type', '-', + 'A_type', 'A_id', 'A_label', '-', + 'B_type', 'B_id', 'B_label', 'B_port' + ]; + + function displayLink(data) { + detail.setup(); + + var svg = detail.appendHeader('div') + .classed('icon', true) + .append('svg'), + title = detail.appendHeader('h2'), + table = detail.appendBody('table'), + tbody = table.append('tbody'), + edgeLink = data.type() === 'hostLink', + order = edgeLink ? edgeOrder : coreOrder; + + gs.addGlyph(svg, 'ports', 40); + title.text('Link'); + + + listProps(tbody, { + propOrder: order, + props: { + Type: linkType(data), + + A_type: data.source.class, + A_id: data.source.id, + A_label: friendly(data.source), + A_port: data.srcPort, + + B_type: data.target.class, + B_id: data.target.id, + B_label: friendly(data.target), + B_port: data.tgtPort + } + }); + + if (!edgeLink) { + addProp(tbody, 'A → B', linkSummary(data.fromSource)); + addProp(tbody, 'B → A', linkSummary(data.fromTarget)); + } + } + + function displayNothing() { + haveDetails = false; + hideDetailPanel(); + } + + function displaySomething() { + haveDetails = true; + if (useDetails) { + showDetailPanel(); + } + } + + // === ----------------------------------------------------- + // Event Handlers + + function showSummary(data) { + populateSummary(data); + showSummaryPanel(); + } + + function toggleSummary(x) { + var kev = (x === 'keyev'), + on = kev ? !summary.panel().isVisible() : !!x, + verb = on ? 'Show' : 'Hide'; + + if (on) { + // ask server to start sending summary data. + wss.sendEvent('requestSummary'); + // note: the summary panel will appear, once data arrives + } else { + hideSummaryPanel(); + } + flash.flash(verb + ' summary panel'); + return on; + } + + // === ----------------------------------------------------- + // === LOGIC For showing/hiding summary and detail panels... + + function showSummaryPanel() { + function _show() { + summary.panel().show(); + summary.adjustHeight(sumFromTop, sumMax); + } + if (detail.panel().isVisible()) { + detail.down(_show); + } else { + _show(); + } + } + + function hideSummaryPanel() { + // instruct server to stop sending summary data + wss.sendEvent("cancelSummary"); + summary.panel().hide(detail.up); + } + + function showDetailPanel() { + if (summary.panel().isVisible()) { + detail.down(detail.panel().show); + } else { + detail.up(detail.panel().show); + } + } + + function hideDetailPanel() { + detail.panel().hide(); + } + + // ========================== + + function augmentDetailPanel() { + var d = detail, + downPos = sumFromTop + sumMax + 20; + d.ypos = { up: sumFromTop, down: downPos, current: downPos}; + + d._move = function (y, cb) { + var yp = d.ypos, + endCb; + + if (fs.isF(cb)) { + endCb = function () { + cb(); + d.adjustHeight(d.ypos.current); + } + } else { + endCb = function () { + d.adjustHeight(d.ypos.current); + } + } + if (yp.current !== y) { + yp.current = y; + d.panel().el().transition().duration(300) + .each('end', endCb) + .style('top', yp.current + 'px'); + } else { + endCb(); + } + }; + + d.down = function (cb) { d._move(d.ypos.down, cb); }; + d.up = function (cb) { d._move(d.ypos.up, cb); }; + } + + function toggleUseDetailsFlag(x) { + var kev = (x === 'keyev'), + verb; + + useDetails = kev ? !useDetails : !!x; + verb = useDetails ? 'Enable' : 'Disable'; + + if (useDetails) { + if (haveDetails) { + showDetailPanel(); + } + } else { + hideDetailPanel(); + } + flash.flash(verb + ' details panel'); + return useDetails; + } + + // ========================== + + function initPanels() { + sumFromTop = mast.mastHeight() + padTop; + summary = createTopoPanel(idSum, panelOpts); + detail = createTopoPanel(idDet, panelOpts); + + augmentDetailPanel(); + watchWindow(); + } + + function destroyPanels() { + summary.destroy(); + summary = null; + + detail.destroy(); + detail = null; + haveDetails = false; + unbindWatch(); + } + + // ========================== + + angular.module('ovTopo') + .factory('TopoPanelService', + ['$log', '$window', '$rootScope', 'FnService', 'PanelService', 'GlyphService', + 'FlashService', 'WebSocketService', 'ButtonService', 'MastService', + 'NavService', + + function (_$log_, _$window_, _$rootScope_, + _fs_, _ps_, _gs_, _flash_, _wss_, _bns_, _mast_, _ns_) { + $log = _$log_; + $window = _$window_; + $rootScope = _$rootScope_; + fs = _fs_; + ps = _ps_; + gs = _gs_; + flash = _flash_; + wss = _wss_; + bns = _bns_; + mast = _mast_; + ns = _ns_; + + return { + initPanels: initPanels, + destroyPanels: destroyPanels, + createTopoPanel: createTopoPanel, + + showSummary: showSummary, + toggleSummary: toggleSummary, + + toggleUseDetailsFlag: toggleUseDetailsFlag, + displaySingle: displaySingle, + displayMulti: displayMulti, + displayLink: displayLink, + displayNothing: displayNothing, + displaySomething: displaySomething, + addAction: addAction, + + hideSummaryPanel: hideSummaryPanel, + + detailVisible: function () { return detail.panel().isVisible(); }, + summaryVisible: function () { return summary.panel().isVisible(); } + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js new file mode 100644 index 00000000..483c4baa --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js @@ -0,0 +1,286 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Selection Module. + Defines behavior when selecting nodes. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, fs, wss, tov, tps, tts, ns; + + // api to topoForce + var api; + /* + node() // get ref to D3 selection of nodes + zoomingOrPanning( ev ) + updateDeviceColors( [dev] ) + deselectLink() + */ + + // internal state + var hovered, // the node over which the mouse is hovering + selections = {}, // currently selected nodes (by id) + selectOrder = [], // the order in which we made selections + consumeClick = false; // used to coordinate with SVG click handler + + // ========================== + + function nSel() { + return selectOrder.length; + } + function getSel(idx) { + return selections[selectOrder[idx]]; + } + function allSelectionsClass(cls) { + for (var i=0, n=nSel(); i<n; i++) { + if (getSel(i).obj.class !== cls) { + return false; + } + } + return true; + } + + // ========================== + + function nodeMouseOver(m) { + if (!m.dragStarted) { + if (hovered != m) { + hovered = m; + tov.hooks.mouseOver({ + id: m.id, + class: m.class, + type: m.type + }); + } + } + } + + function nodeMouseOut(m) { + if (!m.dragStarted) { + if (hovered) { + hovered = null; + tov.hooks.mouseOut(); + } + } + } + + // ========================== + + function selectObject(obj) { + var el = this, + nodeEv = el && el.tagName === 'g', + ev = d3.event.sourceEvent || {}, + n; + + if (api.zoomingOrPanning(ev)) { + return; + } + + if (nodeEv) { + n = d3.select(el); + } else { + api.node().each(function (d) { + if (d == obj) { + n = d3.select(el = this); + } + }); + } + if (!n) return; + + if (nodeEv) { + consumeClick = true; + } + api.deselectLink(); + + if (ev.shiftKey && n.classed('selected')) { + deselectObject(obj.id); + updateDetail(); + return; + } + + if (!ev.shiftKey) { + deselectAll(true); + } + + selections[obj.id] = { obj: obj, el: el }; + selectOrder.push(obj.id); + + n.classed('selected', true); + api.updateDeviceColors(obj); + updateDetail(); + } + + function deselectObject(id) { + var obj = selections[id]; + if (obj) { + d3.select(obj.el).classed('selected', false); + delete selections[id]; + fs.removeFromArray(id, selectOrder); + api.updateDeviceColors(obj.obj); + } + } + + function deselectAll(skipUpdate) { + var something = (selectOrder.length > 0); + + // deselect all nodes in the network... + api.node().classed('selected', false); + selections = {}; + selectOrder = []; + api.updateDeviceColors(); + if (!skipUpdate) { + updateDetail(); + } + + // return true if something was selected + return something; + } + + // === ----------------------------------------------------- + + function requestDetails(data) { + wss.sendEvent('requestDetails', { + id: data.id, + class: data.class + }); + } + + // === ----------------------------------------------------- + + function updateDetail() { + var nSel = selectOrder.length; + if (!nSel) { + emptySelect(); + } else if (nSel === 1) { + singleSelect(); + } else { + multiSelect(); + } + } + + function emptySelect() { + tov.hooks.emptySelect(); + tps.displayNothing(); + } + + function singleSelect() { + var data = getSel(0).obj; + requestDetails(data); + // NOTE: detail panel is shown as a response to receiving + // a 'showDetails' event from the server. See 'showDetails' + // callback function below... + } + + function multiSelect() { + // display the selected nodes in the detail panel + tps.displayMulti(selectOrder); + addHostSelectionActions(); + tov.hooks.multiSelect(selectOrder); + tps.displaySomething(); + } + + function addHostSelectionActions() { + if (allSelectionsClass('host')) { + if (nSel() === 2) { + tps.addAction({ + id: 'host-flow-btn', + gid: 'endstation', + cb: tts.addHostIntent, + tt: 'Create Host-to-Host Flow' + }); + } else if (nSel() >= 2) { + tps.addAction({ + id: 'mult-src-flow-btn', + gid: 'flows', + cb: tts.addMultiSourceIntent, + tt: 'Create Multi-Source Flow' + }); + } + } + } + + + // === ----------------------------------------------------- + // Event Handlers + + // display the data for the single selected node + function showDetails(data) { + var buttons = fs.isA(data.buttons) || []; + tps.displaySingle(data); + tov.installButtons(buttons, data, data.props['URI']); + tov.hooks.singleSelect(data); + tps.displaySomething(); + } + + // returns true if one or more nodes are selected. + function somethingSelected() { + return nSel(); + } + + function clickConsumed(x) { + var cc = consumeClick; + consumeClick = !!x; + return cc; + } + + // === ----------------------------------------------------- + // === MODULE DEFINITION === + + angular.module('ovTopo') + .factory('TopoSelectService', + ['$log', 'FnService', 'WebSocketService', 'TopoOverlayService', + 'TopoPanelService', 'TopoTrafficService', 'NavService', + + function (_$log_, _fs_, _wss_, _tov_, _tps_, _tts_, _ns_) { + $log = _$log_; + fs = _fs_; + wss = _wss_; + tov = _tov_; + tps = _tps_; + tts = _tts_; + ns = _ns_; + + function initSelect(_api_) { + api = _api_; + } + + function destroySelect() { } + + return { + initSelect: initSelect, + destroySelect: destroySelect, + + showDetails: showDetails, + + nodeMouseOver: nodeMouseOver, + nodeMouseOut: nodeMouseOut, + selectObject: selectObject, + deselectObject: deselectObject, + deselectAll: deselectAll, + updateDetail: updateDetail, + + hovered: function () { return hovered; }, + selectOrder: function () { return selectOrder; }, + somethingSelected: somethingSelected, + + clickConsumed: clickConsumed + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js new file mode 100644 index 00000000..1c957412 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js @@ -0,0 +1,262 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Sprite Module. + Defines behavior for loading sprites into the sprite layer. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, $http, fs, gs, sus, wss; + + // constants + var tssid = 'TopoSpriteService: ', + fontsize = 20; // default font size 20pt. + + // internal state + var spriteLayer, defsElement; + + + function registerPathsAsGlyphs(paths) { + var custom = {}, + ids = []; + + function mkd(d) { + return fs.isA(d) ? d.join('') : d; + } + + paths.forEach(function (path) { + var tag = 'spr_' + path.tag; + + if (path.glyph) { + // assumption is that we are using a built-in glyph + return; + } + + custom['_' + tag] = path.viewbox || '0 0 1000 1000'; + custom[tag] = mkd(path.d); + ids.push(tag); + }); + + gs.registerGlyphs(custom); + gs.loadDefs(defsElement, ids, true); + } + + function applyStrokeStyle(s, use) { + var style; + if (s) { + style = {}; + angular.forEach(s, function (value, key) { + style['stroke-' + key] = value; + }); + use.style(style); + } + } + + function applyFillClass(f, use) { + use.classed('fill-' + f, true); + } + + function doSprite(spr, def, pathmeta) { + var pmeta = pathmeta[def.path], + c = spr.class || 'gray1', + p = spr.pos || [0,0], + lab = spr.label, + dim = def.dim || [40,40], + w = dim[0], + h = dim[1], + dy = def.labelyoff || 1, + sc = def.scale, + xfm = sus.translate(p), + g, attr, use; + + if (sc) { + xfm += sus.scale(sc, sc); + } + + g = spriteLayer.append('g') + .classed(c, true) + .attr('transform', xfm); + + attr = { + width: w, + height: h, + 'xlink:href': '#' + pmeta.u + }; + + use = g.append('use').attr(attr); + applyStrokeStyle(pmeta.s, use); + applyFillClass(def.fill, use); + + + // add subpaths if they have been defined + if (fs.isA(def.subpaths)) { + def.subpaths.forEach(function (v) { + pmeta = pathmeta[v.path]; + attr = { + width: w, + height: h, + 'xlink:href': '#' + pmeta.u, + transform: sus.translate(v.pos) + }; + use = g.append('use').attr(attr); + applyStrokeStyle(pmeta.s, use); + applyFillClass(def.subpathfill, use); + }); + } + + if (lab) { + g.append('text') + .text(lab) + .attr({ x: w / 2, y: h * dy }); + } + } + + function doLabel(label) { + var c = label.class || 'gray1', + p = label.pos || [0,0], + sz = label.size || 1.0, + g = spriteLayer.append('g') + .classed(c, true) + .attr('transform', sus.translate(p)) + .append('text') + .text(label.text) + .style('font-size', (fontsize * sz)+'pt'); + } + + + // ========================== + // event handlers + + // Handles response from 'spriteListRequest' which lists all the + // registered sprite definitions on the server. + // (see onos-upload-sprites) + function inList(payload) { + $log.debug(tssid + 'Registered sprite definitions:', payload.names); + // Some day, we will make this list available to the user in + // a dropdown selection box... + } + + // Handles response from 'spriteDataRequest' which provides the + // data for the requested sprite definition. + function inData(payload) { + var data = payload.data, + name, desc, pfx, sprites, labels, alpha, + paths, defn, load, + pathmeta = {}, + defs = {}, + warn = []; + + if (!data) { + $log.warn(tssid + 'No sprite data loaded.'); + return; + } + name = data.defn_name; + desc = data.defn_desc; + paths = data.paths; + defn = data.defn; + load = data.load; + pfx = tssid + '[' + name + ']: '; + + $log.debug("Loading sprites...[" + name + "]", desc); + + function no(what) { + warn.push(pfx + 'No ' + what + ' property defined'); + } + + if (!paths) no('paths'); + if (!defn) no('defn'); + if (!load) no('load'); + + if (warn.length) { + $log.error(warn.join('\n')); + return; + } + + // any custom paths need to be added to the glyph DB, and imported + registerPathsAsGlyphs(paths); + + paths.forEach(function (p) { + pathmeta[p.tag] = { + s: p.stroke, + u: p.glyph || 'spr_' + p.tag + }; + }); + + defn.forEach(function (d) { + defs[d.id] = d; + }); + + // sprites, labels and alpha are each optional components of the load + sprites = load.sprites; + labels = load.labels; + alpha = load.alpha; + + if (alpha) { + spriteLayer.style('opacity', alpha); + } + + if (sprites) { + sprites.forEach(function (spr) { + var def = defs[spr.id]; + doSprite(spr, def, pathmeta); + }); + } + + if (labels) { + labels.forEach(doLabel); + } + } + + + function loadSprites(layer, defsElem, defname) { + var name = defname || 'sprites'; + spriteLayer = layer; + defsElement = defsElem; + + $log.info(tssid + 'Requesting sprite definition ['+name+']...'); + + wss.sendEvent('spriteListRequest'); + wss.sendEvent('spriteDataRequest', {name: name}); + } + + // === ----------------------------------------------------- + // === MODULE DEFINITION === + + angular.module('ovTopo') + .factory('TopoSpriteService', + ['$log', '$http', 'FnService', 'GlyphService', + 'SvgUtilService', 'WebSocketService', + + function (_$log_, _$http_, _fs_, _gs_, _sus_, _wss_) { + $log = _$log_; + $http = _$http_; + fs = _fs_; + gs = _gs_; + sus = _sus_; + wss = _wss_; + + return { + loadSprites: loadSprites, + spriteListResponse: inList, + spriteDataResponse: inData + }; + }]); + +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js new file mode 100644 index 00000000..84de261b --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js @@ -0,0 +1,284 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Toolbar Module. + Functions for creating and interacting with the toolbar. + */ + +(function () { + 'use strict'; + + // injected references + var $log, fs, tbs, ps, tov, api; + + // API: + // getActionEntry + // setUpKeys + + // internal state + var toolbar, keyData, cachedState, thirdRow; + + // constants + var name = 'topo-tbar', + cooktag = 'topo_prefs', + soa = 'switchOverlayActions: ', + selOver = 'Select overlay here ⇧'; + + + // key to button mapping data + var k2b = { + O: { id: 'summary-tog', gid: 'summary', isel: true}, + I: { id: 'instance-tog', gid: 'uiAttached', isel: true }, + D: { id: 'details-tog', gid: 'details', isel: true }, + H: { id: 'hosts-tog', gid: 'endstation', isel: false }, + M: { id: 'offline-tog', gid: 'switch', isel: true }, + P: { id: 'ports-tog', gid: 'ports', isel: true }, + B: { id: 'bkgrnd-tog', gid: 'map', isel: false }, + S: { id: 'sprite-tog', gid: 'cloud', isel: false }, + + //X: { id: 'nodelock-tog', gid: 'lock', isel: false }, + Z: { id: 'oblique-tog', gid: 'oblique', isel: false }, + N: { id: 'filters-btn', gid: 'filters' }, + L: { id: 'cycleLabels-btn', gid: 'cycleLabels' }, + R: { id: 'resetZoom-btn', gid: 'resetZoom' }, + + E: { id: 'eqMaster-btn', gid: 'eqMaster' } + }; + + var prohibited = [ + 'T', 'backSlash', 'slash', + 'X' // needed until we re-instate X above. + ]; + prohibited = prohibited.concat(d3.map(k2b).keys()); + + + // initial toggle state: default settings and tag to key mapping + var defaultPrefsState = { + summary: 1, + insts: 1, + detail: 1, + hosts: 0, + offdev: 1, + porthl: 1, + bg: 0, + spr: 0, + toolbar: 0 + }, + prefsMap = { + summary: 'O', + insts: 'I', + detail: 'D', + hosts: 'H', + offdev: 'M', + porthl: 'P', + bg: 'B', + spr: 'S' + // NOTE: toolbar state is handled separately + }; + + function init(_api_) { + api = _api_; + + // retrieve initial toggle button settings from user prefs + setInitToggleState(); + } + + function topoDefPrefs() { + return angular.extend({}, defaultPrefsState); + } + + function setInitToggleState() { + cachedState = ps.asNumbers(ps.getPrefs(cooktag)); + $log.debug('TOOLBAR---- read prefs state:', cachedState); + + if (!cachedState) { + cachedState = topoDefPrefs(); + ps.setPrefs(cooktag, cachedState); + $log.debug('TOOLBAR---- Set default prefs state:', cachedState); + } + + angular.forEach(prefsMap, function (v, k) { + var cfg = k2b[v]; + cfg && (cfg.isel = !!cachedState[k]); + }); + } + + function initKeyData() { + // TODO: use angular forEach instead of d3.map + keyData = d3.map(k2b); + keyData.forEach(function(key, value) { + var data = api.getActionEntry(key); + value.cb = data[0]; // on-click callback + value.tt = data[1] + ' (' + key + ')'; // tooltip + }); + } + + function addButton(key) { + var v = keyData.get(key); + v.btn = toolbar.addButton(v.id, v.gid, v.cb, v.tt); + } + + function addToggle(key, suppressIfMobile) { + var v = keyData.get(key); + if (suppressIfMobile && fs.isMobile()) { return; } + v.tog = toolbar.addToggle(v.id, v.gid, v.isel, v.cb, v.tt); + } + + function addFirstRow() { + addToggle('I'); + addToggle('O'); + addToggle('D'); + toolbar.addSeparator(); + + addToggle('H'); + addToggle('M'); + addToggle('P', true); + addToggle('B'); + addToggle('S', true); + } + + function addSecondRow() { + //addToggle('X'); + addToggle('Z'); + addButton('N'); + addButton('L'); + addButton('R'); + toolbar.addSeparator(); + addButton('E'); + } + + function addOverlays() { + toolbar.addSeparator(); + + // generate radio button set for overlays; start with 'none' + var rset = [{ + gid: 'topo', + tooltip: 'No Overlay', + cb: function () { + tov.tbSelection(null, switchOverlayActions); + } + }]; + tov.augmentRbset(rset, switchOverlayActions); + toolbar.addRadioSet('topo-overlays', rset); + } + + // invoked by overlay service to switch out old buttons and switch in new + function switchOverlayActions(oid, keyBindings) { + var prohibits = [], + kb = fs.isO(keyBindings) || {}, + order = fs.isA(kb._keyOrder) || []; + + if (keyBindings && !keyBindings._keyOrder) { + $log.warn(soa + 'no _keyOrder property defined'); + } else { + // sanity removal of reserved property names + ['esc', '_keyListener', '_helpFormat'].forEach(function (k) { + fs.removeFromArray(k, order); + }); + } + + thirdRow.clear(); + + if (!order.length) { + thirdRow.setText(selOver); + thirdRow.classed('right', true); + api.setUpKeys(); // clear previous overlay key bindings + + } else { + thirdRow.classed('right', false); + angular.forEach(order, function (key) { + var value, bid, gid, tt; + + if (prohibited.indexOf(key) > -1) { + prohibits.push(key); + + } else { + value = keyBindings[key]; + bid = oid + '-' + key; + gid = tov.mkGlyphId(oid, value.gid); + tt = value.tt + ' (' + key + ')'; + thirdRow.addButton(bid, gid, value.cb, tt); + } + }); + api.setUpKeys(keyBindings); // add overlay key bindings + } + + if (prohibits.length) { + $log.warn(soa + 'Prohibited key bindings ignored:', prohibits); + } + } + + function createToolbar() { + initKeyData(); + toolbar = tbs.createToolbar(name); + addFirstRow(); + toolbar.addRow(); + addSecondRow(); + addOverlays(); + thirdRow = toolbar.addRow(); + thirdRow.setText(selOver); + thirdRow.classed('right', true); + + if (cachedState.toolbar) { + toolbar.show(); + } else { + toolbar.hide(); + } + } + + function destroyToolbar() { + tbs.destroyToolbar(name); + } + + // allows us to ensure the button states track key strokes + function keyListener(key) { + var v = keyData.get(key); + + if (v) { + // we have a valid button mapping + if (v.tog) { + // it's a toggle button + v.tog.toggleNoCb(); + } + } + } + + function toggleToolbar() { + toolbar.toggle(); + } + + angular.module('ovTopo') + .factory('TopoToolbarService', + ['$log', 'FnService', 'ToolbarService', 'PrefsService', + 'TopoOverlayService', + + function (_$log_, _fs_, _tbs_, _ps_, _tov_) { + $log = _$log_; + fs = _fs_; + tbs = _tbs_; + ps = _ps_; + tov = _tov_; + + return { + init: init, + createToolbar: createToolbar, + destroyToolbar: destroyToolbar, + keyListener: keyListener, + toggleToolbar: toggleToolbar + }; + }]); +}());
\ No newline at end of file diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js new file mode 100644 index 00000000..ca379360 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js @@ -0,0 +1,220 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Topology Traffic Module. + Defines behavior for viewing different traffic modes. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, fs, flash, wss, api; + + /* + API to topoForce + hovered() + somethingSelected() + selectOrder() + */ + + // internal state + var trafficMode = null, + hoverMode = null; + + + // === ----------------------------------------------------- + // Helper functions + + // invoked in response to change in selection and/or mouseover/out: + function requestTrafficForMode(mouse) { + if (trafficMode === 'flows') { + requestDeviceLinkFlows(); + } else if (trafficMode === 'intents') { + if (!mouse || hoverMode === 'intents') { + requestRelatedIntents(); + } + } else { + // do nothing + } + } + + function requestDeviceLinkFlows() { + // generates payload based on current hover-state + var hov = api.hovered(); + + function hoverValid() { + return hoverMode === 'flows' && + hov && (hov.class === 'device'); + } + + if (api.somethingSelected()) { + wss.sendEvent('requestDeviceLinkFlows', { + ids: api.selectOrder(), + hover: hoverValid() ? hov.id : '' + }); + } + } + + function requestRelatedIntents() { + // generates payload based on current hover-state + var hov = api.hovered(); + + function hoverValid() { + return hoverMode === 'intents' && + hov && (hov.class === 'host' || hov.class === 'device'); + } + + if (api.somethingSelected()) { + wss.sendEvent('requestRelatedIntents', { + ids: api.selectOrder(), + hover: hoverValid() ? hov.id : '' + }); + } + } + + + // === ------------------------------------------------------------- + // Traffic requests invoked from keystrokes or toolbar buttons... + + function cancelTraffic(forced) { + if (!trafficMode || (!forced && trafficMode === 'allFlowPort')) { + return false; + } + + trafficMode = hoverMode = null; + wss.sendEvent('cancelTraffic'); + flash.flash('Traffic monitoring canceled'); + return true; + } + + function showAllFlowTraffic() { + trafficMode = 'allFlowPort'; + hoverMode = null; + wss.sendEvent('requestAllFlowTraffic'); + flash.flash('All Flow Traffic'); + } + + function showAllPortTraffic() { + trafficMode = 'allFlowPort'; + hoverMode = null; + wss.sendEvent('requestAllPortTraffic'); + flash.flash('All Port Traffic'); + } + + function showDeviceLinkFlows () { + trafficMode = hoverMode = 'flows'; + requestDeviceLinkFlows(); + flash.flash('Device Flows'); + } + + function showRelatedIntents () { + trafficMode = hoverMode = 'intents'; + requestRelatedIntents(); + flash.flash('Related Paths'); + } + + function showPrevIntent() { + if (trafficMode === 'intents') { + hoverMode = null; + wss.sendEvent('requestPrevRelatedIntent'); + flash.flash('Previous related intent'); + } + } + + function showNextIntent() { + if (trafficMode === 'intents') { + hoverMode = null; + wss.sendEvent('requestNextRelatedIntent'); + flash.flash('Next related intent'); + } + } + + function showSelectedIntentTraffic() { + if (trafficMode === 'intents') { + hoverMode = null; + wss.sendEvent('requestSelectedIntentTraffic'); + flash.flash('Traffic on Selected Path'); + } + } + + + // === ------------------------------------------------------ + // action buttons on detail panel (multiple selection) + + function addHostIntent () { + var so = api.selectOrder(); + wss.sendEvent('addHostIntent', { + one: so[0], + two: so[1], + ids: so + }); + trafficMode = 'intents'; + hoverMode = null; + flash.flash('Host-to-Host flow added'); + } + + function addMultiSourceIntent () { + var so = api.selectOrder(); + wss.sendEvent('addMultiSourceIntent', { + src: so.slice(0, so.length - 1), + dst: so[so.length - 1], + ids: so + }); + trafficMode = 'intents'; + hoverMode = null; + flash.flash('Multi-Source flow added'); + } + + + // === ----------------------------------------------------- + // === MODULE DEFINITION === + + angular.module('ovTopo') + .factory('TopoTrafficService', + ['$log', 'FnService', 'FlashService', 'WebSocketService', + + function (_$log_, _fs_, _flash_, _wss_) { + $log = _$log_; + fs = _fs_; + flash = _flash_; + wss = _wss_; + + return { + initTraffic: function (_api_) { api = _api_; }, + destroyTraffic: function () { }, + + // invoked from toolbar overlay buttons or keystrokes + cancelTraffic: cancelTraffic, + showAllFlowTraffic: showAllFlowTraffic, + showAllPortTraffic: showAllPortTraffic, + showDeviceLinkFlows: showDeviceLinkFlows, + showRelatedIntents: showRelatedIntents, + showPrevIntent: showPrevIntent, + showNextIntent: showNextIntent, + showSelectedIntentTraffic: showSelectedIntentTraffic, + + // invoked from mouseover/mouseout and selection change + requestTrafficForMode: requestTrafficForMode, + + // TODO: these should move to new UI demo app + // invoked from buttons on detail (multi-select) panel + addHostIntent: addHostIntent, + addMultiSourceIntent: addMultiSourceIntent + }; + }]); +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js new file mode 100644 index 00000000..cb4bc49a --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js @@ -0,0 +1,159 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + ONOS GUI -- Topology Traffic Overlay Module. + Defines behavior for viewing different traffic modes. + Installed as a Topology Overlay. + */ +(function () { + 'use strict'; + + // injected refs + var $log, tov, tts; + + // NOTE: no internal state here -- see TopoTrafficService for that + + // NOTE: providing button disabling requires too big a refactoring of + // the button factory etc. Will have to be done another time. + + + // traffic overlay definition + var overlay = { + overlayId: 'traffic', + glyphId: 'allTraffic', + tooltip: 'Traffic Overlay', + + // NOTE: Traffic glyphs already installed as part of the base ONOS set. + + activate: function () { + $log.debug("Traffic overlay ACTIVATED"); + }, + + deactivate: function () { + tts.cancelTraffic(true); + $log.debug("Traffic overlay DEACTIVATED"); + }, + + // detail panel button definitions + // (keys match button identifiers, also defined in TrafficOverlay.java) + buttons: { + showDeviceFlows: { + gid: 'flows', + tt: 'Show Device Flows', + cb: function (data) { tts.showDeviceLinkFlows(); } + }, + + showRelatedTraffic: { + gid: 'relatedIntents', + tt: 'Show Related Traffic', + cb: function (data) { tts.showRelatedIntents(); } + } + }, + + // key bindings for traffic overlay toolbar buttons + // NOTE: fully qual. button ID is derived from overlay-id and key-name + keyBindings: { + 0: { + cb: function () { tts.cancelTraffic(true); }, + tt: 'Cancel traffic monitoring', + gid: 'xMark' + }, + + A: { + cb: function () { tts.showAllFlowTraffic(); }, + tt: 'Monitor all traffic using flow stats', + gid: 'allTraffic' + }, + Q: { + cb: function () { tts.showAllPortTraffic(); }, + tt: 'Monitor all traffic using port stats', + gid: 'allTraffic' + }, + F: { + cb: function () { tts.showDeviceLinkFlows(); }, + tt: 'Show device link flows', + gid: 'flows' + }, + V: { + cb: function () { tts.showRelatedIntents(); }, + tt: 'Show all related intents', + gid: 'relatedIntents' + }, + leftArrow: { + cb: function () { tts.showPrevIntent(); }, + tt: 'Show previous related intent', + gid: 'prevIntent' + }, + rightArrow: { + cb: function () { tts.showNextIntent(); }, + tt: 'Show next related intent', + gid: 'nextIntent' + }, + W: { + cb: function () { tts.showSelectedIntentTraffic(); }, + tt: 'Monitor traffic of selected intent', + gid: 'intentTraffic' + }, + + _keyOrder: [ + '0', 'A', 'Q', 'F', 'V', 'leftArrow', 'rightArrow', 'W' + ] + }, + + hooks: { + // hook for handling escape key + escape: function () { + // Must return true to consume ESC, false otherwise. + return tts.cancelTraffic(true); + }, + + // hooks for when the selection changes... + empty: function () { + tts.cancelTraffic(); + }, + single: function (data) { + tts.requestTrafficForMode(); + }, + multi: function (selectOrder) { + tts.requestTrafficForMode(); + tov.addDetailButton('showRelatedTraffic'); + }, + + // mouse hooks + mouseover: function (m) { + // m has id, class, and type properties + tts.requestTrafficForMode(true); + }, + mouseout: function () { + tts.requestTrafficForMode(true); + } + } + }; + + // invoke code to register with the overlay service + angular.module('ovTopo') + .run(['$log', 'TopoOverlayService', 'TopoTrafficService', + + function (_$log_, _tov_, _tts_) { + $log = _$log_; + tov = _tov_; + tts = _tts_; + tov.register(overlay); + }]); + +}()); diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.css b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.css new file mode 100644 index 00000000..da7d06da --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.css @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Link View -- CSS file + */ + +#ov-tunnel h2 { + display: inline-block; +} + +#ov-tunnel div.ctrl-btns { + width: 45px; +}
\ No newline at end of file diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.html b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.html new file mode 100644 index 00000000..4909cc0e --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.html @@ -0,0 +1,72 @@ +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Tunnel partial HTML --> +<div id="ov-tunnel"> + <div class="tabular-header"> + <h2>Tunnels ({{tableData.length}} total)</h2> + <div class="ctrl-btns"> + <div class="refresh" ng-class="{active: autoRefresh}" + icon icon-size="36" icon-id="refresh" + tooltip tt-msg="autoRefreshTip" + ng-click="toggleRefresh()"></div> + </div> + </div> + + <div class="summary-list" onos-table-resize> + <div ng-show="loading" class="loading-wheel" + icon icon-id="loading" icon-size="75"></div> + + <div class="table-header" onos-sortable-header> + <table> + <tr> + <td colId="id" sortable>ID </td> + <td colId="name" sortable>Name</td> + <td colId="one" sortable>Port 1 </td> + <td colId="two" sortable>Port 2 </td> + <td colId="type" sortable>Type </td> + <td colId="group_id" sortable>Group Id</td> + <td colId="bandwidth" sortable>Bandwidth</td> + <td colId="path" sortable>Path</td> + </tr> + </table> + </div> + + <div class="table-body"> + <table onos-flash-changes id-prop="one"> + <tr ng-if="!tableData.length" class="no-data"> + <td colspan="6"> + No tunnels found + </td> + </tr> + + <tr ng-repeat="tunnel in tableData track by $index" + ng-repeat-complete row-id="{{tunnel.id}}"> + <td>{{tunnel.id}}</td> + <td>{{tunnel.name}}</td> + <td>{{tunnel.one}}</td> + <td>{{tunnel.two}}</td> + <td>{{tunnel.type}}</td> + <td>{{tunnel.group_id}}</td> + <td>{{tunnel.bandwidth}}</td> + <td>{{tunnel.path}}</td> + </tr> + </table> + </div> + + </div> + +</div> diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.js new file mode 100644 index 00000000..5cde0457 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.js @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ONOS GUI -- Host View Module + */ + +(function () { + 'use strict'; + + angular.module('ovTunnel', []) + .controller('OvTunnelCtrl', + ['$log', '$scope', '$sce', 'FnService', 'TableBuilderService', + + function ($log, $scope, $sce, fs, tbs) { + tbs.buildTable({ + scope: $scope, + tag: 'tunnel' + }); + + $scope.$watch('tableData', function () { + if (!fs.isEmptyObject($scope.tableData)) { + $scope.tableData.forEach(function (tunnel) { + //tunnel.direction = $sce.trustAsHtml(tunnel.direction); + }); + } + }); + + $log.log('OvTunnelCtrl has been created'); + }]); +}()); |