diff --git a/web/gui/src/main/webapp/app/fw/layer/details-panel.css b/web/gui/src/main/webapp/app/fw/layer/details-panel.css new file mode 100644 index 0000000000..1c4183508c --- /dev/null +++ b/web/gui/src/main/webapp/app/fw/layer/details-panel.css @@ -0,0 +1,16 @@ +/* + * Copyright 2017-present Open Networking Foundation + * + * 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. + */ + diff --git a/web/gui/src/main/webapp/app/fw/layer/details-panel.js b/web/gui/src/main/webapp/app/fw/layer/details-panel.js new file mode 100644 index 0000000000..cc72a06dcf --- /dev/null +++ b/web/gui/src/main/webapp/app/fw/layer/details-panel.js @@ -0,0 +1,178 @@ +/* + * Copyright 2017-present Open Networking Foundation + * + * 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. + */ + +(function () { + + var ps, fs, mast, wss, is, EditableTextComponent; + + var panel, + pStartY, + wSize, + wssHandlers = {}, + options; + + // Constants + var topPdg = 28, + defaultLabelWidth = 110, + defaultValueWidth = 80; + + // Elements + var container, + top, + bottom; + + function createDetailsPanel(name, _options) { + options = _options; + scope = options.scope; + + panel = ps.createPanel(name, options); + + calculatePositions(); + + panel.el().style({ + position: 'absolute', + top: pStartY + 'px', + }); + + hide(); + + return panel; + } + + function calculatePositions() { + pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height') + + mast.mastHeight() + topPdg; + wSize = fs.windowSize(pStartY); + pHeight = wSize.height; + } + + function hide() { + panel.hide(); + } + + function setResponse(name, callback) { + var additionalHandler = {}; + additionalHandler[name] = callback; + + wss.bindHandlers(additionalHandler); + wssHandlers = _.extend({}, wssHandlers, additionalHandler); + } + + function addContainers() { + container = panel.append('div').classed('container', true); + top = container.append('div').classed('top', true); + bottom = container.append('div').classed('bottom', true); + } + + function addCloseButton(onClose) { + var closeBtn = top.append('div').classed('close-btn', true); + + is.loadEmbeddedIcon(closeBtn, 'close', 20); + closeBtn.on('click', onClose || function () {}); + } + + function addHeading(icon) { + top.append('div').classed('iconDiv ' + icon, true); + new EditableTextComponent(top.append('h2'), { + scope: options.scope, + nameChangeRequest: options.nameChangeRequest, + keyBindings: options.keyBindings, + }); + } + + function addTable(parent, className) { + return parent.append('div').classed(className, true).append('table'); + } + + function addProp(tbody, key, value) { + console.log(tbody); + var tr = tbody.append('tr'); + + function addCell(cls, txt, width) { + tr.append('td').attr('class', cls).attr('width', width).text(txt); + } + + addCell('label', key + ' :', defaultLabelWidth); + addCell('value', value, defaultValueWidth); + } + + function addPropsList(el, props) { + var tblDiv = el.append('div').classed('top-tables', true); + var left = addTable(tblDiv, 'left').append('tbody'); + var right = addTable(tblDiv, 'right').append('tbody'); + + var keys = _.keys(props); + + _.each(props, function (value, key) { + var index = keys.indexOf(key); + + if (index < keys.length / 2) { + addProp(left, key, value); + } else { + addProp(right, key, value); + } + }); + } + + function empty() { + panel.empty(); + } + + function select(id) { + return panel.el().select(id); + } + + function destroy() { + wss.unbindHandlers(wssHandlers); + } + + angular.module('onosLayer') + .factory('DetailsPanelService', [ + + 'PanelService', 'FnService', 'MastService', 'WebSocketService', + 'IconService', 'EditableTextComponent', + + function (_ps_, _fs_, _mast_, _wss_, _is_, _etc_) { + + ps = _ps_; + fs = _fs_; + mast = _mast_; + wss = _wss_; + is = _is_; + EditableTextComponent = _etc_; + + return { + create: createDetailsPanel, + setResponse: setResponse, + + addContainers: addContainers, + addCloseButton: addCloseButton, + addHeading: addHeading, + addPropsList: addPropsList, + + // Elements + container: function () { return container; }, + top: function () { return top; }, + bottom: function () { return bottom; }, + select: select, + + empty: empty, + hide: hide, + destroy: destroy, + }; + }, + ]); +})(); diff --git a/web/gui/src/main/webapp/app/fw/layer/editable-text.js b/web/gui/src/main/webapp/app/fw/layer/editable-text.js new file mode 100644 index 0000000000..09da1ec2cc --- /dev/null +++ b/web/gui/src/main/webapp/app/fw/layer/editable-text.js @@ -0,0 +1,119 @@ +/* + * Copyright 2017-present Open Networking Foundation + * + * 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. + */ + +(function () { + + var ks, wss; + + var EditableText = function (el, options) { + // constructor + this.el = el; + this.scope = options.scope; + this.options = options; + + this.el.classed('editable clickable', true).on('click', this.onEdit.bind(this)); + this.editingName = false; + }; + + EditableText.prototype = { + + bindHandlers: function () { + ks.keyBindings({ + 'enter': this.save.bind(this), + 'esc': [this.cancel.bind(this), 'Close the details panel'] + }); + }, + + unbindHandlers: function () { + ks.unbindKeys(); + + if (this.options.keyBindings) { + // Reset to original bindings before editable text + ks.keyBindings(this.options.keyBindings); + } + }, + + addTextField: function () { + return this.el.append('input').classed('name-input', true) + .attr('type', 'text') + .attr('value', this.scope.panelData.name)[0][0]; + }, + + onEdit: function () { + if (!this.editingName) { + this.el.classed('editable clickable', false); + this.el.text(''); + + var el = this.addTextField(); + el.focus(); + el.select(); + this.editingName = true; + + this.bindHandlers(); + + ks.enableGlobalKeys(false); + } + }, + + exit: function (name) { + this.el.text(name); + this.el.classed('editable clickable', true); + this.editingName = false; + ks.enableGlobalKeys(true); + this.unbindHandlers(); + }, + + cancel: function (a, b, ev) { + + if (this.editingName) { + this.exit(this.scope.panelData.name); + return true; + } + + return false; + }, + + save: function () { + var id = this.scope.panelData.id, + val, + newVal; + + if (this.editingName) { + val = this.el.select('input').property('value').trim(); + newVal = val || id; + + this.exit(newVal); + this.scope.panelData.name = newVal; + wss.sendEvent(this.options.nameChangeRequest, { id: id, name: val }); + } + }, + }; + + + angular.module('onosLayer') + .factory('EditableTextComponent', [ + + 'KeyService', 'WebSocketService', + + function (_ks_, _wss_) { + ks = _ks_; + wss = _wss_; + + return EditableText; + }, + ]); + +})(); diff --git a/web/gui/src/main/webapp/app/view/device/device.js b/web/gui/src/main/webapp/app/view/device/device.js index 4fbb0df37c..6bc66d8c28 100644 --- a/web/gui/src/main/webapp/app/view/device/device.js +++ b/web/gui/src/main/webapp/app/view/device/device.js @@ -22,17 +22,14 @@ 'use strict'; // injected refs - var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks; + var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks, dps; // internal state var detailsPanel, pStartY, pHeight, top, - bottom, - iconDiv, wSize, - editingName = false, device; // constants @@ -40,19 +37,12 @@ ctnrPdg = 24, scrollSize = 17, portsTblPdg = 50, - defaultLabelWidth = 110, - defaultValueWidth = 80, pName = 'device-details-panel', detailsReq = 'deviceDetailsRequest', detailsResp = 'deviceDetailsResponse', nameChangeReq = 'deviceNameChangeRequest', nameChangeResp = 'deviceNameChangeResponse', - friendlyProps = [ - 'URI', 'Type', 'Master ID', 'Chassis ID', - 'Vendor', 'H/W Version', 'S/W Version', 'Protocol', 'Serial #', - 'Pipeconf', - ], portCols = [ 'enabled', 'id', 'speed', 'type', 'elinks_dest', 'name', ], @@ -60,6 +50,11 @@ 'Enabled', 'ID', 'Speed', 'Type', 'Egress Links', 'Name', ]; + var keyBindings = { + esc: [closePanel, 'Close the details panel'], + _helpFormat: ['esc'], + }; + function closePanel() { if (detailsPanel.isVisible()) { $scope.selId = null; @@ -69,119 +64,44 @@ return false; } - function addCloseBtn(div) { - is.loadEmbeddedIcon(div, 'close', 20); - div.on('click', closePanel); - } - - function exitEditMode(nameH2, name) { - nameH2.text(name); - nameH2.classed('editable clickable', true); - editingName = false; - ks.enableGlobalKeys(true); - } - - function editNameSave() { - var nameH2 = top.select('h2'), - id = $scope.panelData.id, - val, - newVal; - - if (editingName) { - val = nameH2.select('input').property('value').trim(); - newVal = val || id; - - exitEditMode(nameH2, newVal); - $scope.panelData.name = newVal; - wss.sendEvent(nameChangeReq, { id: id, name: val }); - } - } - - function editNameCancel() { - if (editingName) { - exitEditMode(top.select('h2'), $scope.panelData.name); - return true; - } - return false; - } - - function editName() { - var nameH2 = top.select('h2'), - tf, el; - - if (!editingName) { - nameH2.classed('editable clickable', false); - nameH2.text(''); - tf = nameH2.append('input').classed('name-input', true) - .attr('type', 'text') - .attr('value', $scope.panelData.name); - el = tf[0][0]; - el.focus(); - el.select(); - editingName = true; - ks.enableGlobalKeys(false); - } - } - - function handleEscape() { - return editNameCancel() || closePanel(); - } - function setUpPanel() { - var container, closeBtn, tblDiv; - detailsPanel.empty(); + // var container, closeBtn, tblDiv; - container = detailsPanel.append('div').classed('container', true); + dps.empty(); + dps.addContainers(); + dps.addCloseButton(closePanel); - 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').classed('editable clickable', true).on('click', editName); + var top = dps.top(); + var bottom = dps.bottom(); - tblDiv = top.append('div').classed('top-tables', true); - tblDiv.append('div').classed('left', true).append('table'); - tblDiv.append('div').classed('right', true).append('table'); + dps.addHeading('dev-icon'); + top.append('div').classed('top-content', true); top.append('hr'); - bottom = container.append('div').classed('bottom', true); bottom.append('h2').classed('ports-title', true).text('Ports'); bottom.append('table'); } - function addProp(tbody, index, value) { - var tr = tbody.append('tr'); - - function addCell(cls, txt, width) { - tr.append('td').attr('class', cls).attr('width', width).text(txt); - } - addCell('label', friendlyProps[index] + ' :', defaultLabelWidth); - addCell('value', value, defaultValueWidth); + function friendlyPropsList(details) { + return { + 'URI': device.id, + 'Type': device.type, + 'Master ID': details['masterid'], + 'Chassis ID': details['chassid'], + 'Vendor': device.mfr, + 'H/W Version': device.hw, + 'S/W Version': device.sw, + 'Protocol': details['protocol'], + 'Serial #': device.serial, + 'Pipeconf': details['pipeconf'], + }; } 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').text(details.name); - - // === demonstrate use of JsonCodec object see ONOS-5976 - addProp(leftTbl, 0, device.id); - addProp(leftTbl, 1, device.type); - addProp(leftTbl, 2, details['masterid']); - addProp(leftTbl, 3, details['chassid']); - addProp(leftTbl, 4, device.mfr); - addProp(rightTbl, 5, device.hw); - addProp(rightTbl, 6, device.sw); - addProp(rightTbl, 7, details['protocol']); - addProp(rightTbl, 8, device.serial); - addProp(rightTbl, 9, details['pipeconf']); + is.loadEmbeddedIcon(dps.select('.iconDiv'), details._iconid_type, 40); + dps.top().select('h2').text(details.name); + dps.addPropsList(tblDiv, friendlyPropsList(details)); } function addPortRow(tbody, port) { @@ -193,6 +113,7 @@ } function populateBottom(table, ports) { + var theader = table.append('thead').append('tr'), tbody = table.append('tbody'), tbWidth, tbHeight; @@ -200,16 +121,15 @@ friendlyPortCols.forEach(function (col) { theader.append('th').text(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') + - (fs.noPxStyle(detailsPanel.el().select('.top'), 'height') + + fs.noPxStyle(detailsPanel.el().select('.ports-title'), 'height') + portsTblPdg); table.style({ @@ -223,15 +143,14 @@ } function populateDetails(details) { - var topTbs, btmTbl, ports; + var btmTbl, ports; setUpPanel(); - topTbs = top.select('.top-tables'); - btmTbl = bottom.select('table'); + btmTbl = dps.bottom().select('table'); ports = details.ports; - populateTop(topTbs, details); + populateTop(dps.select('.top-content'), details); populateBottom(btmTbl, ports); detailsPanel.height(pHeight); @@ -250,18 +169,19 @@ } } - function createDetailsPane() { - detailsPanel = ps.createPanel(pName, { + function createDetailsPanel() { + detailsPanel = dps.create(pName, { width: wSize.width, margin: 0, hideMargin: 0, + scope: $scope, + keyBindings: keyBindings, + nameChangeRequest: nameChangeReq, }); - detailsPanel.el().style({ - position: 'absolute', - top: pStartY + 'px', - }); + + dps.setResponse(detailsResp, respDetailsCb); + $scope.hidePanel = function () { detailsPanel.hide(); }; - detailsPanel.hide(); } // Sample functions for detail panel creation @@ -283,10 +203,10 @@ ['$log', '$scope', '$location', 'TableBuilderService', 'TableDetailService', 'FnService', 'MastService', 'PanelService', 'WebSocketService', 'IconService', - 'NavService', 'KeyService', + 'NavService', 'KeyService', 'DetailsPanelService', function (_$log_, _$scope_, _$location_, - tbs, tds, _fs_, _mast_, _ps_, _wss_, _is_, _ns_, _ks_) { + tbs, tds, _fs_, _mast_, _ps_, _wss_, _is_, _ns_, _ks_, _dps_) { var params, handlers = {}; @@ -300,6 +220,7 @@ is = _is_; ns = _ns_; ks = _ks_; + dps = _dps_; params = $loc.search(); @@ -310,7 +231,7 @@ $scope.meterTip = 'Show meter view for selected device'; // details panel handlers - handlers[detailsResp] = respDetailsCb; + // handlers[detailsResp] = respDetailsCb; handlers[nameChangeResp] = respNameCb; wss.bindHandlers(handlers); @@ -352,6 +273,7 @@ }; $scope.$on('$destroy', function () { + dps.destroy(); wss.unbindHandlers(handlers); }); @@ -373,7 +295,7 @@ function initPanel() { heightCalc(); - createDetailsPane(); + createDetailsPanel(); } // Safari has a bug where it renders the fixed-layout table wrong @@ -384,11 +306,8 @@ initPanel(); } // create key bindings to handle panel - ks.keyBindings({ - enter: editNameSave, - esc: [handleEscape, 'Close the details panel'], - _helpFormat: ['esc'], - }); + ks.keyBindings(keyBindings); + ks.gestureNotes([ ['click', 'Select a row to show device details'], ['scroll down', 'See more devices'], diff --git a/web/gui/src/main/webapp/app/view/host/host.js b/web/gui/src/main/webapp/app/view/host/host.js index b5bf018296..37668bbb8f 100644 --- a/web/gui/src/main/webapp/app/view/host/host.js +++ b/web/gui/src/main/webapp/app/view/host/host.js @@ -22,16 +22,14 @@ 'use strict'; // injected refs - var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks; + var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks, dps; // internal state var detailsPanel, pStartY, pHeight, top, - iconDiv, - wSize, - editingName = false; + wSize; // constants var topPdg = 28, @@ -41,13 +39,10 @@ nameChangeReq = 'hostNameChangeRequest', nameChangeResp = 'hostNameChangeResponse'; - var propOrder = [ - 'id', 'ip', 'mac', 'vlan', 'configured', 'location', - ], - friendlyProps = [ - 'Host ID', 'IP Address', 'MAC Address', 'VLAN', - 'Configured', 'Location', - ]; + var keyBindings = { + esc: [closePanel, 'Close the details panel'], + _helpFormat: ['esc'], + }; function closePanel() { if (detailsPanel.isVisible()) { @@ -58,105 +53,41 @@ return false; } - function addCloseBtn(div) { - is.loadEmbeddedIcon(div, 'close', 20); - div.on('click', closePanel); - } - - function exitEditMode(nameH2, name) { - nameH2.text(name); - nameH2.classed('editable clickable', true); - editingName = false; - ks.enableGlobalKeys(true); - } - - function editNameSave() { - var nameH2 = top.select('h2'), - id = $scope.panelData.id, - ip = $scope.panelData.ip, - val, - newVal; - - if (editingName) { - val = nameH2.select('input').property('value').trim(); - newVal = val || ip; - - exitEditMode(nameH2, newVal); - $scope.panelData.name = newVal; - wss.sendEvent(nameChangeReq, { id: id, name: val }); - } - } - - function editNameCancel() { - if (editingName) { - exitEditMode(top.select('h2'), $scope.panelData.name); - return true; - } - return false; - } - - function editName() { - var nameH2 = top.select('h2'), - tf, el; - - if (!editingName) { - nameH2.classed('editable clickable', false); - nameH2.text(''); - tf = nameH2.append('input').classed('name-input', true) - .attr('type', 'text') - .attr('value', $scope.panelData.name); - el = tf[0][0]; - el.focus(); - el.select(); - editingName = true; - ks.enableGlobalKeys(false); - } - } - - function handleEscape() { - return editNameCancel() || closePanel(); - } function setUpPanel() { - var container, closeBtn; - detailsPanel.empty(); - container = detailsPanel.append('div').classed('container', true); + dps.empty(); + dps.addContainers(); + dps.addCloseButton(closePanel); - top = container.append('div').classed('top', true); - closeBtn = top.append('div').classed('close-btn', true); - addCloseBtn(closeBtn); - iconDiv = top.append('div').classed('host-icon', true); - top.append('h2').classed('editable clickable', true).on('click', editName); + var top = dps.top(); + + dps.addHeading('host-icon'); + top.append('div').classed('top-content', true); - top.append('div').classed('top-tables', true); top.append('hr'); } - function addProp(tbody, index, value) { - var tr = tbody.append('tr'); - - function addCell(cls, txt) { - tr.append('td').attr('class', cls).text(txt); - } - addCell('label', friendlyProps[index] + ' :'); - addCell('value', value); + function friendlyPropsList(details) { + return { + 'Host ID': details.id, + 'IP Address': details.ip[0], + 'MAC Address': details.mac, + 'VLAN': details.vlan, + 'Configured': details.configured, + 'Location': details.location, + }; } - function populateTop(details) { - var tab = top.select('.top-tables').append('table').append('tbody'); - - is.loadEmbeddedIcon(iconDiv, details._iconid_type, 40); - top.select('h2').text(details.name); - - propOrder.forEach(function (prop, i) { - addProp(tab, i, details[prop]); - }); + function populateTop(tblDiv, details) { + is.loadEmbeddedIcon(dps.select('.iconDiv'), details._iconid_type, 40); + dps.top().select('h2').text(details.name); + dps.addPropsList(tblDiv, friendlyPropsList(details)); } function populateDetails(details) { setUpPanel(); - populateTop(details); + populateTop(dps.select('.top-content'), details); detailsPanel.height(pHeight); // configure width based on content.. for now hardcoded detailsPanel.width(400); @@ -174,18 +105,19 @@ } } - function createDetailsPane() { - detailsPanel = ps.createPanel(pName, { + function createDetailsPanel() { + detailsPanel = dps.create(pName, { width: wSize.width, margin: 0, hideMargin: 0, + scope: $scope, + keyBindings: keyBindings, + nameChangeRequest: nameChangeReq, }); - detailsPanel.el().style({ - position: 'absolute', - top: pStartY + 'px', - }); + + dps.setResponse(detailsResp, respDetailsCb); + $scope.hidePanel = function () { detailsPanel.hide(); }; - detailsPanel.hide(); } @@ -196,12 +128,12 @@ '$location', 'TableBuilderService', 'FnService', 'MastService', 'PanelService', 'WebSocketService', - 'IconService', 'NavService', 'KeyService', + 'IconService', 'NavService', 'KeyService', 'DetailsPanelService', function (_$log_, _$scope_, _$location_, tbs, _fs_, _mast_, _ps_, _wss_, - _is_, _ns_, _ks_) { + _is_, _ns_, _ks_, _dps_) { var params, handlers = {}; @@ -216,13 +148,13 @@ is = _is_; ns = _ns_; ks = _ks_; + dps = _dps_; params = $loc.search(); $scope.panelData = {}; // details panel handlers - handlers[detailsResp] = respDetailsCb; handlers[nameChangeResp] = respNameCb; wss.bindHandlers(handlers); @@ -254,6 +186,7 @@ }; $scope.$on('$destroy', function () { + dps.destroy(); wss.unbindHandlers(handlers); }); @@ -261,7 +194,7 @@ }]) .directive('hostDetailsPanel', - ['$rootScope', '$window', '$timeout', 'KeyService', + ['$rootScope', '$window', '$timeout', 'KeyService', 'DetailsPanelService', function ($rootScope, $window, $timeout, ks) { return function (scope) { var unbindWatch; @@ -275,7 +208,7 @@ function initPanel() { heightCalc(); - createDetailsPane(); + createDetailsPanel(); } // Safari has a bug where it renders the fixed-layout table wrong @@ -286,11 +219,7 @@ initPanel(); } // create key bindings to handle panel - ks.keyBindings({ - enter: editNameSave, - esc: [handleEscape, 'Close the details panel'], - _helpFormat: ['esc'], - }); + ks.keyBindings(keyBindings); ks.gestureNotes([ ['click', 'Select a row to show device details'], ['scroll down', 'See more devices'],