Implemented WebSockets for GUI2

Change-Id: I4776ce392b1e8e94ebee938cf7df22791a1e0b8f
diff --git a/web/gui2/src/main/webapp/app/detectbrowser.directive.ts b/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
index b69ca39..b8a3c77 100644
--- a/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
+++ b/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import { Inject } from '@angular/core';
 import { Directive } from '@angular/core';
 import { FnService } from './fw/util/fn.service';
 import { LogService } from './log.service';
@@ -28,10 +29,9 @@
   constructor(
     private fs: FnService,
     private log: LogService,
-    private onos: OnosService
+    private onos: OnosService,
+    @Inject(Window) private w: Window
   ) {
-        log.debug('DetectBrowserDirective constructed');
-
         const body: HTMLBodyElement = document.getElementsByTagName('body')[0];
 //        let body = d3.select('body');
         let browser = '';
@@ -44,7 +44,10 @@
         } else if (fs.isFirefox()) {
             browser = 'firefox';
         } else {
-            this.log.warn('Unknown browser:', window.navigator.vendor);
+            this.log.warn('Unknown browser. ',
+            'Vendor:', this.w.navigator.vendor,
+            'Agent:', this.w.navigator.userAgent);
+            return;
         }
         body.classList.add(browser);
 //        body.classed(browser, true);
diff --git a/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts b/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
index e01b004..6292a8d 100644
--- a/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
+++ b/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
@@ -24,21 +24,23 @@
 import { LoadingService } from './loading.service';
 import { PanelService } from './panel.service';
 import { QuickHelpService } from './quickhelp.service';
-import { VeilService } from './veil.service';
+import { VeilComponent } from './veil/veil.component';
 
 /**
  * ONOS GUI -- Layers Module
  */
 @NgModule({
   exports: [
-    FlashComponent
+    FlashComponent,
+    VeilComponent
   ],
   imports: [
     CommonModule,
     UtilModule
   ],
   declarations: [
-    FlashComponent
+    FlashComponent,
+    VeilComponent
   ],
   providers: [
     DetailsPanelService,
@@ -46,8 +48,7 @@
     EditableTextService,
     LoadingService,
     PanelService,
-    QuickHelpService,
-    VeilService
+    QuickHelpService
   ]
 })
 export class LayerModule { }
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts b/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts
deleted file mode 100644
index d755e5d..0000000
--- a/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2015-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.
- */
-import { Injectable } from '@angular/core';
-import { FnService } from '../util/fn.service';
-import { GlyphService } from '../svg/glyph.service';
-import { KeyService } from '../util/key.service';
-import { LogService } from '../../log.service';
-import { WebSocketService } from '../remote/websocket.service';
-
-/**
- * ONOS GUI -- Layer -- Veil Service
- *
- * Provides a mechanism to display an overlaying div with information.
- * Used mainly for web socket connection interruption.
- */
-@Injectable()
-export class VeilService {
-
-  constructor(
-    private fs: FnService,
-    private gs: GlyphService,
-    private ks: KeyService,
-    private log: LogService,
-    private wss: WebSocketService
-  ) {
-      this.log.debug('VeilService constructed');
-  }
-
-}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css
new file mode 100644
index 0000000..851d6e3
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/*
+ ONOS GUI -- Veil Service (layout) -- CSS file
+ */
+
+#veil {
+    z-index: 5000;
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    padding: 60px;
+}
+
+#veil p {
+    display: block;
+    text-align: left;
+    font-size: 14pt;
+    font-style: italic;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html
new file mode 100644
index 0000000..79d202b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html
@@ -0,0 +1,9 @@
+<div id="veil" *ngIf="enabled">
+    <p *ngFor="let msg of messages">{{ msg }}</p>
+    <svg [attr.width]="fs.windowSize().width" [attr.height]="fs.windowSize().height">
+        <use [attr.width]="birdDim" [attr.height]="birdDim" class="glyph"
+             style="opacity: 0.2;"
+             xlink:href = "#bird" [attr.transform]="trans"/>
+
+    </svg>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
new file mode 100644
index 0000000..0ed493f
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/*
+ ONOS GUI -- Layer -- Veil Service - Unit Tests
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { VeilComponent } from './veil.component';
+import { ConsoleLoggerService } from '../../../consolelogger.service';
+import { LogService } from '../../../log.service';
+import { KeyService } from '../../util/key.service';
+import { GlyphService } from '../../svg/glyph.service';
+
+class MockKeyService {}
+
+class MockGlyphService {}
+
+describe('VeilComponent', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            declarations: [ VeilComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                { provide: KeyService, useClass: MockKeyService },
+                { provide: GlyphService, useClass: MockGlyphService },
+            ]
+        });
+    });
+
+    it('should create', () => {
+        const fixture = TestBed.createComponent(VeilComponent);
+        const component = fixture.componentInstance;
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css
new file mode 100644
index 0000000..7a3bded
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/*
+ ONOS GUI -- Veil Service (theme) -- CSS file
+ */
+
+.light, #veil {
+    background-color: rgba(0,0,0,0.75);
+}
+.dark, #veil {
+    background-color: rgba(64,64,64,0.75);
+}
+
+#veil p {
+    color: #ddd;
+}
+
+#veil svg .glyph {
+    fill: #222;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts
new file mode 100644
index 0000000..cea6bed
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2015-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.
+ */
+import { Component, OnInit } from '@angular/core';
+import { FnService } from '../../util/fn.service';
+import { GlyphService } from '../../svg/glyph.service';
+import { KeyService } from '../../util/key.service';
+import { LogService } from '../../../log.service';
+import { SvgUtilService } from '../../svg/svgutil.service';
+import { WebSocketService } from '../../remote/websocket.service';
+
+const BIRD = 'bird';
+
+/**
+ * ONOS GUI -- Layer -- Veil Component
+ *
+ * Provides a mechanism to display an overlaying div with information.
+ * Used mainly for web socket connection interruption.
+ *
+ * It can be added to an component's template as follows:
+ *     <onos-veil #veil></onos-veil>
+ *     <p (click)="veil.show(['t1','t2','t3'])">Test Veil</p>
+ */
+@Component({
+  selector: 'onos-veil',
+  templateUrl: './veil.component.html',
+  styleUrls: ['./veil.component.css', './veil.component.theme.css']
+})
+export class VeilComponent implements OnInit {
+    ww: number;
+    wh: number;
+    birdSvg: string;
+    birdDim: number;
+    enabled: boolean = false;
+    trans: string;
+    messages: string[] = [];
+    veilStyle: string;
+
+    constructor(
+        private fs: FnService,
+        private gs: GlyphService,
+        private ks: KeyService,
+        private log: LogService,
+        private sus: SvgUtilService,
+        private wss: WebSocketService
+    ) {
+        const wSize = this.fs.windowSize();
+        this.ww = wSize.width;
+        this.wh = wSize.height;
+        const shrink = this.wh * 0.3;
+        this.birdDim = this.wh - shrink;
+        const birdCenter = (this.ww - this.birdDim) / 2;
+        this.trans = this.sus.translate([birdCenter, shrink / 2]);
+
+        this.log.debug('VeilComponent with ' + BIRD + ' constructed');
+    }
+
+    ngOnInit() {
+    }
+
+    // msg should be an array of strings
+    show(msgs: string[]): void {
+        this.messages = msgs;
+        this.enabled = true;
+//        this.ks.enableKeys(false);
+    }
+
+    hide(): void {
+        this.veilStyle = 'display: none';
+//        this.ks.enableKeys(true);
+    }
+
+
+}
diff --git a/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
index 4685a65..381a0c9 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
@@ -13,27 +13,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
 import { LogService } from '../../log.service';
 
-const uiContext = '/onos/ui/';
-const rsSuffix = uiContext + 'rs/';
-const wsSuffix = uiContext + 'websock/';
+const UICONTEXT = '/onos/ui/';
+const RSSUFFIX = UICONTEXT + 'rs/';
+const WSSUFFIX = UICONTEXT + 'websock/';
 
 /**
  * ONOS GUI -- Remote -- General Purpose URL Functions
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class UrlFnService {
     constructor(
         private log: LogService,
-        private window: Window
+        @Inject(Window) private w: Window
     ) {
         this.log.debug('UrlFnService constructed');
     }
 
     matchSecure(protocol: string): string {
-        const p: string = window.location.protocol;
+        const p: string = this.w.location.protocol;
         const secure: boolean = (p === 'https' || p === 'wss');
         return secure ? protocol + 's' : protocol;
     }
@@ -42,32 +44,35 @@
      * behind a proxy and has an app prefix, e.g.
      *      http://host:port/my/app/onos/ui...
      * This bit of regex grabs everything after the host:port and
-     * before the uiContext (/onos/ui/) and uses that as an app
+     * before the UICONTEXT (/onos/ui/) and uses that as an app
      * prefix by inserting it back into the WS URL.
      * If no prefix, then no insert.
      */
-    urlBase(protocol: string, port: string, host: string): string {
-        const match = window.location.href.match('.*//[^/]+/(.+)' + uiContext);
+    urlBase(protocol: string, port: string = '', host: string = ''): string {
+        const match = this.w.location.href.match('.*//[^/]+/(.+)' + UICONTEXT);
         const appPrefix = match ? '/' + match[1] : '';
 
-        return this.matchSecure(protocol) + '://' +
-            (host || window.location.hostname) + ':' +
-            (port || window.location.port) + appPrefix;
+        return this.matchSecure(protocol) +
+            '://' +
+            (host === '' ? this.w.location.hostname : host) +
+            ':' +
+            (port === '' ? this.w.location.port : port) +
+            appPrefix;
     }
 
     httpPrefix(suffix: string): string {
-        return this.urlBase('http', '', '') + suffix;
+        return this.urlBase('http') + suffix;
     }
 
-    wsPrefix(suffix: string, wsport: any, host: string): string {
+    wsPrefix(suffix: string, wsport: string, host: string): string {
         return this.urlBase('ws', wsport, host) + suffix;
     }
 
     rsUrl(path: string): string {
-        return this.httpPrefix(rsSuffix) + path;
+        return this.httpPrefix(RSSUFFIX) + path;
     }
 
-    wsUrl(path: string, wsport: any, host: string): string {
-        return this.wsPrefix(wsSuffix, wsport, host) + path;
+    wsUrl(path: string, wsport?: string, host?: string): string {
+        return this.wsPrefix(WSSUFFIX, wsport, host) + path;
     }
 }
diff --git a/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
index 0691c72..e470ec1 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
@@ -18,37 +18,111 @@
 import { GlyphService } from '../svg/glyph.service';
 import { LogService } from '../../log.service';
 import { UrlFnService } from './urlfn.service';
+import { VeilComponent } from '../layer/veil/veil.component';
 import { WSock } from './wsock.service';
 
 /**
  * Event Type structure for the WebSocketService
  */
-interface EventType {
+export interface EventType {
     event: string;
-    payload: any;
+    payload: Object;
+}
+
+export interface Callback {
+    id: number;
+    error: string;
+    cb(host: string, url: string): void;
+}
+
+interface ClusterNode {
+    id: string;
+    ip: string;
+    m_uiAttached: boolean;
+}
+
+interface Bootstrap {
+    user: string;
+    clusterNodes: ClusterNode[];
+    glyphs: any[]; // TODO: narrow this down to a known type
+}
+
+interface ErrorData {
+    message: string;
+}
+
+export interface WsOptions {
+    wsport: number;
 }
 
 /**
  * ONOS GUI -- Remote -- Web Socket Service
+ *
+ * To see debug messages add ?debug=txrx to the URL
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class WebSocketService {
     // internal state
-    private webSockOpts; // web socket options
-    private ws = null; // web socket reference
-    private wsUp = false; // web socket is good to go
-    private handlers = {}; // event handler bindings
+    private webSockOpts: WsOptions; // web socket options
+    private ws: WebSocket = null; // web socket reference
+    private wsUp: boolean = false; // web socket is good to go
+
+    // A map of event handler bindings - names and functions (that accept data and return void)
+    private handlers = new Map<string, (data: any) => void>([]);
     private pendingEvents: EventType[] = []; // events TX'd while socket not up
     private host: string; // web socket host
     private url; // web socket URL
-    private clusterNodes = []; // ONOS instances data for failover
+    private clusterNodes: ClusterNode[] = []; // ONOS instances data for failover
     private clusterIndex = -1; // the instance to which we are connected
     private glyphs = [];
-    private connectRetries = 0; // limit our attempts at reconnecting
-    private openListeners = {}; // registered listeners for websocket open()
-    private nextListenerId = 1; // internal ID for open listeners
-    private loggedInUser = null; // name of logged-in user
+    private connectRetries: number = 0; // limit our attempts at reconnecting
 
+    // A map of registered Callbacks for websocket open()
+    private openListeners = new Map<number, Callback>([]);
+    private nextListenerId: number = 1; // internal ID for open listeners
+    private loggedInUser = null; // name of logged-in user
+    private lcd: any; // The loading component delegate
+    private vcd: any; // The veil component delegate
+
+    /**
+     * built-in handler for the 'boostrap' event
+     */
+    private bootstrap(data: Bootstrap) {
+        this.log.debug('bootstrap data', data);
+        this.loggedInUser = data.user;
+
+        this.clusterNodes = data.clusterNodes;
+        this.clusterNodes.forEach((d, i) => {
+            if (d.m_uiAttached) {
+                this.clusterIndex = i;
+                this.log.info('Connected to cluster node ' + d.ip);
+                // TODO: add connect info to masthead somewhere
+            }
+        });
+        this.glyphs = data.glyphs;
+        const glyphsMap = new Map<string, string>([]);
+        this.glyphs.forEach((d, i) => {
+            glyphsMap.set('_' + d.id, d.viewbox);
+            glyphsMap.set(d.id, d.path);
+            this.gs.registerGlyphs(glyphsMap);
+        });
+    }
+
+    private error(data: ErrorData) {
+        const m: string = data.message || 'error from server';
+        this.log.error(m, data);
+
+        // Unrecoverable error - throw up the veil...
+        if (this.vcd) {
+            this.vcd.show([
+                'Oops!',
+                'Server reports error...',
+                m,
+            ]);
+        }
+    }
 
     constructor(
         private fs: FnService,
@@ -59,90 +133,319 @@
         private window: Window
     ) {
         this.log.debug(window.location.hostname);
+
+        // Bind the boot strap event by default
+        this.bindHandlers(new Map<string, (data) => void>([
+            ['bootstrap', (data) => this.bootstrap(data)],
+            ['error', (data) => this.error(data)]
+        ]));
+
         this.log.debug('WebSocketService constructed');
     }
 
+
+    // ==========================
+    // === Web socket callbacks
+
+    /**
+     * Called when WebSocket has just opened
+     *
+     * Lift the Veil if it is displayed
+     * If there are any events pending, send them
+     * Mark the WSS as up and inform any listeners for this open event
+     */
+    handleOpen(): void {
+        this.log.info('Web socket open - ', this.url);
+        // Hide the veil
+        if (this.vcd) {
+            this.vcd.hide();
+        }
+
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug('Sending ' + this.pendingEvents.length + ' pending event(s)...');
+        }
+        this.pendingEvents.forEach((ev) => {
+            this.send(ev);
+        });
+        this.pendingEvents = [];
+
+        this.connectRetries = 0;
+        this.wsUp = true;
+        this.informListeners(this.host, this.url);
+    }
+
+    /**
+     * Function called when WebSocket send a message
+     */
+    handleMessage(msgEvent: MessageEvent): void {
+        let ev: EventType;
+        let h;
+        try {
+            ev = JSON.parse(msgEvent.data);
+        } catch (e) {
+            this.log.error('Message.data is not valid JSON', msgEvent.data, e);
+            return null;
+        }
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug(' << *Rx* ', ev.event, ev.payload);
+        }
+        h = this.handlers.get(ev.event);
+        if (h) {
+            try {
+                h(ev.payload);
+            } catch (e) {
+                this.log.error('Problem handling event:', ev, e);
+                return null;
+            }
+        } else {
+            this.log.warn('Unhandled event:', ev);
+        }
+    }
+
+    /**
+     * Called by the WebSocket if it is closed from the server end
+     *
+     * If the loading component is shown, call stop() on it
+     * Try to find another node in the cluster to connect to
+     * If this is not possible then show the Veil Component
+     */
+    handleClose(): void {
+        this.log.warn('Web socket closed');
+        if (this.lcd) {
+            this.lcd.stop();
+        }
+        this.wsUp = false;
+        let gsucc;
+
+        if (gsucc = this.findGuiSuccessor()) {
+            this.url = this.createWebSocket(this.webSockOpts, gsucc);
+        } else {
+            // If no controllers left to contact, show the Veil...
+            if (this.vcd) {
+                this.vcd.show([
+                    'Oops!',  // TODO: Localize this
+                    'Web-socket connection to server closed...',
+                    'Try refreshing the page.',
+                ]);
+            }
+        }
+    }
+
+    // ==============================
+    // === Private Helper Functions
+
+    /**
+     * Find the next node in the ONOS cluster.
+     *
+     * This is used if the WebSocket connection closes because a
+     * node in the cluster ges down - fail over should be automatic
+     */
+    findGuiSuccessor(): string {
+        const ncn = this.clusterNodes.length;
+        let ip: string;
+        let node;
+
+        while (this.connectRetries < ncn && !ip) {
+            this.connectRetries++;
+            this.clusterIndex = (this.clusterIndex + 1) % ncn;
+            node = this.clusterNodes[this.clusterIndex];
+            ip = node && node.ip;
+        }
+
+        return ip;
+    }
+
+    /**
+     * When the WebSocket is opened, inform any listeners that registered
+     * for that event
+     */
+    informListeners(host: string, url: string): void {
+        for (const [key, cb] of this.openListeners.entries()) {
+            cb.cb(host, url);
+        }
+    }
+
+    send(ev: EventType): void {
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug(' *Tx* >> ', ev.event, ev.payload);
+        }
+        this.ws.send(JSON.stringify(ev));
+    }
+
+    /**
+     * Check if there are no WSS event handlers left
+     */
+    noHandlersWarn(handlers: Map<string, Object>, caller: string): boolean {
+        if (!handlers || handlers.size === 0) {
+            this.log.warn('WSS.' + caller + '(): no event handlers');
+            return true;
+        }
+        return false;
+    }
+
     /* ===================
      * === API Functions
-     *
+     */
+
+    /**
      * Required for unit tests to set to known state
      */
-    resetState() {
+    resetState(): void {
         this.webSockOpts = undefined;
         this.ws = null;
         this.wsUp = false;
         this.host = undefined;
         this.url = undefined;
         this.pendingEvents = [];
-        this.handlers = {};
+        this.handlers.clear();
         this.clusterNodes = [];
         this.clusterIndex = -1;
         this.glyphs = [];
         this.connectRetries = 0;
-        this.openListeners = {};
+        this.openListeners.clear();
         this.nextListenerId = 1;
 
     }
 
-    /* Currently supported opts:
+    /*
+     * Currently supported opts:
      *  wsport: web socket port (other than default 8181)
      *  host:   if defined, is the host address to use
      */
-    createWebSocket(opts, _host_: string = '') {
-        const wsport = (opts && opts.wsport) || null;
-
+    createWebSocket(opts?: WsOptions, host?: string) {
         this.webSockOpts = opts; // preserved for future calls
+        this.host = host === undefined ? this.window.location.host : host;
+        this.url = this.ufs.wsUrl('core', opts === undefined ? '' : opts['wsport'].toString(), host);
 
-//        this.host = _host_ || this.host();
-        const url = this.ufs.wsUrl('core', wsport, _host_);
-
-        this.log.debug('Attempting to open websocket to: ' + url);
-        this.ws = this.wsock.newWebSocket(url);
+        this.log.debug('Attempting to open websocket to: ' + this.url);
+        this.ws = this.wsock.newWebSocket(this.url);
         if (this.ws) {
-            this.ws.onopen = this.handleOpen();
-            this.ws.onmessage = this.handleMessage('???');
-            this.ws.onclose = this.handleClose();
+            // fat arrow => syntax means that the 'this' context passed will
+            // be of WebSocketService, not the WebSocket
+            this.ws.onopen = (() => this.handleOpen());
+            this.ws.onmessage = ((msgEvent) => this.handleMessage(msgEvent));
+            this.ws.onclose = (() => this.handleClose());
 
-//            sendEvent('authentication', { token: onosAuth });
-            this.sendEvent('authentication token', '');
+            this.sendEvent('authentication token', { token: 'testAuth' });
         }
         // Note: Wsock logs an error if the new WebSocket call fails
-        return url;
+        return this.url;
     }
 
-    handleOpen() {
-        this.log.debug('WebSocketService: handleOpen() not yet implemented');
+
+    /**
+     * Binds the message handlers to their message type (event type) as
+     *  specified in the given map. Note that keys are the event IDs; values
+     *  are either:
+     *     * the event handler function, or
+     *     * an API object which has an event handler for the key
+     */
+    bindHandlers(handlerMap: Map<string, (data) => void>): void {
+        const dups: string[] = [];
+
+        if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
+            return null;
+        }
+        for (const [eventId, api] of handlerMap) {
+            this.log.debug('Adding handler for ', eventId);
+            const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
+            if (!fn) {
+                this.log.warn(eventId + ' handler not a function');
+                return;
+            }
+
+            if (this.handlers.get(eventId)) {
+                dups.push(eventId);
+            } else {
+                this.handlers.set(eventId, fn);
+            }
+        }
+        if (dups.length) {
+            this.log.warn('duplicate bindings ignored:', dups);
+        }
     }
 
-    handleMessage(msgEvent: any) {
-        this.log.debug('WebSocketService: handleMessage() not yet implemented');
+    /**
+     * Unbinds the specified message handlers.
+     *   Expected that the same map will be used, but we only care about keys
+     */
+    unbindHandlers(handlerMap: Map<string, (data) => void>): void {
+        if (this.noHandlersWarn(handlerMap, 'unbindHandlers')) {
+            return null;
+        }
+
+        for (const [eventId, api] of handlerMap) {
+            this.handlers.delete(eventId);
+        }
     }
 
-    handleClose() {
-        this.log.debug('WebSocketService: handleClose() not yet implemented');
+    /**
+     * Add a listener function for listening for WebSocket opening.
+     * The function must give a host and url and return void
+     */
+    addOpenListener(callback: (host: string, url: string) => void ): Callback {
+        const id: number = this.nextListenerId++;
+        const cb = this.fs.isF(callback);
+        const o: Callback = <Callback>{ id: id, cb: cb };
+
+        if (cb) {
+            this.openListeners.set(id, o);
+        } else {
+            this.log.error('WSS.addOpenListener(): callback not a function');
+            o.error = 'No callback defined';
+        }
+        return o;
     }
 
-    /* Formulates an event message and sends it via the web-socket.
+    /**
+     * Remove a listener of WebSocket opening
+     */
+    removeOpenListener(lsnr: Callback): void {
+        const id = this.fs.isO(lsnr) && lsnr.id;
+        let o;
+
+        if (!id) {
+            this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
+            return null;
+        }
+        o = this.openListeners[id];
+
+        if (o) {
+            this.openListeners.delete(id);
+        }
+    }
+
+    /**
+     * Formulates an event message and sends it via the web-socket.
      * If the websocket is not up yet, we store it in a pending list.
      */
-    sendEvent(evType, payload) {
+    sendEvent(evType: string, payload: Object ): void {
         const ev = <EventType> {
             event: evType,
             payload: payload
         };
 
         if (this.wsUp) {
-            this._send(ev);
+            this.send(ev);
         } else {
             this.pendingEvents.push(ev);
         }
     }
 
-    _send(ev: EventType) {
-        if (this.fs.debugOn('txrx')) {
-            this.log.debug(' *Tx* >> ', ev.event, ev.payload);
-        }
-        this.ws.send(JSON.stringify(ev));
+    /**
+     * Binds the veil service as a delegate.
+     */
+    setVeilDelegate(vd: VeilComponent): void {
+        this.vcd = vd;
+    }
+
+    /**
+     * Binds the loading service as a delegate
+     */
+    setLoadingDelegate(ld: any): void {
+        // TODO - Investigate changing Loading Service to LoadingComponent
+        this.log.debug('Loading delegate set', ld);
+        this.lcd = ld;
     }
 
 }
diff --git a/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts b/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
index 176b5d3..86e0625 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
@@ -21,7 +21,9 @@
  *
  * This service provided specifically so that it can be mocked in unit tests.
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class WSock {
 
   constructor(
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
index 674cbbf..c6a81e9 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
@@ -222,7 +222,7 @@
             'xlink:href': '#' + glyphId,
             width: dim,
             height: dim,
-            transform: this.sus.translate(xlate, xlate),
+            transform: this.sus.translate([xlate], xlate),
         });
         return g;
     }
diff --git a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
index 51d5d2a..9cba079 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
@@ -22,7 +22,9 @@
  *
  * The SVG Util Service provides a miscellany of utility functions.
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class SvgUtilService {
 
     constructor(
@@ -32,7 +34,7 @@
         this.log.debug('SvgUtilService constructed');
     }
 
-    translate(x, y) {
+    translate(x: number[], y?: any): string {
         if (this.fs.isA(x) && x.length === 2 && !y) {
             return 'translate(' + x[0] + ',' + x[1] + ')';
         }
diff --git a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
index 0726afc..d355ce9 100644
--- a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
@@ -13,13 +13,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
 import { ActivatedRoute, Router} from '@angular/router';
 import { LogService } from '../../log.service';
 
 // Angular>=2 workaround for missing definition
 declare const InstallTrigger: any;
 
+const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
+const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
+const evillist: string[] = ['script', 'style', 'iframe'];
+
+/**
+ * Used with the Window size function;
+ **/
+export interface WindowSize {
+    width: number;
+    height: number;
+}
+
+/**
+ * For the sanitize() and analyze() functions
+ */
+export interface Match {
+    full: string;
+    name: string;
+}
 
 // TODO Move all this trie stuff to its own class
 // Angular>=2 Tightened up on types to avoid compiler errors
@@ -133,13 +152,14 @@
 @Injectable()
 export class FnService {
     // internal state
-    debugFlags = new Map<string, boolean>([
+    private debugFlags = new Map<string, boolean>([
 //        [ "LoadingService", true ]
     ]);
 
     constructor(
         private route: ActivatedRoute,
-        private log: LogService
+        private log: LogService,
+        @Inject(Window) private w: Window
     ) {
         this.route.queryParams.subscribe(params => {
             const debugparam: string = params['debug'];
@@ -149,33 +169,155 @@
         log.debug('FnService constructed');
     }
 
-    isF(f) {
+    /**
+     * Test if an argument is a function
+     *
+     * Note: the need for this would go away if all functions
+     * were strongly typed
+     */
+    isF(f: any): any {
         return typeof f === 'function' ? f : null;
     }
 
-    isA(a) {
+    /**
+     * Test if an argument is an array
+     *
+     * Note: the need for this would go away if all arrays
+     * were strongly typed
+     */
+    isA(a: any): any {
     // NOTE: Array.isArray() is part of EMCAScript 5.1
         return Array.isArray(a) ? a : null;
     }
 
-    isS(s) {
+    /**
+     * Test if an argument is a string
+     *
+     * Note: the need for this would go away if all strings
+     * were strongly typed
+     */
+    isS(s: any): string {
         return typeof s === 'string' ? s : null;
     }
 
-    isO(o) {
+    /**
+     * Test if an argument is an object
+     *
+     * Note: the need for this would go away if all objects
+     * were strongly typed
+     */
+    isO(o: any): Object {
         return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
     }
 
-//    contains: contains,
-//    areFunctions: areFunctions,
-//    areFunctionsNonStrict: areFunctionsNonStrict,
-//    windowSize: windowSize,
+    /**
+     * Test that an array contains an object
+     */
+    contains(a: any[], x: any): boolean {
+        return this.isA(a) && a.indexOf(x) > -1;
+    }
+
+    /**
+     * Returns width and height of window inner dimensions.
+     * offH, offW : offset width/height are subtracted, if present
+     */
+    windowSize(offH: number = 0, offW: number = 0): WindowSize {
+        return {
+            height: this.w.innerHeight - offH,
+            width: this.w.innerWidth - offW
+        };
+    }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise.
+     * Also returns false if there are properties on the api that are NOT
+     * listed in the array of names.
+     *
+     * This gets extra complicated when the api Object is an
+     * Angular service - while the functions can be retrieved
+     * by an indexed get, the ownProperties does not show the
+     * functions of the class. We have to dive in to the prototypes
+     * properties to get these - and even then we have to filter
+     * out the constructor and any member variables
+     */
+    areFunctions(api: Object, fnNames: string[]): boolean {
+        const fnLookup: Map<string, boolean> = new Map();
+        let extraFound: boolean = false;
+
+        if (!this.isA(fnNames)) {
+            return false;
+        }
+
+        const n: number = fnNames.length;
+        let i: number;
+        let name: string;
+
+        for (i = 0; i < n; i++) {
+            name = fnNames[i];
+            if (!this.isF(api[name])) {
+                return false;
+            }
+            fnLookup.set(name, true);
+        }
+
+        // check for properties on the API that are not listed in the array,
+        const keys = Object.getOwnPropertyNames(api);
+        if (keys.length === 0) {
+            return true;
+        }
+        // If the api is a class it will have a name,
+        //  else it will just be called 'Object'
+        const apiObjectName: string = api.constructor.name;
+        if (apiObjectName === 'Object') {
+            Object.keys(api).forEach((key) => {
+                if (!fnLookup.get(key)) {
+                    extraFound = true;
+                }
+            });
+        } else { // It is a class, so its functions will be in the child (prototype)
+            const pObj: Object = Object.getPrototypeOf(api);
+            for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
+                if (key === 'constructor') { // Filter out constructor
+                    continue;
+                }
+                const value = Object.getOwnPropertyDescriptor(pObj, key);
+                // Only compare functions. Look for any not given in the map
+                if (this.isF(value.value) && !fnLookup.get(key)) {
+                    extraFound = true;
+                }
+            }
+        }
+        return !extraFound;
+    }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise. This is a non-strict version
+     * that does not care about other properties on the api.
+     */
+    areFunctionsNonStrict(api, fnNames): boolean {
+        if (!this.isA(fnNames)) {
+            return false;
+        }
+        const n = fnNames.length;
+        let i;
+        let name;
+
+        for (i = 0; i < n; i++) {
+            name = fnNames[i];
+            if (!this.isF(api[name])) {
+                return false;
+            }
+        }
+        return true;
+    }
 
     /**
      * Returns true if current browser determined to be a mobile device
      */
     isMobile() {
-        const ua = window.navigator.userAgent;
+        const ua = this.w.navigator.userAgent;
         const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
         return patt.test(ua);
     }
@@ -184,10 +326,10 @@
      * Returns true if the current browser determined to be Chrome
      */
     isChrome() {
-        const isChromium = (window as any).chrome;
-        const vendorName = window.navigator.vendor;
+        const isChromium = (this.w as any).chrome;
+        const vendorName = this.w.navigator.vendor;
 
-        const isOpera = window.navigator.userAgent.indexOf('OPR') > -1;
+        const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
         return (isChromium !== null &&
         isChromium !== undefined &&
         vendorName === 'Google Inc.' &&
@@ -195,8 +337,8 @@
     }
 
     isChromeHeadless() {
-        const vendorName = window.navigator.vendor;
-        const headlessChrome = window.navigator.userAgent.indexOf('HeadlessChrome') > -1;
+        const vendorName = this.w.navigator.vendor;
+        const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
 
         return (vendorName === 'Google Inc.' && headlessChrome === true);
     }
@@ -205,8 +347,8 @@
      * Returns true if the current browser determined to be Safari
      */
     isSafari() {
-        return (window.navigator.userAgent.indexOf('Safari') !== -1 &&
-        window.navigator.userAgent.indexOf('Chrome') === -1);
+        return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
+        this.w.navigator.userAgent.indexOf('Chrome') === -1);
     }
 
     /**
@@ -217,13 +359,90 @@
     }
 
     /**
+     * search through an array of objects, looking for the one with the
+     * tagged property matching the given key. tag defaults to 'id'.
+     * returns the index of the matching object, or -1 for no match.
+     */
+    find(key: string, array: Object[], tag: string = 'id'): number {
+        let idx: number;
+        const n: number = array.length;
+
+        for (idx = 0 ; idx < n; idx++) {
+            const d: Object = array[idx];
+            if (d[tag] === key) {
+                return idx;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * search through array to find (the first occurrence of) item,
+     * returning its index if found; otherwise returning -1.
+     */
+    inArray(item: any, array: any[]): number {
+        if (this.isA(array)) {
+            for (let i = 0; i < array.length; i++) {
+                if (array[i] === item) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * remove (the first occurrence of) the specified item from the given
+     * array, if any. Return true if the removal was made; false otherwise.
+     */
+    removeFromArray(item: any, array: any[]): boolean {
+        const i: number = this.inArray(item, array);
+        if (i >= 0) {
+            array.splice(i, 1);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * return true if the object is empty, return false otherwise
+     */
+    isEmptyObject(obj: Object): boolean {
+        for (const key in obj) {
+            if (true) { return false; }
+        }
+        return true;
+    }
+
+    /**
      * Return the given string with the first character capitalized.
      */
-    cap(s) {
+    cap(s: string): string {
         return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
     }
 
     /**
+     * return the parameter without a px suffix
+     */
+    noPx(num: string): number {
+        return Number(num.replace(/px$/, ''));
+    }
+
+    /**
+     * return an element's given style property without px suffix
+     */
+    noPxStyle(elem: any, prop: string): number {
+        return Number(elem.style(prop).replace(/px$/, ''));
+    }
+
+    /**
+     * Return true if a str ends with suffix
+     */
+    endsWith(str: string, suffix: string) {
+        return str.indexOf(suffix, str.length - suffix.length) !== -1;
+    }
+
+    /**
      * output debug message to console, if debug tag set...
      * e.g. fs.debug('mytag', arg1, arg2, ...)
      */
@@ -233,7 +452,7 @@
         }
     }
 
-    parseDebugFlags(dbgstr: string): void {
+    private parseDebugFlags(dbgstr: string): void {
         const bits = dbgstr ? dbgstr.split(',') : [];
         bits.forEach((key) => {
             this.debugFlags.set(key, true);
@@ -248,4 +467,66 @@
         return this.debugFlags.get(tag);
     }
 
+
+
+    // -----------------------------------------------------------------
+    // The next section deals with sanitizing external strings destined
+    // to be loaded via a .html() function call.
+    //
+    // See definition of matcher, evillist and whitelist at the top of this file
+
+    /*
+     * Returns true if the tag is in the evil list, (and is not an end-tag)
+     */
+    inEvilList(tag: any): boolean {
+        return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+    }
+
+    /*
+     * Returns an array of Matches of matcher in html
+     */
+    analyze(html: string): Match[] {
+        const matches: Match[] = [];
+        let match;
+
+        // extract all tags
+        while ((match = matcher.exec(html)) !== null) {
+            matches.push({
+                full: match[0],
+                name: match[1],
+                // NOTE: ignoring attributes {match[2].split(' ')} for now
+            });
+        }
+
+        return matches;
+    }
+
+    /*
+     * Returns a cleaned version of html
+     */
+    sanitize(html: string): string {
+        const matches: Match[] = this.analyze(html);
+
+        // completely obliterate evil tags and their contents...
+        evillist.forEach((tag) => {
+            const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
+            html = html.replace(re, '');
+        });
+
+        // filter out all but white-listed tags and end-tags
+        matches.forEach((tag) => {
+            if (whitelist.indexOf(tag.name) === -1) {
+                html = html.replace(tag.full, '');
+                if (this.inEvilList(tag)) {
+                    this.log.warn('Unsanitary HTML input -- ' +
+                        tag.full + ' detected!');
+                }
+            }
+        });
+
+        // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
+
+        return html;
+    }
+
 }
diff --git a/web/gui2/src/main/webapp/app/fw/util/lion.service.ts b/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
index a33ef6b..5ab9f65 100644
--- a/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
@@ -29,19 +29,43 @@
 /**
  * ONOS GUI -- Lion -- Localization Utilities
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class LionService {
 
     ubercache: any[];
 
+    /**
+     * Handler for uberlion event from WSS
+     */
+    uberlion(data: Lion) {
+        this.ubercache = data.lion;
+
+        this.log.info('LION service: Locale... [' + data.locale + ']');
+        this.log.info('LION service: Bundles installed...');
+
+        for (const p in this.ubercache) {
+            if (this.ubercache[p]) {
+                this.log.info('            :=> ', p);
+            }
+        }
+
+        this.log.debug('LION service: uber-lion bundle received:', data);
+    }
+
     constructor(
         private log: LogService,
         private wss: WebSocketService
     ) {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            ['uberlion', (data) => this.uberlion(data) ]
+        ]));
         this.log.debug('LionService constructed');
     }
 
-    /* returns a lion bundle (function) for the given bundle ID
+    /**
+     * Returns a lion bundle (function) for the given bundle ID (string)
      * returns a function that takes a string and returns a string
      */
     bundle(bundleId: string): (string) => string {
@@ -58,21 +82,4 @@
     getKey(key: string): string {
         return this.bundle[key] || '%' + key + '%';
     }
-
-    /* handler for uberlion event..
-     */
-    uberlion(data: Lion) {
-        this.ubercache = data.lion;
-
-        this.log.info('LION service: Locale... [' + data.locale + ']');
-        this.log.info('LION service: Bundles installed...');
-
-        for (const p in this.ubercache) {
-            if (this.ubercache[p]) {
-                this.log.info('            :=> ', p);
-            }
-        }
-
-        this.log.debug('LION service: uber-lion bundle received:', data);
-    }
 }
diff --git a/web/gui2/src/main/webapp/app/onos.common.css b/web/gui2/src/main/webapp/app/onos.common.css
new file mode 100644
index 0000000..9a3f5a6
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/onos.common.css
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/*
+ ONOS GUI -- common -- CSS file
+ */
+
+.clickable {
+    cursor: pointer;
+}
+
+.light .editable {
+    border-bottom: 1px dashed #ca504b;
+}
+
+.dark .editable {
+    border-bottom: 1px dashed #df4f4a;
+}
+
+.light svg.embeddedIcon .icon .glyph {
+    fill: #0071bd;
+}
+
+.dark svg.embeddedIcon .icon .glyph {
+    fill: #375b7f;
+}
diff --git a/web/gui2/src/main/webapp/app/onos.component.css b/web/gui2/src/main/webapp/app/onos.component.css
index e69de29..60933d8 100644
--- a/web/gui2/src/main/webapp/app/onos.component.css
+++ b/web/gui2/src/main/webapp/app/onos.component.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014-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.
+ */
+
+/*
+ ONOS GUI -- core (layout) -- CSS file
+ */
+
+#view {
+    padding: 6px;
+    width: 100%;
+    height: 100%;
+}
+
+#view h2 {
+    -webkit-margin-before: 0;
+    -webkit-margin-after: 0;
+    margin: 32px 0 4px 16px;
+    padding: 0;
+    font-size: 18pt;
+    font-weight: lighter;
+}
diff --git a/web/gui2/src/main/webapp/app/onos.component.html b/web/gui2/src/main/webapp/app/onos.component.html
index 566c972..42d2bdd 100644
--- a/web/gui2/src/main/webapp/app/onos.component.html
+++ b/web/gui2/src/main/webapp/app/onos.component.html
@@ -1,6 +1,8 @@
 <div style="text-align:center" onosDetectBrowser>
     <onos-mast></onos-mast>
     <onos-nav></onos-nav>
+    <onos-veil #veil></onos-veil>
+    <div>{{ wss.setVeilDelegate(veil) }}</div>
     <router-outlet></router-outlet>
 </div>
 
diff --git a/web/gui2/src/main/webapp/app/onos.component.ts b/web/gui2/src/main/webapp/app/onos.component.ts
index 02b3a44..bbec955 100644
--- a/web/gui2/src/main/webapp/app/onos.component.ts
+++ b/web/gui2/src/main/webapp/app/onos.component.ts
@@ -19,11 +19,10 @@
 import { KeyService } from './fw/util/key.service';
 import { ThemeService } from './fw/util/theme.service';
 import { GlyphService } from './fw/svg/glyph.service';
-import { VeilService } from './fw/layer/veil.service';
 import { PanelService } from './fw/layer/panel.service';
 import { QuickHelpService } from './fw/layer/quickhelp.service';
 import { EeService } from './fw/util/ee.service';
-import { WebSocketService } from './fw/remote/websocket.service';
+import { WebSocketService, WsOptions } from './fw/remote/websocket.service';
 import { SpriteService } from './fw/svg/sprite.service';
 import { OnosService, View } from './onos.service';
 
@@ -61,7 +60,7 @@
 @Component({
   selector: 'onos-root',
   templateUrl: './onos.component.html',
-  styleUrls: ['./onos.component.css']
+  styleUrls: ['./onos.component.css', './onos.common.css']
 })
 export class OnosComponent implements OnInit {
     public title = 'onos';
@@ -81,7 +80,6 @@
         private ks: KeyService,
         private ts: ThemeService,
         private gs: GlyphService,
-        private vs: VeilService,
         private ps: PanelService,
         private qhs: QuickHelpService,
         private ee: EeService,
@@ -101,6 +99,8 @@
         log.warn('OnosComponent: testing logger.warn()');
         log.error('OnosComponent: testing logger.error()');
 
+        this.wss.createWebSocket(<WsOptions>{ wsport: 8181});
+
         log.debug('OnosComponent constructed');
     }
 
diff --git a/web/gui2/src/main/webapp/app/onos.css b/web/gui2/src/main/webapp/app/onos.css
deleted file mode 100644
index fa485ac..0000000
--- a/web/gui2/src/main/webapp/app/onos.css
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2014-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.
- */
-
-/*
- ONOS GUI -- core (layout) -- CSS file
- */
-
-html {
-    font-family: 'Open Sans', sans-serif;
-    -webkit-text-size-adjust: 100%;
-    -ms-text-size-adjust: 100%;
-    height: 100%;
-}
-
-/*
-   overflow hidden is to ensure that the body does not expand to account
-   for any flyout panes, that are positioned "off screen".
- */
-body {
-    height: 100%;
-    margin: 0;
-    overflow: hidden;
-}
-
-#view {
-    padding: 6px;
-    width: 100%;
-    height: 100%;
-}
-
-#view h2 {
-    -webkit-margin-before: 0;
-    -webkit-margin-after: 0;
-    margin: 32px 0 4px 16px;
-    padding: 0;
-    font-size: 18pt;
-    font-weight: lighter;
-}
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.ts b/web/gui2/src/main/webapp/app/view/device/device.component.ts
index db42c4d..6929f9b 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.ts
@@ -58,7 +58,7 @@
     ngOnInit() {
         this.log.debug('DeviceComponent initialized');
         // TODO: Remove this - it's only for demo purposes
-        this.ls.startAnim();
+//        this.ls.startAnim();
     }
 
 }
diff --git a/web/gui2/src/main/webapp/app/view/device/device.module.ts b/web/gui2/src/main/webapp/app/view/device/device.module.ts
index 9cd50cb..8f0f351 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.module.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.module.ts
@@ -17,7 +17,7 @@
 import { CommonModule } from '@angular/common';
 import { DeviceComponent } from './device.component';
 import { DeviceDetailsPanelDirective } from './devicedetailspanel.directive';
-
+import { RemoteModule } from '../../fw/remote/remote.module';
 /**
  * ONOS GUI -- Device View Module
  */
@@ -26,7 +26,8 @@
     DeviceComponent
   ],
   imports: [
-    CommonModule
+    CommonModule,
+    RemoteModule
   ],
   declarations: [
     DeviceComponent,