GUI2 Handle node additions and removals in Topology view

Change-Id: Ic16fc1325fe338e2136f1cc70febc621342be4f2
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java
index 36375eb..5ca86e3 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java
@@ -73,6 +73,16 @@
     }
 
     /**
+     * Returns the identifier of the region to which this device belongs.
+     * This will be null if the device does not belong to any region.
+     *
+     * @return region ID
+     */
+    public RegionId regionId() {
+        return regionId;
+    }
+
+    /**
      * Sets the ID of the region to which this device belongs.
      *
      * @param regionId region identifier
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
index 74c8677..36629f2 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
@@ -40,6 +40,7 @@
 
     private static final String NULL_NAME = "(root)";
     private static final String NO_NAME = "???";
+    private static final String MEMO_ADDED = "added";
 
     /**
      * The identifier for the null-region. That is, a container for devices,
@@ -327,10 +328,24 @@
                 return true;
 
             case REGION_ADDED_OR_UPDATED:
+                if (MEMO_ADDED.equalsIgnoreCase(event.memo()) &&
+                        regionId.toString().equalsIgnoreCase(
+                          ((UiRegion) event.subject()).backingRegion().toString())) {
+                    return true;
+                } else {
+                    return isDeviceRelevant(((UiDevice) event.subject()).id());
+                }
             case REGION_REMOVED:
                 return isRegionRelevant(((UiRegion) event.subject()).id());
 
             case DEVICE_ADDED_OR_UPDATED:
+                if (MEMO_ADDED.equalsIgnoreCase(event.memo()) &&
+                        regionId.toString().equalsIgnoreCase(
+                          ((UiDevice) event.subject()).regionId().toString())) {
+                    return true;
+                } else {
+                    return isDeviceRelevant(((UiDevice) event.subject()).id());
+                }
             case DEVICE_REMOVED:
                 return isDeviceRelevant(((UiDevice) event.subject()).id());
 
@@ -339,6 +354,13 @@
                 return isLinkRelevant((UiLink) event.subject());
 
             case HOST_ADDED_OR_UPDATED:
+                if (MEMO_ADDED.equalsIgnoreCase(event.memo()) &&
+                        regionId.toString().equalsIgnoreCase(
+                          ((UiHost) event.subject()).regionId().toString())) {
+                    return true;
+                } else {
+                    return isDeviceRelevant(((UiDevice) event.subject()).id());
+                }
             case HOST_MOVED:
             case HOST_REMOVED:
                 return isDeviceRelevant(((UiHost) event.subject()).locationDevice());
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
index b673b24..58606e9 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
@@ -89,7 +89,7 @@
         <svg:g onos-hostnodesvg [host]="host"
                *ngFor="let host of regionData.hosts[visibleLayerIdx()]"
                onosDraggableNode [draggableNode]="host" [draggableInGraph]="graph"
-                   (newLocation)="nodeMoved('host', device.id, $event)"
+                   (newLocation)="nodeMoved('host', host.id, $event)"
                (selectedEvent)="updateSelected($event)"
                [labelToggle]="hostLabelToggle"
                [scale]="scale">
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
index ebc70aa..48d973b 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
@@ -57,10 +57,7 @@
     HostNodeSvgComponent,
     LinkSvgComponent
 } from './visuals';
-import {
-    BackgroundSvgComponent,
-    LocationType
-} from '../backgroundsvg/backgroundsvg.component';
+import {LocationType} from '../backgroundsvg/backgroundsvg.component';
 
 interface UpdateMeta {
     id: string;
@@ -130,6 +127,28 @@
         }
     }
 
+    /**
+     * Recursive method to compare 2 objects attribute by attribute and update
+     * the first where a change is detected
+     * @param existingNode 1st object
+     * @param updatedNode 2nd object
+     */
+    private static updateObject(existingNode: Object, updatedNode: Object): number {
+        let changed: number = 0;
+        for (const key of Object.keys(updatedNode)) {
+            const o = updatedNode[key];
+            if (key === 'id') {
+                continue;
+            } else if (o && typeof o === 'object' && o.constructor === Object) {
+                changed += ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
+            } else if (existingNode[key] !== updatedNode[key]) {
+                changed++;
+                existingNode[key] = updatedNode[key];
+            }
+        }
+        return changed;
+    }
+
     @HostListener('window:resize', ['$event'])
     onResize(event) {
         this.graph.initSimulation(this.options);
@@ -310,25 +329,32 @@
             case ModelEventType.DEVICE_ADDED_OR_UPDATED:
                 if (memo === ModelEventMemo.ADDED) {
                     this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
+                    this.graph.nodes.push(<Device>data);
+                    this.log.debug('Device added', (<Device>data).id);
                 } else if (memo === ModelEventMemo.UPDATED) {
                     const oldDevice: Device =
                         this.regionData.devices[this.visibleLayerIdx()]
                             .find((d) => d.id === subject);
-                    this.compareDevice(oldDevice, <Device>data);
+                    const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
+                    if (changes > 0) {
+                        this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
+                    }
                 } else {
                     this.log.warn('Device ', memo, ' - not yet implemented', data);
                 }
-                this.log.warn('Device ', memo, ' - not yet implemented', data);
                 break;
             case ModelEventType.HOST_ADDED_OR_UPDATED:
                 if (memo === ModelEventMemo.ADDED) {
                     this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
-                    this.log.warn('Host added - not yet implemented', data);
+                    this.graph.nodes.push(<Host>data);
+                    this.log.debug('Host added', (<Host>data).id);
                 } else if (memo === ModelEventMemo.UPDATED) {
                     const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
                         .find((h) => h.id === subject);
-                    this.compareHost(oldHost, <Host>data);
-                    this.log.warn('Host updated - not yet implemented', data);
+                    const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
+                    if (changes > 0) {
+                        this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
+                    }
                 } else {
                     this.log.warn('Host change', memo, ' - unexpected');
                 }
@@ -338,9 +364,9 @@
                     const removeIdx: number =
                         this.regionData.devices[this.visibleLayerIdx()]
                             .findIndex((d) => d.id === subject);
-                    const removeCmpt: DeviceNodeSvgComponent =
-                        this.devices.find((dc) => dc.device.id === subject);
-                    this.log.warn('Device ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.device.id);
+                    this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
+                    this.removeRelatedLinks(subject);
+                    this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
                 } else {
                     this.log.warn('Device removed - unexpected memo', memo);
                 }
@@ -350,33 +376,53 @@
                     const removeIdx: number =
                         this.regionData.hosts[this.visibleLayerIdx()]
                             .findIndex((h) => h.id === subject);
-                    const removeCmpt: HostNodeSvgComponent =
-                        this.hosts.find((hc) => hc.host.id === subject);
-                    this.log.warn('Host ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.host.id);
+                    this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
+                    this.removeRelatedLinks(subject);
+                    this.log.warn('Host ', subject, 'removed');
                 } else {
                     this.log.warn('Host removed - unexpected memo', memo);
                 }
                 break;
             case ModelEventType.LINK_ADDED_OR_UPDATED:
-                this.log.warn('link added or updated - not yet implemented', subject);
+                if (memo === ModelEventMemo.ADDED &&
+                    this.regionData.links.findIndex((l) => l.id === subject) === -1) {
+                    const listLen = this.regionData.links.push(<RegionLink>data);
+                    const epA = ForceSvgComponent.extractNodeName(
+                        this.regionData.links[listLen - 1].epA);
+                    this.regionData.links[listLen - 1].source =
+                        this.graph.nodes.find((node) =>
+                            node.id === epA);
+                    const epB = ForceSvgComponent.extractNodeName(
+                        this.regionData.links[listLen - 1].epB);
+                    this.regionData.links[listLen - 1].target =
+                        this.graph.nodes.find((node) =>
+                            node.id === epB);
+                    this.log.debug('Link added', subject);
+                } else if (memo === ModelEventMemo.UPDATED) {
+                    const oldLink = this.regionData.links.find((l) => l.id === subject);
+                    const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
+                    this.log.debug('Link ', subject, '. Updated', changes, 'items');
+                } else {
+                    this.log.warn('Link added or updated - unexpected memo', memo);
+                }
                 break;
             default:
                 this.log.error('Unexpected model event', type, 'for', subject);
         }
+        this.ref.markForCheck();
+        this.graph.initSimulation(this.options);
     }
 
-    private compareDevice(oldDevice: Device, updatedDevice: Device) {
-        if (oldDevice.master !== updatedDevice.master) {
-            this.log.debug('Mastership has changed for', updatedDevice.id, 'to', updatedDevice.master);
-        }
-        if (oldDevice.online !== updatedDevice.online) {
-            this.log.debug('Status has changed for', updatedDevice.id, 'to', updatedDevice.online);
-        }
-    }
-
-    private compareHost(oldHost: Host, updatedHost: Host) {
-        if (oldHost.configured !== updatedHost.configured) {
-            this.log.debug('Configured has changed for', updatedHost.id, 'to', updatedHost.configured);
+    private removeRelatedLinks(subject: string) {
+        const len = this.regionData.links.length;
+        for (let i = 0; i < len; i++) {
+            const linkIdx = this.regionData.links.findIndex((l) =>
+                (ForceSvgComponent.extractNodeName(l.epA) === subject ||
+                    ForceSvgComponent.extractNodeName(l.epB) === subject));
+            if (linkIdx >= 0) {
+                this.regionData.links.splice(linkIdx, 1);
+                this.log.debug('Link ', linkIdx, 'removed on attempt', i);
+            }
         }
     }
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
index ce5441f..af280a4 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
@@ -111,6 +111,9 @@
     name: string;
     locType: LocationType;
     uiType: string;
+    channelId: string;
+    managementAddress: string;
+    protocol: string;
 }
 
 export interface HostProps {
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
index 8644ac1..8f3378f 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
@@ -56,7 +56,7 @@
         are driven by the d3.force engine
     -->
     <svg:svg #svgZoom xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" id="topo2"
-        preserveAspectRatio="xMaxYMax none">
+        preserveAspectRatio="xMaxYMax meet">
         <svg:desc>The main SVG canvas of the Topology View</svg:desc>
         <svg:g *ngIf="force.regionData?.devices[0].length +
                         force.regionData?.devices[1].length +
diff --git a/web/gui2/src/main/webapp/login.html b/web/gui2/src/main/webapp/login.html
index d05260f..40676fb 100644
--- a/web/gui2/src/main/webapp/login.html
+++ b/web/gui2/src/main/webapp/login.html
@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <title>ONOS Login</title>
-
+    <link rel="shortcut icon" href="data/img/onos-logo.png">
     <style type="text/css">
         img {
             margin: 24px;