Implemented Instance View of Topo in GUI2

Change-Id: If603481e729ebc19a6f91db2739f1b422cc762d0
diff --git a/web/gui2/src/main/webapp/app/onos-routing.module.ts b/web/gui2/src/main/webapp/app/onos-routing.module.ts
index 60ec9d7..962f5a3 100644
--- a/web/gui2/src/main/webapp/app/onos-routing.module.ts
+++ b/web/gui2/src/main/webapp/app/onos-routing.module.ts
@@ -79,7 +79,7 @@
         loadChildren: 'app/view/meter/meter.module#MeterModule'
     },
     {
-        path: 'topo',
+        path: 'topo2',
         loadChildren: 'app/view/topology/topology.module#TopologyModule'
     },
 /*  Comment out below section for running locally with 'ng serve' when developing */
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
index 3f78ebb..389ec9c 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
@@ -14,3 +14,9 @@
 ~ limitations under the License.
 -->
 <svg:g onos-mapsvg />
+<svg:g>
+    <svg:text>Layout: {{ layoutData.id }} {{ layoutData.bgDesc }}</svg:text>
+    <svg:text>Region: {{ layoutData.region }} {{ layoutData.regionName }}</svg:text>
+    <svg:text>Parent {{ layoutData.parent }}</svg:text>
+    <svg:text *ngFor="let crumb of layoutData.crumbs">{{ crumb.id }} {{ crumb.name }}</svg:text>
+</svg:g>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
index 6a010d1..ff6f99a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
@@ -16,6 +16,57 @@
 import { Component, OnInit } from '@angular/core';
 
 /**
+ * model of the topo2CurrentLayout attrs from BgZoom below
+ */
+export interface BgZoomAttrs {
+    offsetX: number;
+    offsetY: number;
+    scale: number;
+}
+
+/**
+ * model of the topo2CurrentLayout background zoom attrs from Layout below
+ */
+export interface BgZoom {
+    cfg: BgZoomAttrs;
+    usr?: BgZoomAttrs;
+}
+
+/**
+ * model of the topo2CurrentLayout breadcrumb from Layout below
+ */
+export interface LayoutCrumb {
+    id: string;
+    name: string;
+}
+
+/**
+ * Enum of the topo2CurrentRegion location type from Location below
+ */
+export enum LocationType {
+    GEO = 'geo',
+    GRID = 'grid'
+}
+
+/**
+ * model of the topo2CurrentLayout WebSocket response
+ */
+export interface Layout {
+    id: string;
+    bgDefaultScale: number;
+    bgDesc: string;
+    bgFilePath: string;
+    bgId: string;
+    bgType: LocationType;
+    bgWarn: string;
+    bgZoom: BgZoom;
+    crumbs: LayoutCrumb[];
+    parent: string;
+    region: string;
+    regionName: string;
+}
+
+/**
  * ONOS GUI -- Topology Background Layer View.
  */
 @Component({
@@ -25,9 +76,13 @@
 })
 export class BackgroundSvgComponent implements OnInit {
 
+    layoutData: Layout = <Layout>{};
+
     constructor() { }
 
     ngOnInit() {
     }
 
+
+
 }
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 45fa000..d159f06 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
@@ -13,7 +13,127 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import {Component, Input, OnInit} from '@angular/core';
+import { LocationType } from '../backgroundsvg/backgroundsvg.component';
+
+/**
+ * Enum of the topo2CurrentRegion node type from SubRegion below
+ */
+export enum NodeType {
+    REGION = 'region',
+    DEVICE = 'device'
+}
+
+/**
+ * Enum of the topo2CurrentRegion layerOrder from Region below
+ */
+export enum LayerOrder {
+    LAYER_OPTICAL = 'opt',
+    LAYER_PACKET = 'pkt',
+    LAYER_DEFAULT = 'def'
+}
+
+/**
+ * model of the topo2CurrentRegion location from SubRegion below
+ */
+export interface Location {
+    locType: LocationType;
+    latOrY: number;
+    longOrX: number;
+}
+
+/**
+ * model of the topo2CurrentRegion props from SubRegion below
+ */
+export interface RegionProps {
+    latitude: number;
+    longitude: number;
+    name: string;
+    peerLocations: string;
+}
+
+/**
+ * model of the topo2CurrentRegion subregion from Region below
+ */
+export interface SubRegion {
+    id: string;
+    location: Location;
+    nDevs: number;
+    nHosts: number;
+    name: string;
+    nodeType: NodeType;
+    props: RegionProps;
+}
+
+export enum LinkType {
+    UiRegionLink,
+    UiDeviceLink
+}
+
+/**
+ * model of the topo2CurrentRegion region rollup from Region below
+ */
+export interface RegionRollup {
+    id: string;
+    epA: string;
+    epB: string;
+    portA: string;
+    portB: string;
+    type: LinkType;
+}
+
+/**
+ * model of the topo2CurrentRegion region link from Region below
+ */
+export interface RegionLink {
+    id: string;
+    epA: string;
+    epB: string;
+    rollup: RegionRollup[];
+    type: LinkType;
+}
+
+/**
+ * model of the topo2CurrentRegion device props from Device below
+ */
+export interface DeviceProps {
+    latitude: number;
+    longitude: number;
+    name: string;
+    locType: LocationType;
+}
+
+export interface Device {
+    id: string;
+    layer: LayerOrder;
+    location: LocationType;
+    master: string;
+    nodeType: NodeType;
+    online: boolean;
+    props: DeviceProps;
+    type: string;
+}
+
+/**
+ * model of the topo2CurrentRegion WebSocket response
+ */
+export interface Region {
+    note?: string;
+    id: string;
+    devices: Device[][];
+    hosts: Object[];
+    links: RegionLink[];
+    layerOrder: LayerOrder[];
+    peerLocations?: Location[];
+    subregions: SubRegion[];
+}
+
+/**
+ * model of the topo2PeerRegions WebSocket response
+ */
+export interface Peer {
+    peers: SubRegion[];
+}
 
 /**
  * ONOS GUI -- Topology Forces Graph Layer View.
@@ -24,6 +144,8 @@
     styleUrls: ['./forcesvg.component.css']
 })
 export class ForceSvgComponent implements OnInit {
+    @Input() onosInstMastership: string = '';
+    regionData: Region;
 
     constructor() { }
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/layout/layout.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/layout/layout.component.ts
index 477dd15..122c14b 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/layout/layout.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/layout/layout.component.ts
@@ -16,15 +16,15 @@
 import { Component, OnInit } from '@angular/core';
 
 @Component({
-  selector: 'onos-layout',
-  templateUrl: './layout.component.html',
-  styleUrls: ['./layout.component.css']
+    selector: 'onos-layout',
+    templateUrl: './layout.component.html',
+    styleUrls: ['./layout.component.css']
 })
 export class LayoutComponent implements OnInit {
 
-  constructor() { }
+    constructor() { }
 
-  ngOnInit() {
-  }
+    ngOnInit() {
+    }
 
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.css b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.css
index cb78e8d..f335726 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.css
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.css
@@ -15,11 +15,6 @@
  */
 /* --- Topo Instance Panel --- */
 
-#topo-p-instance {
-    height: 85px;
-    padding: 10px;
-}
-
 #topo-p-instance div.onosInst {
     display: inline-block;
     width: 170px;
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.html b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.html
index 6a1aba9..f856213 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.html
@@ -13,17 +13,28 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
-<div id="topo-p-instance" class="floatpanel" style="left: 20px; width: 170px; height: 85px;" [@instancePanelState]="!on">
-    <div class="onosInst online ready mastership affinity">
+<div id="topo-p-instance" class="floatpanel" [ngStyle]="{'left': '20px', 'top':divTopPx+'px', 'width': (onosInstances.length * 170)+'px', 'height': '85px'}" [@instancePanelState]="!on">
+    <div *ngFor="let inst of onosInstances | keyvalue ; let i=index"
+         [ngClass]="['onosInst', inst.value.online?'online':'', inst.value.ready? 'ready': '', mastership?'mastership':'', 'affinity']"
+            (click)="chooseMastership(inst.value.id)">
         <svg width="170" height="85" viewBox="0 0 170 85">
-            <rect x="5" y="5" width="160" height="30" style="fill: rgb(91, 153, 210);"></rect>
+            <!-- The following blue-glow effect is applied (through CSS) when mastership style is activated on a rectangle -->
+            <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>
+            <rect x="5" y="5" width="160" height="30" [ngStyle]="{ 'fill': panelColour(i)}"></rect>
+            <text class="instTitle" x="48" y="27">{{ inst.value.id }}</text>
             <rect x="5" y="35" width="160" height="45"></rect>
+            <text class="instLabel ip" x="48" y="55">{{ inst.value.ip }}</text>
             <use width="20" height="20" class="glyph badgeIcon bird" xlink:href="#bird" transform="translate(15,10)"></use>
-            <use width="16" height="16" class="glyph overlay badgeIcon readyBadge" xlink:href="#checkMark" transform="translate(18,40)"></use>
-            <text class="instTitle" x="48" y="27">127.0.0.1</text>
-            <text class="instLabel ip" x="48" y="55">127.0.0.1</text>
-            <text class="instLabel ns" x="48" y="73">Devices: 0</text>
-            <use width="24" height="24" class="glyph overlay badgeIcon uiBadge" xlink:href="#uiAttached" transform="translate(14,54)"></use>
+            <use *ngIf="inst.value.ready" width="16" height="16" class="glyph overlay badgeIcon readyBadge" xlink:href="#checkMark" transform="translate(18,40)"></use>
+            <text class="instLabel ns" x="48" y="73">Devices: {{ inst.value.switches }}</text>
+            <use *ngIf="inst.value.uiAttached" width="24" height="24" class="glyph overlay badgeIcon uiBadge" xlink:href="#uiAttached" transform="translate(14,54)"></use>
         </svg>
     </div>
 </div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.ts b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.ts
index 66b2a05..05e8768 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.component.ts
@@ -13,18 +13,41 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit, Input } from '@angular/core';
+import {
+    Component,
+    Input,
+    Output,
+    EventEmitter
+} from '@angular/core';
 import { animate, state, style, transition, trigger } from '@angular/animations';
 import {
     LogService,
     LoadingService,
     FnService,
-    PanelBaseImpl
+    PanelBaseImpl,
+    IconService,
+    SvgUtilService
 } from 'gui2-fw-lib';
 
-/*
- ONOS GUI -- Topology Instances Panel.
- Displays ONOS instances.
+/**
+ * A model of instance information that drives each panel
+ */
+export interface Instance {
+    id: string;
+    ip: string;
+    online: boolean;
+    ready: boolean;
+    switches: number;
+    uiAttached: boolean;
+}
+
+/**
+ * ONOS GUI -- Topology Instances Panel.
+ * Displays ONOS instances. The onosInstances Array gets updated by topology.service
+ * whenever a topo2AllInstances update arrives back on the WebSocket
+ *
+ * This emits a mastership event when the user clicks on an instance, to
+ * see the devices that it has mastership of.
  */
 @Component({
     selector: 'onos-instance',
@@ -38,29 +61,57 @@
         trigger('instancePanelState', [
             state('true', style({
                 transform: 'translateX(0%)',
-                opacity: '100'
+                opacity: '1.0'
             })),
             state('false', style({
                 transform: 'translateX(-100%)',
-                opacity: '0'
+                opacity: '0.0'
             })),
             transition('0 => 1', animate('100ms ease-in')),
             transition('1 => 0', animate('100ms ease-out'))
         ])
     ]
 })
-export class InstanceComponent extends PanelBaseImpl implements OnInit {
+export class InstanceComponent extends PanelBaseImpl {
+    @Input() divTopPx: number = 100;
+    @Output() mastershipEvent = new EventEmitter<string>();
+    public onosInstances: Array<Instance>;
+    protected mastership: string;
 
     constructor(
         protected fs: FnService,
         protected log: LogService,
         protected ls: LoadingService,
+        protected is: IconService,
+        protected sus: SvgUtilService
     ) {
         super(fs, ls, log);
+        this.onosInstances = <Array<Instance>>[];
+        this.is.loadIconDef('active');
+        this.is.loadIconDef('uiAttached');
         this.log.debug('InstanceComponent constructed');
     }
 
-    ngOnInit() {
+    /**
+     * Get a colour for the banner of the nth panel
+     * @param idx The index of the panel (0-6)
+     */
+    panelColour(idx: number): string {
+        return this.sus.cat7().getColor(idx, false, '');
     }
 
+    /**
+     * Toggle the display of mastership
+     * If the same instance is clicked a second time then cancel display of mastership
+     * @param instId The instance to display mastership for
+     */
+    chooseMastership(instId: string): void {
+        if (this.mastership === instId) {
+            this.mastership = '';
+        } else {
+            this.mastership = instId;
+        }
+        this.mastershipEvent.emit(this.mastership);
+        this.log.debug('Instance', this.mastership, 'chosen on GUI');
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.theme.css b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.theme.css
index a20711d..3be7bdd 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.theme.css
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/instance/instance.theme.css
@@ -34,4 +34,119 @@
 }
 .dark #topo-p-instance .online svg .glyph.overlay {
     fill: #fff;
+}
+
+/* offline */
+#topo-p-instance svg .badgeIcon {
+    opacity: 0.4;
+    fill: #939598;
+}
+
+/* online */
+#topo-p-instance .online svg .badgeIcon {
+    opacity: 1.0;
+    fill: #939598;
+}
+#topo-p-instance .online svg .badgeIcon.bird {
+    fill: #ffffff;
+}
+
+#topo-p-instance svg .readyBadge {
+    visibility: hidden;
+}
+#topo-p-instance .ready svg .readyBadge {
+    visibility: visible;
+}
+
+#topo-p-instance svg text {
+    text-anchor: start;
+    opacity: 0.5;
+    fill: #3c3a3a;
+}
+
+#topo-p-instance .online svg text {
+    opacity: 1.0;
+    fill: #3c3a3a;
+}
+
+#topo-p-instance .onosInst.mastership {
+    opacity: 0.3;
+}
+#topo-p-instance .onosInst.mastership.affinity {
+    opacity: 1.0;
+}
+#topo-p-instance .onosInst.mastership.affinity svg rect {
+    filter: url(#blue-glow);
+}
+
+.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+    filter: url(#blue-glow);
+}
+
+.dark #topo-p-instance {
+    background-color: #2f313c;
+    color: #c2c2b7;
+    border: 1px solid #364144;
+
+}
+
+.dark #topo-p-instance svg rect {
+    stroke-width: 0;
+    fill: #525660;
+}
+
+/* body of an instance */
+.dark #topo-p-instance .online svg rect {
+    opacity: 1;
+    fill: #838992;
+}
+
+.dark #topo-p-instance svg .glyph {
+    fill: #ddd;
+}
+.dark #topo-p-instance .online svg .glyph {
+    fill: #fff;
+}
+.dark #topo-p-instance .online svg .glyph.overlay {
+    fill: #c7c7c7;
+}
+
+/* offline */
+.dark #topo-p-instance svg .badgeIcon {
+    opacity: 0.4;
+    fill: #939598;
+}
+
+/* online */
+.dark #topo-p-instance .online svg .badgeIcon {
+    opacity: 1.0;
+    fill: #939598;
+}
+.dark #topo-p-instance .online svg .badgeIcon.bird {
+    fill: #ffffff;
+}
+
+.dark #topo-p-instance svg text {
+    text-anchor: start;
+    opacity: 0.5;
+    fill: #aaa;
+}
+
+.dark #topo-p-instance .online svg text {
+    opacity: 1.0;
+    fill: #fff;
+}
+
+.dark #topo-p-instance .onosInst.mastership {
+    opacity: 0.3;
+}
+.dark #topo-p-instance .onosInst.mastership.affinity {
+    opacity: 1.0;
+}
+.dark #topo-p-instance .onosInst.mastership.affinity svg rect {
+    filter: url(#blue-glow);
+}
+
+.dark.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+    filter: url(#blue-glow);
 }
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts b/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts
index 7de5e89..c1c41f8 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts
@@ -94,7 +94,6 @@
     handleSummaryData(data: SummaryResponse) {
         this.summaryData = data;
         this.render();
-        this.log.debug('Summary', data);
     }
 
     private render() {
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
index a26f3ec..a954827 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
@@ -27,6 +27,7 @@
 import { BackgroundSvgComponent } from './layer/backgroundsvg/backgroundsvg.component';
 import { ForceSvgComponent } from './layer/forcesvg/forcesvg.component';
 import { MapSvgComponent } from './layer/mapsvg/mapsvg.component';
+import { TopologyService } from './topology.service';
 
 /**
  * ONOS GUI -- Topology View Module
@@ -51,6 +52,9 @@
         BackgroundSvgComponent,
         ForceSvgComponent,
         MapSvgComponent
+    ],
+    providers: [
+        TopologyService
     ]
 })
 export class TopologyModule { }
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology.service.spec.ts b/web/gui2/src/main/webapp/app/view/topology/topology.service.spec.ts
index ca8711a..29d456f 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology.service.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.service.spec.ts
@@ -14,23 +14,61 @@
  * limitations under the License.
  */
 import { TestBed, inject } from '@angular/core/testing';
-import { LogService, ConsoleLoggerService } from 'gui2-fw-lib';
+import { ActivatedRoute, Params } from '@angular/router';
+import {of} from 'rxjs';
+
 import { TopologyService } from './topology.service';
+import {
+    LogService,
+    FnService
+} from 'gui2-fw-lib';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
 
 /**
  * ONOS GUI -- Topology Service - Unit Tests
  */
 describe('TopologyService', () => {
-    let log: LogService;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let mockWindow: Window;
 
     beforeEach(() => {
-        log = new ConsoleLoggerService();
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            },
+            location: <any>{
+                hostname: 'foo',
+                host: 'foo',
+                port: '80',
+                protocol: 'http',
+                search: { debug: 'true' },
+                href: 'ws://foo:123/onos/ui/websock/path',
+                absUrl: 'ws://foo:123/onos/ui/websock/path'
+            }
+        };
+        fs = new FnService(ar, logSpy, mockWindow);
 
         TestBed.configureTestingModule({
             providers: [TopologyService,
-                { provide: LogService, useValue: log },
+                { provide: FnService, useValue: fs},
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
             ]
         });
+        logServiceSpy = TestBed.get(LogService);
     });
 
     it('should be created', inject([TopologyService], (service: TopologyService) => {
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology.service.ts b/web/gui2/src/main/webapp/app/view/topology/topology.service.ts
index b75ed07..1d85a08 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology.service.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.service.ts
@@ -15,9 +15,11 @@
  */
 import { Injectable } from '@angular/core';
 import {
-    LogService,
+    LogService, WebSocketService,
 } from 'gui2-fw-lib';
-
+import { InstanceComponent } from './panel/instance/instance.component';
+import { BackgroundSvgComponent } from './layer/backgroundsvg/backgroundsvg.component';
+import { ForceSvgComponent } from './layer/forcesvg/forcesvg.component';
 
 /**
  * ONOS GUI -- Topology Service Module.
@@ -25,19 +27,72 @@
 @Injectable()
 export class TopologyService {
 
+    private handlers: string[] = [];
+    private openListener: any;
+
     constructor(
         protected log: LogService,
+        protected wss: WebSocketService
     ) {
-        this.log.debug('Initialized TopologyService');
+        this.log.debug('TopologyService constructed');
     }
 
-    init() {
+    /**
+     * bind our event handlers to the web socket service, so that our
+     * callbacks get invoked for incoming events
+     */
+    init(instance: InstanceComponent, background: BackgroundSvgComponent, force: ForceSvgComponent) {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            ['topo2AllInstances', (data) => {
+                    this.log.warn('Add fn for topo2AllInstances callback', data);
+                    instance.onosInstances = data.members;
+                }
+            ],
+            ['topo2CurrentLayout', (data) => {
+                    this.log.warn('Add fn for topo2CurrentLayout callback', data);
+                    background.layoutData = data;
+                }
+            ],
+            ['topo2CurrentRegion', (data) => {
+                    this.log.warn('Add fn for topo2CurrentRegion callback', data);
+                    force.regionData = data;
+                }
+            ],
+            ['topo2PeerRegions', (data) => { this.log.warn('Add fn for topo2PeerRegions callback', data); } ],
+            ['topo2UiModelEvent', (data) => { this.log.warn('Add fn for topo2UiModelEvent callback', data); } ],
+            ['topo2Highlights', (data) => { this.log.warn('Add fn for topo2Highlights callback', data); } ],
+        ]));
+        this.handlers.push('topo2AllInstances');
+        this.handlers.push('topo2CurrentLayout');
+        this.handlers.push('topo2CurrentRegion');
+        this.handlers.push('topo2PeerRegions');
+        this.handlers.push('topo2UiModelEvent');
+        this.handlers.push('topo2Highlights');
 
+        // in case we fail over to a new server,
+        // listen for wsock-open events
+        this.openListener = this.wss.addOpenListener(() => this.wsOpen);
 
+        // tell the server we are ready to receive topology events
+        this.wss.sendEvent('topo2Start', {});
+        this.log.debug('TopologyService initialized');
     }
 
+    /**
+     * tell the server we no longer wish to receive topology events
+     */
     destroy() {
+        this.wss.sendEvent('topo2Stop', {});
+        this.wss.unbindHandlers(this.handlers);
+        this.wss.removeOpenListener(this.openListener);
+        this.openListener = null;
+        this.log.debug('TopologyService destroyed');
+    }
 
 
+    wsOpen(host: string, url: string) {
+        this.log.debug('topo2Event: WSopen - cluster node:', host, 'URL:', url);
+        // tell the server we are ready to receive topo events
+        this.wss.sendEvent('topo2Start', {});
     }
 }
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 0db2c82..d74991c 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
@@ -15,7 +15,7 @@
 -->
 <onos-flash id="topoMsgFlash" message="{{ flashMsg }}" (closed)="flashMsg = ''"></onos-flash>
 
-<onos-instance #instance></onos-instance>
+<onos-instance #instance [divTopPx]="80" (mastershipEvent)="force.onosInstMastership = $event"></onos-instance>
 <onos-summary #summary></onos-summary>
 <onos-toolbar #toolbar></onos-toolbar>
 <onos-details #details></onos-details>
@@ -24,8 +24,8 @@
     <svg viewBox="0 0 1000 1000" id="topo2">
         <svg:g onos-nodeviceconnected />
         <svg:g id="topo-zoomlayer">
-            <svg:g onos-backgroundsvg/>
-            <svg:g onos-forcesvg/>
+            <svg:g #background onos-backgroundsvg/>
+            <svg:g #force onos-forcesvg/>
         </svg:g>
     </svg>
 </div>
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts
index 5933d37..d98a55c 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts
@@ -20,10 +20,14 @@
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 
 import { TopologyComponent } from './topology.component';
-import { InstanceComponent } from '../panel/instance/instance.component';
+import {
+    Instance,
+    InstanceComponent
+} from '../panel/instance/instance.component';
 import { SummaryComponent } from '../panel/summary/summary.component';
 import { ToolbarComponent } from '../panel/toolbar/toolbar.component';
 import { DetailsComponent } from '../panel/details/details.component';
+import { TopologyService } from '../topology.service';
 
 import {
     FlashComponent,
@@ -41,6 +45,32 @@
 
 class MockHttpClient {}
 
+class MockTopologyService {
+    init(instance: InstanceComponent) {
+        instance.onosInstances = [
+            <Instance>{
+                'id': 'inst1',
+                'ip': '127.0.0.1',
+                'reachable': true,
+                'online': true,
+                'ready': true,
+                'switches': 4,
+                'uiAttached': true
+            },
+            <Instance>{
+                'id': 'inst1',
+                'ip': '127.0.0.2',
+                'reachable': true,
+                'online': true,
+                'ready': true,
+                'switches': 3,
+                'uiAttached': false
+            }
+        ];
+    }
+    destroy() {}
+}
+
 /**
  * ONOS GUI -- Topology View -- Unit Tests
  */
@@ -84,6 +114,7 @@
                 { provide: LogService, useValue: logSpy },
                 { provide: 'Window', useValue: windowMock },
                 { provide: HttpClient, useClass: MockHttpClient },
+                { provide: TopologyService, useClass: MockTopologyService }
             ]
         }).compileComponents();
         logServiceSpy = TestBed.get(LogService);
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
index bf46637..728347d 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit, ViewChild } from '@angular/core';
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
 import * as d3 from 'd3';
 import {
     FnService,
@@ -21,9 +21,12 @@
     LogService, PrefsService,
     SvgUtilService, WebSocketService, Zoomer, ZoomOpts, ZoomService
 } from 'gui2-fw-lib';
-import {InstanceComponent} from '../panel/instance/instance.component';
-import {SummaryComponent} from '../panel/summary/summary.component';
-import {DetailsComponent} from '../panel/details/details.component';
+import { InstanceComponent } from '../panel/instance/instance.component';
+import { SummaryComponent } from '../panel/summary/summary.component';
+import { DetailsComponent } from '../panel/details/details.component';
+import { BackgroundSvgComponent } from '../layer/backgroundsvg/backgroundsvg.component';
+import { ForceSvgComponent } from '../layer/forcesvg/forcesvg.component';
+import { TopologyService } from '../topology.service';
 
 /**
  * ONOS GUI Topology View
@@ -54,10 +57,13 @@
   templateUrl: './topology.component.html',
   styleUrls: ['./topology.component.css']
 })
-export class TopologyComponent implements OnInit {
+export class TopologyComponent implements OnInit, OnDestroy {
+    // These are references to the components inserted in the template
     @ViewChild(InstanceComponent) instance: InstanceComponent;
     @ViewChild(SummaryComponent) summary: SummaryComponent;
     @ViewChild(DetailsComponent) details: DetailsComponent;
+    @ViewChild(BackgroundSvgComponent) background: BackgroundSvgComponent;
+    @ViewChild(ForceSvgComponent) force: ForceSvgComponent;
 
     flashMsg: string = '';
     prefsState = {};
@@ -73,7 +79,8 @@
         protected sus: SvgUtilService,
         protected ps: PrefsService,
         protected wss: WebSocketService,
-        protected zs: ZoomService
+        protected zs: ZoomService,
+        protected ts: TopologyService,
     ) {
 
         this.log.debug('Topology component constructed');
@@ -90,9 +97,19 @@
             zoomCallback: (() => { return; })
         });
         this.zoomEventListeners = [];
+        // The components from the template are handed over to TopologyService here
+        // so that WebSocket responses can be passed back in to them
+        // The handling of the WebSocket call is delegated out to the Topology
+        // Service just to compartmentalize things a bit
+        this.ts.init(this.instance, this.background, this.force);
         this.log.debug('Topology component initialized');
     }
 
+    ngOnDestroy() {
+        this.ts.destroy();
+        this.log.debug('Topology component destroyed');
+    }
+
     actionMap() {
         return {
             L: [() => {this.cycleDeviceLabels(); }, 'Cycle device labels'],