GUI2 Display of mastership for devices

Change-Id: I13ed95d1a58d055aa913c69402541b87855c28c8
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
index 89923ae..af0ca2b 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
@@ -28,7 +28,7 @@
     SimpleChanges,
     ViewChildren
 } from '@angular/core';
-import {LocMeta, LogService, MetaUi, WebSocketService, ZoomUtils} from 'gui2-fw-lib';
+import {LocMeta, LogService, MetaUi, SvgUtilService, WebSocketService, ZoomUtils} from 'gui2-fw-lib';
 import {
     Device,
     DeviceProps,
@@ -256,6 +256,21 @@
     }
 
     /**
+     * If instance has a value then mute colors of devices not connected to it
+     * Otherwise if instance does not have a value unmute all
+     * @param instanceName name of the selected instance
+     */
+    changeInstSelection(instanceName: string) {
+        this.log.debug('Mastership changed', instanceName);
+        this.devices.filter((d) => d.device.master !== instanceName)
+            .forEach((d) => {
+                const isMuted = Boolean(instanceName);
+                d.ngOnChanges({'colorMuted': new SimpleChange(!isMuted, isMuted, true)});
+            }
+        );
+    }
+
+    /**
      * If a node has a fixed location then assign it to fx and fy so
      * that it doesn't get affected by forces
      * @param graphNode The node whose location should be processed
@@ -397,6 +412,11 @@
                         if (changes.locationChanged) {
                             this.fixPosition(oldDevice);
                         }
+                        const svgDevice: DeviceNodeSvgComponent =
+                            this.devices.find((svgdevice) => svgdevice.device.id === subject);
+                        svgDevice.ngOnChanges({'device':
+                                new SimpleChange(<Device>{}, oldDevice, true)
+                        });
                     }
                 } else {
                     this.log.warn('Device ', memo, ' - not yet implemented', data);
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
index 35c3fd4..fb0569b 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
@@ -67,7 +67,7 @@
         overlaid on the above. This is the blue box, and its width and height does
         not change
     -->
-    <svg:rect x="-16" y="-16" width="32" height="32" [ngStyle]="{'fill': panelColour(0)}">
+    <svg:rect x="-16" y="-16" width="32" height="32" [ngStyle]="{'fill': panelColor}">
     </svg:rect>
     <svg:path *ngIf="device.location && device.location.locType != 'none'"
               d="M-15 12 v3 h5" style="stroke: white; stroke-width: 1; fill: none"></svg:path>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
index b2f0592..994967d 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
@@ -16,12 +16,13 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { DeviceNodeSvgComponent } from './devicenodesvg.component';
-import {FnService, IconService, LogService} from 'gui2-fw-lib';
+import {FnService, IconService, LogService, SvgUtilService} from 'gui2-fw-lib';
 import {ActivatedRoute, Params} from '@angular/router';
 import {of} from 'rxjs';
 import {ChangeDetectorRef} from '@angular/core';
 import {Device} from '../../models';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {TopologyService} from '../../../../topology.service';
 
 class MockActivatedRoute extends ActivatedRoute {
     constructor(params: Params) {
@@ -34,6 +35,40 @@
     loadIconDef() { }
 }
 
+class MockSvgUtilService {
+
+    cat7() {
+        const tcid = 'd3utilTestCard';
+
+        function getColor(id, muted, theme) {
+            // NOTE: since we are lazily assigning domain ids, we need to
+            //       get the color from all 4 scales, to keep the domains
+            //       in sync.
+            const ln = '#5b99d2';
+            const lm = '#9ebedf';
+            const dn = '#5b99d2';
+            const dm = '#9ebedf';
+            if (theme === 'dark') {
+                return muted ? dm : dn;
+            } else {
+                return muted ? lm : ln;
+            }
+        }
+
+        return {
+            // testCard: testCard,
+            getColor: getColor,
+        };
+    }
+}
+
+class MockTopologyService {
+    public instancesIndex: Map<string, number>;
+    constructor() {
+        this.instancesIndex = new Map();
+    }
+}
+
 describe('DeviceNodeSvgComponent', () => {
     let fs: FnService;
     let logServiceSpy: jasmine.SpyObj<LogService>;
@@ -71,6 +106,8 @@
                 { provide: ActivatedRoute, useValue: ar },
                 { provide: ChangeDetectorRef, useClass: ChangeDetectorRef },
                 { provide: IconService, useClass: MockIconService },
+                { provide: SvgUtilService, useClass: MockSvgUtilService },
+                { provide: TopologyService, useClass: MockTopologyService },
                 { provide: 'Window', useValue: windowMock },
             ]
         })
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
index d40f413..9c246d9 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
@@ -19,7 +19,7 @@
     Component,
     EventEmitter,
     Input,
-    OnChanges, Output,
+    OnChanges, OnInit, Output,
     SimpleChanges,
 } from '@angular/core';
 import {Device, LabelToggle, UiElement} from '../../models';
@@ -27,6 +27,7 @@
 import {NodeVisual, SelectedEvent} from '../nodevisual';
 import {animate, state, style, transition, trigger} from '@angular/animations';
 import {LocationType} from '../../../backgroundsvg/backgroundsvg.component';
+import {TopologyService} from '../../../../topology.service';
 
 /**
  * The Device node in the force graph
@@ -64,21 +65,30 @@
         ])
     ]
 })
-export class DeviceNodeSvgComponent extends NodeVisual implements OnChanges {
+export class DeviceNodeSvgComponent extends NodeVisual implements OnInit, OnChanges {
     @Input() device: Device;
     @Input() scale: number = 1.0;
     @Input() labelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
+    @Input() colorMuted: boolean = false;
+    @Input() colorTheme: string = 'light';
     @Output() selectedEvent = new EventEmitter<SelectedEvent>();
     textWidth: number = 36;
+    panelColor: string = '#9ebedf';
+
     constructor(
         protected log: LogService,
         private is: IconService,
         protected sus: SvgUtilService,
+        protected ts: TopologyService,
         private ref: ChangeDetectorRef
     ) {
         super();
     }
 
+    ngOnInit(): void {
+        this.panelColor = this.panelColour();
+    }
+
     /**
      * Called by parent (forcesvg) when a change happens
      *
@@ -92,6 +102,13 @@
                 this.device.x = 0;
                 this.device.y = 0;
             }
+            // The master might have changed - recalculate color
+            this.panelColor = this.panelColour();
+        }
+
+        if (changes['colorMuted']) {
+            this.colorMuted = changes['colorMuted'].currentValue;
+            this.panelColor = this.panelColour();
         }
         this.ref.markForCheck();
     }
@@ -137,7 +154,8 @@
      * 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, '');
+    panelColour(): string {
+        const idx = this.ts.instancesIndex.get(this.device.master);
+        return this.sus.cat7().getColor(idx, this.colorMuted, this.colorTheme);
     }
 }
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.html
index f5a2859..9f2ee1a 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.html
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.html
@@ -15,7 +15,7 @@
 -->
 <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']"
+         [ngClass]="['onosInst', inst.value.online?'online':'', inst.value.ready? 'ready': '', mastership===inst.value.id?'mastership':'', 'affinity']"
             (click)="chooseMastership(inst.value.id)">
         <svg xmlns="http://www.w3.org/2000/svg" width="170" height="85" viewBox="0 0 170 85">
             <!-- The following blue-glow effect is applied (through CSS) when mastership style is activated on a rectangle -->
@@ -37,4 +37,4 @@
             <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
+</div>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.ts
index 70cbb1f..1ae8de4 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.ts
@@ -17,7 +17,7 @@
     Component,
     Input,
     Output,
-    EventEmitter
+    EventEmitter, OnChanges, SimpleChanges
 } from '@angular/core';
 import { animate, state, style, transition, trigger } from '@angular/animations';
 import {
@@ -71,11 +71,11 @@
         ])
     ]
 })
-export class InstanceComponent extends PanelBaseImpl {
+export class InstanceComponent extends PanelBaseImpl implements OnChanges {
+    @Input() onosInstances: Instance[] = [];
     @Input() divTopPx: number = 100;
     @Input() on: boolean = false; // Override the parent class attribute
     @Output() mastershipEvent = new EventEmitter<string>();
-    public onosInstances: Array<Instance>;
     public mastership: string;
     lionFn; // Function
 
@@ -87,7 +87,6 @@
         private lion: LionService
     ) {
         super(fs, log);
-        this.onosInstances = <Array<Instance>>[];
 
         if (this.lion.ubercache.length === 0) {
             this.lionFn = this.dummyLion;
@@ -98,6 +97,12 @@
         this.log.debug('InstanceComponent constructed');
     }
 
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes['onosInstances']) {
+            this.onosInstances = <Instance[]>changes['onosInstances'].currentValue;
+        }
+    }
+
     /**
      * Get a colour for the banner of the nth panel
      * @param idx The index of the panel (0-6)
@@ -113,7 +118,7 @@
      */
     chooseMastership(instId: string): void {
         if (this.mastership === instId) {
-            this.mastership = '';
+            this.mastership = undefined;
         } else {
             this.mastership = instId;
         }
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
index 304be9e..f2a1f60 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
@@ -17,7 +17,7 @@
 import {
     LogService, WebSocketService,
 } from 'gui2-fw-lib';
-import { InstanceComponent } from './panel/instance/instance.component';
+import {Instance, InstanceComponent} from './panel/instance/instance.component';
 import { BackgroundSvgComponent } from './layer/backgroundsvg/backgroundsvg.component';
 import { ForceSvgComponent } from './layer/forcesvg/forcesvg.component';
 import {
@@ -34,11 +34,13 @@
 
     private handlers: string[] = [];
     private openListener: any;
+    public instancesIndex: Map<string, number>;
 
     constructor(
         protected log: LogService,
         protected wss: WebSocketService
     ) {
+        this.instancesIndex = new Map();
         this.log.debug('TopologyService constructed');
     }
 
@@ -50,7 +52,14 @@
         this.wss.bindHandlers(new Map<string, (data) => void>([
             ['topo2AllInstances', (data) => {
                     this.log.debug('Instances updated through WSS as topo2AllInstances', data);
-                    instance.onosInstances = data.members;
+                    instance.ngOnChanges(
+                        {'onosInstances': new SimpleChange({}, data.members, true)});
+
+                    // Also generate an index locally of the instances
+                    // needed so that devices can be coloured by instance
+                    this.instancesIndex.clear();
+                    (<Instance[]>data.members).forEach((inst, idx) => this.instancesIndex.set(inst.id, idx));
+                    this.log.debug('Created local index of instances', this.instancesIndex);
                 }
             ],
             ['topo2CurrentLayout', (data) => {
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.html
index e7425d1..e49ae9e 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.html
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.html
@@ -23,7 +23,7 @@
     These are referenced inside the typescript by @ViewChild and their label
 -->
 <onos-instance #instance [divTopPx]="80"
-               (mastershipEvent)="force.onosInstMastership = $event"
+               (mastershipEvent)="force.changeInstSelection($event)"
                [on]="prefsState.insts">
 </onos-instance>
 <onos-summary #summary [on]="prefsState.summary"></onos-summary>