ONOS-2876 -- GUI - Setting friendly device names.

Change-Id: I3e84f473af4c26c9a5e8b5cf9598d1448c4be20c
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java
index 3057981..53b16a6 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java
@@ -33,6 +33,8 @@
 import org.onosproject.ui.UiMessageHandler;
 import org.onosproject.ui.table.TableModel;
 import org.onosproject.ui.table.TableRequestHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -61,6 +63,8 @@
     private static final String DEV_NAME_CHANGE_REQ = "deviceNameChangeRequest";
     private static final String DEV_NAME_CHANGE_RESP = "deviceNameChangeResponse";
 
+    private static final String ZERO_URI = "of:0000000000000000";
+
     private static final String ID = "id";
     private static final String TYPE = "type";
     private static final String AVAILABLE = "available";
@@ -80,17 +84,21 @@
     private static final String ENABLED = "enabled";
     private static final String SPEED = "speed";
     private static final String NAME = "name";
+    private static final String WARN = "warn";
 
 
     private static final String[] COL_IDS = {
-            AVAILABLE, AVAILABLE_IID, TYPE_IID, ID,
-            NUM_PORTS, MASTER_ID, MFR, HW, SW,
+            AVAILABLE, AVAILABLE_IID, TYPE_IID,
+            NAME, ID, MASTER_ID, NUM_PORTS, MFR, HW, SW,
             PROTOCOL, CHASSIS_ID, SERIAL
     };
 
     private static final String ICON_ID_ONLINE = "active";
     private static final String ICON_ID_OFFLINE = "inactive";
 
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+
     @Override
     protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
@@ -100,6 +108,17 @@
         );
     }
 
+    // Get friendly name of the device from the annotations
+    private static String deviceName(Device device) {
+        String name = device.annotations().value(AnnotationKeys.NAME);
+        return isNullOrEmpty(name) ? device.id().toString() : name;
+    }
+
+    private static String deviceProtocol(Device device) {
+        String protocol = device.annotations().value(PROTOCOL);
+        return protocol != null ? protocol : "";
+    }
+
     private static String getTypeIconId(Device d) {
         return DEV_ICON_PREFIX + d.type().toString();
     }
@@ -130,16 +149,15 @@
             boolean available = ds.isAvailable(id);
             String iconId = available ? ICON_ID_ONLINE : ICON_ID_OFFLINE;
 
-            String protocol = dev.annotations().value(PROTOCOL);
-
             row.cell(ID, id)
+                .cell(NAME, deviceName(dev))
                 .cell(AVAILABLE, available)
                 .cell(AVAILABLE_IID, iconId)
                 .cell(TYPE_IID, getTypeIconId(dev))
                 .cell(MFR, dev.manufacturer())
                 .cell(HW, dev.hwVersion())
                 .cell(SW, dev.swVersion())
-                .cell(PROTOCOL, protocol != null ? protocol : "")
+                .cell(PROTOCOL, deviceProtocol(dev))
                 .cell(NUM_PORTS, ds.getPorts(id).size())
                 .cell(MASTER_ID, ms.getMasterFor(id));
         }
@@ -153,20 +171,16 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            String id = string(payload, "id", "of:0000000000000000");
+            String id = string(payload, ID, ZERO_URI);
 
             DeviceId deviceId = deviceId(id);
             DeviceService service = get(DeviceService.class);
             MastershipService ms = get(MastershipService.class);
             Device device = service.getDevice(deviceId);
-            ObjectNode data = MAPPER.createObjectNode();
+            ObjectNode data = objectNode();
 
             data.put(ID, deviceId.toString());
-
-            // Get friendly name of the device from the annotations
-            String name = device.annotations().value(AnnotationKeys.NAME);
-            data.put(NAME, isNullOrEmpty(name) ? deviceId.toString() : name);
-
+            data.put(NAME, deviceName(device));
             data.put(TYPE, capitalizeFully(device.type().toString()));
             data.put(TYPE_IID, getTypeIconId(device));
             data.put(MFR, device.manufacturer());
@@ -175,9 +189,9 @@
             data.put(SERIAL, device.serialNumber());
             data.put(CHASSIS_ID, device.chassisId().toString());
             data.put(MASTER_ID, ms.getMasterFor(deviceId).toString());
-            data.put(PROTOCOL, device.annotations().value(PROTOCOL));
+            data.put(PROTOCOL, deviceProtocol(device));
 
-            ArrayNode ports = MAPPER.createArrayNode();
+            ArrayNode ports = arrayNode();
 
             List<Port> portList = new ArrayList<>(service.getPorts(deviceId));
             Collections.sort(portList, (p1, p2) -> {
@@ -190,13 +204,13 @@
             }
             data.set(PORTS, ports);
 
-            ObjectNode rootNode = MAPPER.createObjectNode();
+            ObjectNode rootNode = objectNode();
             rootNode.set(DETAILS, data);
             sendMessage(DEV_DETAILS_RESP, 0, rootNode);
         }
 
         private ObjectNode portData(Port p, DeviceId id) {
-            ObjectNode port = MAPPER.createObjectNode();
+            ObjectNode port = objectNode();
             LinkService ls = get(LinkService.class);
             String name = p.annotations().value(AnnotationKeys.PORT_NAME);
 
@@ -221,6 +235,7 @@
         }
     }
 
+
     // handler for changing device friendly name
     private final class NameChangeHandler extends RequestHandler {
         private NameChangeHandler() {
@@ -229,13 +244,17 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            DeviceId deviceId = deviceId(string(payload, "id", "of:0000000000000000"));
-            NetworkConfigService service = get(NetworkConfigService.class);
-            BasicDeviceConfig cfg = service.getConfig(deviceId, BasicDeviceConfig.class);
+            DeviceId deviceId = deviceId(string(payload, ID, ZERO_URI));
+            String name = emptyToNull(string(payload, NAME, null));
+            log.debug("Name change request: {} -- '{}'", deviceId, name);
 
-            // Name attribute missing (or being empty) from the payload means
-            // that the friendly name should be unset.
-            cfg.name(emptyToNull(string(payload, "name", null)));
+            NetworkConfigService service = get(NetworkConfigService.class);
+            BasicDeviceConfig cfg =
+                    service.addConfig(deviceId, BasicDeviceConfig.class);
+
+            // Name attribute missing from the payload (or empty string)
+            // means that the friendly name should be unset.
+            cfg.name(name);
             cfg.apply();
             sendMessage(DEV_NAME_CHANGE_RESP, 0, payload);
         }
diff --git a/web/gui/src/main/webapp/app/fw/util/keys.js b/web/gui/src/main/webapp/app/fw/util/keys.js
index 10a4e9a..5ff4f7f 100644
--- a/web/gui/src/main/webapp/app/fw/util/keys.js
+++ b/web/gui/src/main/webapp/app/fw/util/keys.js
@@ -25,6 +25,7 @@
 
     // internal state
     var enabled = true,
+        globalEnabled = true,
         keyHandler = {
             globalKeys: {},
             maskedKeys: {},
@@ -116,6 +117,9 @@
     }
 
     function quickHelp(view, key, code, ev) {
+        if (!globalEnabled) {
+            return false;
+        }
         qhs.showQuickHelp(keyHandler);
         return true;
     }
@@ -126,6 +130,9 @@
     }
 
     function toggleTheme(view, key, code, ev) {
+        if (!globalEnabled) {
+            return false;
+        }
         ts.toggleTheme();
         return true;
     }
@@ -226,6 +233,9 @@
                 enableKeys: function (b) {
                     enabled = b;
                 },
+                enableGlobalKeys: function (b) {
+                    globalEnabled = b;
+                },
                 checkNotGlobal: checkNotGlobal
             };
     }]);
diff --git a/web/gui/src/main/webapp/app/view/device/device.css b/web/gui/src/main/webapp/app/view/device/device.css
index a2ff2a1..e0e9cf5 100644
--- a/web/gui/src/main/webapp/app/view/device/device.css
+++ b/web/gui/src/main/webapp/app/view/device/device.css
@@ -75,30 +75,15 @@
     margin: 8px 0;
 }
 
-#device-details-panel .name-div {
-    height: 20px;
-    padding: 8px 0 0 8px;
-}
-
-#device-details-panel .name-div span {
-    display: inline-block;
-}
-
-#device-details-panel .name-div .label {
-    font-style: italic;
-    padding-right: 12px;
-    /* works for both light and dark themes ... */
-    color: #777;
-}
-
-#device-details-panel .name-div .value {
-}
-
 #device-details-panel .editable {
     cursor: pointer;
     border-bottom: 1px dashed darkgreen;
 }
 
+#device-details-panel h2 input {
+    font-size: 1.0em;
+}
+
 #device-details-panel .top div.left {
     float: left;
     padding: 0 18px 0 0;
diff --git a/web/gui/src/main/webapp/app/view/device/device.html b/web/gui/src/main/webapp/app/view/device/device.html
index 5d51d1d..63a04db 100644
--- a/web/gui/src/main/webapp/app/view/device/device.html
+++ b/web/gui/src/main/webapp/app/view/device/device.html
@@ -35,13 +35,14 @@
                 <tr>
                     <td colId="available" class="table-icon" sortable></td>
                     <td colId="type" class="table-icon" sortable></td>
+                    <td colId="name" sortable>Friendly Name </td>
                     <td colId="id" sortable>Device ID </td>
                     <td colId="masterid" sortable>Master Instance </td>
-                    <td colId="num_ports" sortable>Ports </td>
+                    <td colId="num_ports" col-width="60px" 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>
+                    <td colId="protocol" col-width="80px" sortable>Protocol </td>
                 </tr>
             </table>
         </div>
@@ -64,6 +65,7 @@
                     <td class="table-icon">
                         <div icon icon-id="{{dev._iconid_type}}"></div>
                     </td>
+                    <td>{{dev.name}}</td>
                     <td>{{dev.id}}</td>
                     <td>{{dev.masterid}}</td>
                     <td>{{dev.num_ports}}</td>
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 76c0d03..5b7120f 100644
--- a/web/gui/src/main/webapp/app/view/device/device.js
+++ b/web/gui/src/main/webapp/app/view/device/device.js
@@ -22,7 +22,7 @@
     'use strict';
 
     // injected refs
-    var $log, $scope, $location, fs, mast, ps, wss, is, ns;
+    var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks;
 
     // internal state
     var detailsPanel,
@@ -43,13 +43,15 @@
         pName = 'device-details-panel',
         detailsReq = 'deviceDetailsRequest',
         detailsResp = 'deviceDetailsResponse',
+        nameChangeReq = 'deviceNameChangeRequest',
+        nameChangeResp = 'deviceNameChangeResponse',
 
         propOrder = [
-            'type', 'masterid', 'chassisid',
+            'id', 'type', 'masterid', 'chassisid',
             'mfr', 'hw', 'sw', 'protocol', 'serial'
         ],
         friendlyProps = [
-            'Type', 'Master ID', 'Chassis ID',
+            'URI', 'Type', 'Master ID', 'Chassis ID',
             'Vendor', 'H/W Version', 'S/W Version', 'Protocol', 'Serial #'
         ],
         portCols = [
@@ -74,57 +76,52 @@
         div.on('click', closePanel);
     }
 
-    function getNameSpan() {
-        return top.select('.name-div').select('.value');
-    }
-
-    function nameValid(name) {
-        // TODO: guard against empty strings etc.
-        return true;
+    function exitEditMode(nameH2, name) {
+        nameH2.html(name);
+        nameH2.classed('editable', true);
+        editingName = false;
+        ks.enableGlobalKeys(true);
     }
 
     function editNameSave() {
-        var span = getNameSpan(),
+        var nameH2 = top.select('h2'),
+            id = $scope.panelData.id,
+            val,
             newVal;
+
         if (editingName) {
-            newVal = span.select('input').property('value');
+            val = nameH2.select('input').property('value').trim();
+            newVal = val || id;
 
-            $log.debug("TODO: Saving name change... to '" + newVal + "'");
-            if (!nameValid(newVal)) {
-                return editNameCancel();
-            }
-
-            // TODO: send event to server to set friendly name for device
+            exitEditMode(nameH2, newVal);
             $scope.panelData.name = newVal;
-
-            span.html(newVal);
-            span.classed('editable', true);
-            editingName = false;
+            wss.sendEvent(nameChangeReq, { id: id, name: val });
         }
     }
 
     function editNameCancel() {
-        var span = getNameSpan();
         if (editingName) {
-            $log.debug("Canceling name change...");
-            span.html($scope.panelData.name);
-            span.classed('editable', true);
-            editingName = false;
+            exitEditMode(top.select('h2'), $scope.panelData.name);
             return true;
         }
         return false;
     }
 
     function editName() {
-        $log.log('editName() .. editing=' + editingName);
-        var span = getNameSpan();
+        var nameH2 = top.select('h2'),
+            tf, el;
+
         if (!editingName) {
-            editingName = true;
-            span.classed('editable', false);
-            span.html('');
-            span.append('input').classed('name-input', true)
+            nameH2.classed('editable', false);
+            nameH2.html('');
+            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);
         }
     }
 
@@ -132,14 +129,6 @@
         return editNameCancel() || closePanel();
     }
 
-    function setUpEditableName(top) {
-        var div = top.append('div').classed('name-div', true);
-
-        div.append('span').classed('label', true);
-        div.append('span').classed('value editable', true)
-            .on('click', editName);
-    }
-
     function setUpPanel() {
         var container, closeBtn, tblDiv;
         detailsPanel.empty();
@@ -150,9 +139,7 @@
         closeBtn = top.append('div').classed('close-btn', true);
         addCloseBtn(closeBtn);
         iconDiv = top.append('div').classed('dev-icon', true);
-        top.append('h2');
-
-        setUpEditableName(top);
+        top.append('h2').classed('editable', true).on('click', editName);
 
         tblDiv = top.append('div').classed('top-tables', true);
         tblDiv.append('div').classed('left', true).append('table');
@@ -184,11 +171,11 @@
                         .append('tbody');
 
         is.loadEmbeddedIcon(iconDiv, details._iconid_type, 40);
-        top.select('h2').html(details.id);
+        top.select('h2').html(details.name);
 
         propOrder.forEach(function (prop, i) {
             // properties are split into two tables
-            addProp(i < 3 ? leftTbl : rightTbl, i, details[prop]);
+            addProp(i < 4 ? leftTbl : rightTbl, i, details[prop]);
         });
     }
 
@@ -258,6 +245,13 @@
         $scope.$apply();
     }
 
+    function respNameCb(data) {
+        if (data.warn) {
+            $log.warn(data.warn, data.id);
+            top.select('h2').html(data.id);
+        }
+    }
+
     function createDetailsPane() {
         detailsPanel = ps.createPanel(pName, {
             width: wSize.width,
@@ -276,21 +270,26 @@
     .controller('OvDeviceCtrl',
         ['$log', '$scope', '$location', 'TableBuilderService', 'FnService',
             'MastService', 'PanelService', 'WebSocketService', 'IconService',
-            'NavService',
+            'NavService', 'KeyService',
 
         function (_$log_, _$scope_, _$location_,
-                  tbs, _fs_, _mast_, _ps_, _wss_, _is_, _ns_) {
+                  tbs, _fs_, _mast_, _ps_, _wss_, _is_, _ns_, _ks_) {
+            var params,
+                handlers = {};
+
             $log = _$log_;
             $scope = _$scope_;
-            $location = _$location_;
+            $loc = _$location_;
             fs = _fs_;
             mast = _mast_;
             ps = _ps_;
             wss = _wss_;
             is = _is_;
             ns = _ns_;
-            var params = $location.search(),
-                handlers = {};
+            ks = _ks_;
+
+            params = $loc.search();
+
             $scope.panelData = {};
             $scope.flowTip = 'Show flow view for selected device';
             $scope.portTip = 'Show port view for selected device';
@@ -298,6 +297,7 @@
 
             // details panel handlers
             handlers[detailsResp] = respDetailsCb;
+            handlers[nameChangeResp] = respNameCb;
             wss.bindHandlers(handlers);
 
             // query for if a certain device needs to be highlighted