Implemented table building functions

Change-Id: Ie4003080b13725561df22de41ec85f8c3f31c794
diff --git a/web/gui2/AngularMigration.md b/web/gui2/AngularMigration.md
index bb83d18..a1cf791 100644
--- a/web/gui2/AngularMigration.md
+++ b/web/gui2/AngularMigration.md
@@ -88,14 +88,17 @@
 is that DOM manipulations from inside JavaScript code is not the Angular 6
 way of doing things - there was a lot of this in the old ONOS GUI, using d3.append(..)
 and so on.
-The Angular 6 way of doing things is to defined DOM objects (elements) in the 
+The Angular 6 way of doing things is to define DOM objects (elements) in the 
 html template of a component, and use the Component Java Script code as a base
 for logic that can influence the display of these objects in the template.
 What this means is that what were previously defined as services (e.g. VeilService or
 LoadingService) should now become Components in Angular 6 (e.g. VeilComponent or
 LoadingComponent). 
 
-###How do I know whether a Service should be made a Component in this new GUI?
+Similarly a directive might be trying to do DOM manipulation and have a CSS - this 
+should be made in to a component instead (see IconComponent)
+
+###How do I know whether a Service or Directive should be made a Component in this new GUI?
 The general rule to follow is _"if a service in the old GUI has an associated CSS 
 file or two then is should be a component in the new GUI"_. 
 
@@ -129,6 +132,15 @@
 needs to include the components in its template by adding <onos-loading> and
 <onos-veil>.
 
+### Consider if a service is really needs to be a service that runs all the time
+Or does it just support a few functions. See the TableBase class. This now
+replaces the old TableBuilderService - that was just on function that 
+manipulated the scope of a view component. Instead view components now
+extend this class.
+
+Also sometimes directive are always used together e.g. icon directive and tooltip
+directive and they can be merged in to one
+
 ## fw/remote/wsock.service
 Taking for a really simple example the fw/remote/WSockService, this was originally defined in 
 the __app/fw/remote/wsock.js__ file and is now redefined in 
@@ -222,22 +234,49 @@
 * (d3 object).append(..).attr values should be listed individually (see icon.service for example)
 * Please try do avoid d3 DOM manipulations in ONOS GUI 2, as this is not the Angular 6 way of 
   doing things
+* $interval should be replaced by 
+    task = setInterval(() => functionname_or_body, speed);
+* To cancel the timer clearInterval(task)
 
 
-# Progress so far - 24 May 2018
+# Progress so far - 18 Jun 2018
 The following services are partially migrated:
-* fw/util/FnService - some essential components of this have been migrated - still lots to do
+* fw/util/FnService - full migrated with Unit tests
 * fw/svg/GlyphDataService - mostly migrated. Values are stored in maps as constants
 * fw/svg/GlyphService - partly implemented - enough to support the icon service
 * fw/svg/IconService - mostly implemented - enough to support the IconDirective
-* fw/svg/IconDirective - mostly implemented - although want to replace this with 
-  the IconComponent
 * fw/svg/icon/IconComponent - replacement for the IconDirective - decided to make 
    it a component because it has CSS files which are scoped to just that component
-* fw/layer/LoadingService - mostly implemented - I'm leaving this as a Service, 
-  although maybe it should become a component - its CSS is has to be loaded 
-  globally in index.html
+   It also incorporates the old fw/widget/tooltip.js which was a directive - combined
+   tooltip in to icon because the 2 are always used together in tabular views
+* fw/layer/LoadingService - mostly implemented - this should become a component 
+    - its CSS is has to be loaded globally in index.html
 * fw/layer/flash/FlashComponent - implemented as a Component instead of the old Flash Service
   because it has a CSS file. Replaced all of the D3 Dom manipulations with Template code
   in the Angular 6 style of doing things
+* fw/layer/veil/VeilComponent - changed to a component - fully implemented
+* fw/remote/urlfn.service - fully implemented with Unit tests
+* fw/remote/WebSocketService - fully implemented with Unit tests
+* fw/widget/TableBase - previously the TableBuilderService this has now been changed
+to a plain interface and class - any table views should extend this
+
+
+# Devices View
+This is now a Component, whose class extends the TableBase - this is where it gets
+most of its functionality. As before all data comes from the WebSocket.
+There is still a bit of work to go on this - scrolling of long lists, device details 
+panel etc
+
+The major change in the template (html) is that there used to be 2 tables and these
+are now brought together in to a header and body. This simplifies trying to keep 
+the widths of both in sync.
+
+For CSS the old device view CSS is included and a link is made across to the
+common table CSS
+
+#Apps View
+This is a Component too, again extending TableBase. Apps view has much more functionality 
+though because it has controls for upload and download of applications.
+
+
   
diff --git a/web/gui2/BUCK b/web/gui2/BUCK
index c875c58..4039147 100644
--- a/web/gui2/BUCK
+++ b/web/gui2/BUCK
@@ -62,7 +62,7 @@
         + 'npm5 install -g @angular/cli@6.0.0 2>&1;'
         + 'npm5 install 2>&1;'
         + 'ng -v;'
-        + 'ng build --preserve-symlinks --base-href /onos/ui2/dist/ --deploy-url /onos/ui2/dist/ --output-path="$OUT" 2>&1',
+        + 'ng build --extract-css --preserve-symlinks --base-href /onos/ui2/dist/ --deploy-url /onos/ui2/dist/ --output-path="$OUT" 2>&1',
     out = 'dist',
     visibility = [ 'PUBLIC' ],
 )
diff --git a/web/gui2/README.md b/web/gui2/README.md
index 63e783e..10b3459 100644
--- a/web/gui2/README.md
+++ b/web/gui2/README.md
@@ -9,14 +9,23 @@
 ```
 feature:install onos-gui2
 ```
-and the gui will be accessible at [http://localhost:8181/onos/ui2/dist/](http://localhost:8181/onos/ui2/dist/)
+and the gui will be accessible at [http://localhost:8181/onos/ui2](http://localhost:8181/onos/ui2)
 
 # Development
-The project relies on [Angular CLI](https://github.com/angular/angular-cli) v6 to simplify development of the browser side code.
+There are 2 ways to go about development - 
+1. rebuild the code and rerun through BUCK OR (much like can be done with any ordinary ONOS app)
+2. use Angular 6 CLI (ng command) to rebuild on the fly (must faster for development) 
 
-This allows you to develop the Angular 6 Type Script code independent of ONOS in a separate container. At the current moment (May '18) the
-implementation of WebSockets and REST calls is not done, so there is __no__ requirement to run ONOS in the background.
-This will change in the coming weeks.
+For 1) if you change the code you can redeploy the application with (requies you to be in ~/onos directory):
+```
+onos-buck build //web/gui2:onos-web-gui2-oar --show-output|grep /app.oar | cut -d\  -f2 | xargs onos-app localhost reinstall!
+```
+
+For 2) it's well worth becoming familiar with Angular CLI.
+The project is created with [Angular CLI](https://github.com/angular/angular-cli) v6 to simplify development of the browser side code.
+
+This allows you to develop the Angular 6 Type Script code independent of ONOS in a separate container. 
+Since WebSockets have been implemented (Jun 18) there is a requirement to run ONOS in the background.
 
 There is no need to install node, npm or ng again on your system, and indeed if they are already installed, it's best
 to use the versions of these that's used by BUCK. To do this add to the __start__ of your PATH environment variable. 
@@ -64,7 +73,8 @@
 ## Running unit tests
 This is automatically done when using "onos-buck test" - see the web/gui2/BUCK file for more details.
 
-To run it manually in Angular CLI run `ng test --watch=true` to execute the unit tests via [Karma](https://karma-runner.github.io).
+To run it manually in Angular CLI run `ng test --watch` to execute the unit tests via [Karma](https://karma-runner.github.io).
+Running it directly like this will test with both Firefox and Chrome. To use only one use the __--browsers__ argument
 
 ## Running checkstyle
 This is automatically done when using "onos-buck test" - see the web/gui2/BUCK file for more details.
diff --git a/web/gui2/karma.conf.js b/web/gui2/karma.conf.js
index 84af9d1..f3b73b4 100644
--- a/web/gui2/karma.conf.js
+++ b/web/gui2/karma.conf.js
@@ -8,6 +8,7 @@
     plugins: [
       require('karma-jasmine'),
       require('karma-chrome-launcher'),
+      require('karma-firefox-launcher'),
       require('karma-jasmine-html-reporter'),
       require('karma-coverage-istanbul-reporter'),
       require('@angular-devkit/build-angular/plugins/karma')
@@ -27,7 +28,7 @@
     colors: true,
     logLevel: config.LOG_INFO,
     autoWatch: true,
-    browsers: ['Chrome'],
+    browsers: ['Chrome', 'Firefox'],
     singleRun: false
   });
 };
diff --git a/web/gui2/package-lock.json b/web/gui2/package-lock.json
index 1acca73..eadeb8c 100644
--- a/web/gui2/package-lock.json
+++ b/web/gui2/package-lock.json
@@ -8446,6 +8446,12 @@
         "minimatch": "3.0.4"
       }
     },
+    "karma-firefox-launcher": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz",
+      "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==",
+      "dev": true
+    },
     "karma-jasmine": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.1.tgz",
diff --git a/web/gui2/package.json b/web/gui2/package.json
index e60a12b..0770d71 100644
--- a/web/gui2/package.json
+++ b/web/gui2/package.json
@@ -28,10 +28,9 @@
     "zone.js": "^0.8.26"
   },
   "devDependencies": {
-    "@angular/compiler-cli": "^6.0.0",
     "@angular-devkit/build-angular": "~0.6.0",
-    "typescript": "~2.7.2",
     "@angular/cli": "~6.0.0",
+    "@angular/compiler-cli": "^6.0.0",
     "@angular/language-service": "^6.0.0",
     "@types/jasmine": "~2.8.6",
     "@types/jasminewd2": "~2.0.3",
@@ -42,10 +41,12 @@
     "karma": "~1.7.1",
     "karma-chrome-launcher": "~2.2.0",
     "karma-coverage-istanbul-reporter": "~1.4.2",
+    "karma-firefox-launcher": "^1.1.0",
     "karma-jasmine": "~1.1.1",
     "karma-jasmine-html-reporter": "^0.2.2",
     "protractor": "~5.3.0",
     "ts-node": "~5.0.1",
-    "tslint": "~5.9.1"
+    "tslint": "~5.9.1",
+    "typescript": "~2.7.2"
   }
 }
diff --git a/web/gui2/src/main/webapp/app/consolelogger.service.ts b/web/gui2/src/main/webapp/app/consolelogger.service.ts
index 1ba88f9..84c9d03 100644
--- a/web/gui2/src/main/webapp/app/consolelogger.service.ts
+++ b/web/gui2/src/main/webapp/app/consolelogger.service.ts
@@ -17,9 +17,7 @@
 import { environment } from '../environments/environment';
 import { Logger } from './log.service';
 
-export let isDebugMode = environment.isDebugMode;
-
-const noop = (): any => undefined;
+export let isDebugMode: boolean = !environment.production;
 
 /**
  * ONOS GUI -- LogService
@@ -27,12 +25,13 @@
  */
 @Injectable()
 export class ConsoleLoggerService implements Logger {
+  private noop: () => void;
 
   get debug() {
     if (isDebugMode) {
       return console.debug.bind(console);
     } else {
-      return noop;
+      return this.noop;
     }
   }
 
@@ -40,7 +39,7 @@
     if (isDebugMode) {
       return console.info.bind(console);
     } else {
-      return noop;
+      return this.noop;
     }
   }
 
@@ -53,7 +52,7 @@
   }
 
   invokeConsoleMethod(type: string, args?: any): void {
-    const logFn: Function = (console)[type] || console.log || noop;
+    const logFn: Function = (console)[type] || console.log || this.noop;
     logFn.apply(console, [args]);
   }
 }
diff --git a/web/gui2/src/main/webapp/app/fw/layer/dialog.service.ts b/web/gui2/src/main/webapp/app/fw/layer/dialog.service.ts
index 880e2bd..d6c37bb 100644
--- a/web/gui2/src/main/webapp/app/fw/layer/dialog.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/layer/dialog.service.ts
@@ -38,4 +38,7 @@
     this.log.debug('DialogService constructed');
   }
 
+  createDiv() {
+  }
+
 }
diff --git a/web/gui2/src/main/webapp/app/fw/layer/loading.service.ts b/web/gui2/src/main/webapp/app/fw/layer/loading.service.ts
index 0d03ad6..3e6d48b 100644
--- a/web/gui2/src/main/webapp/app/fw/layer/loading.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/layer/loading.service.ts
@@ -33,7 +33,9 @@
  *
  * Provides a mechanism to start/stop the loading animation, center screen.
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class LoadingService {
     images: any[] = [];
     idx = 0;
@@ -127,7 +129,7 @@
     }
 
     // return true if start() has been called but not stop()
-    waiting() {
+    waiting(): boolean {
         return !!this.wait;
     }
 
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 e470ec1..34a8ea9 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
@@ -90,7 +90,6 @@
      * 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;
@@ -332,6 +331,13 @@
         return this.url;
     }
 
+    /**
+     * Tell the WebSocket to close - this should call the handleClose() method
+     */
+    closeWebSocket() {
+        this.ws.close();
+    }
+
 
     /**
      * Binds the message handlers to their message type (event type) as
@@ -369,12 +375,12 @@
      * 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')) {
+    unbindHandlers(handlerIds: string[]): void {
+        if ( handlerIds.length === 0 ) {
+            this.log.warn('WSS.unbindHandlers(): no event handlers');
             return null;
         }
-
-        for (const [eventId, api] of handlerMap) {
+        for (const eventId of handlerIds) {
             this.handlers.delete(eventId);
         }
     }
@@ -448,4 +454,7 @@
         this.lcd = ld;
     }
 
+    isConnected(): boolean {
+        return this.wsUp;
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon.directive.ts b/web/gui2/src/main/webapp/app/fw/svg/icon.directive.ts
deleted file mode 100644
index a8de561..0000000
--- a/web/gui2/src/main/webapp/app/fw/svg/icon.directive.ts
+++ /dev/null
@@ -1,50 +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 { Directive, ElementRef, Input, OnInit } from '@angular/core';
-import { IconService } from './icon.service';
-import { LogService } from '../../log.service';
-import * as d3 from 'd3';
-
-/**
- * ONOS GUI -- SVG -- Icon Directive
- *
- * TODO: Deprecated - this directive may be removed altogether as it has been
- * rebuilt as IconComponent instead
- */
-@Directive({
-  selector: '[onosIcon]'
-})
-export class IconDirective implements OnInit {
-    @Input() iconId: string;
-    @Input() iconSize = 20;
-
-    constructor(
-        private el: ElementRef,
-        private is: IconService,
-        private log: LogService
-    ) {
-        // Note: iconId is not available until initialization
-        this.log.debug('IconDirective constructed');
-    }
-
-    ngOnInit() {
-        const div = d3.select(this.el.nativeElement);
-        div.selectAll('*').remove();
-        this.is.loadEmbeddedIcon(div, this.iconId, this.iconSize);
-        this.log.debug('IconDirective initialized:', this.iconId);
-    }
-
-}
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html
index 6a32d87..5be4c19 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html
@@ -13,9 +13,11 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
-<svg class="embeddedIcon" [attr.width]="iconSize" [attr.height]="iconSize" viewBox="0 0 50 50">
-    <g class="icon" [ngClass]="iconId">
+<svg class="embeddedIcon" [attr.width]="iconSize" [attr.height]="iconSize" viewBox="0 0 50 50" (mouseover)="toolTipDisp = toolTip" (mouseout)="toolTipDisp = undefined">
+    <g class="icon" [ngClass]="classes">
         <rect width="50" height="50" rx="5"></rect>
         <use width="50" height="50" class="glyph" [attr.href]="iconTag()"></use>
     </g>
 </svg>
+<!-- I'm fixing class as light as view encapsulation changes how the hirerarchy of css is handled -->
+<p id="tooltip" class="light" *ngIf="toolTip" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none' }">{{ toolTipDisp }}</p>
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.ts b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.ts
index 45be81f..c851f47 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.ts
@@ -16,7 +16,6 @@
 import { Component, OnInit, Input } from '@angular/core';
 import { IconService, glyphMapping } from '../icon.service';
 import { LogService } from '../../../log.service';
-import * as d3 from 'd3';
 
 /**
  * Icon Component
@@ -31,11 +30,20 @@
 @Component({
   selector: 'onos-icon',
   templateUrl: './icon.component.html',
-  styleUrls: ['./icon.component.css', './icon.theme.css', './glyph.css', './glyph-theme.css']
+  styleUrls: [
+    './icon.component.css', './icon.theme.css',
+    './glyph.css', './glyph-theme.css',
+    './tooltip.css', './tooltip-theme.css'
+    ]
 })
 export class IconComponent implements OnInit {
     @Input() iconId: string;
-    @Input() iconSize = 20;
+    @Input() iconSize: number = 20;
+    @Input() toolTip: string = undefined;
+    @Input() classes: string = undefined;
+
+    // The displayed tooltip - undefined until mouse hovers over, then equals toolTip
+    toolTipDisp: string = undefined;
 
     constructor(
         private is: IconService,
@@ -47,7 +55,6 @@
 
     ngOnInit() {
         this.is.loadIconDef(this.iconId);
-        this.log.debug('IconComponent initialized for ', this.iconId);
     }
 
     /**
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css
index 59cf10f..3e6f601 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css
@@ -40,6 +40,46 @@
     fill: #3c3a3a;
 }
 
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+svg.embeddedIcon g.icon use {
+    fill: #e0dfd6;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+svg.embeddedIcon g.icon.active use {
+    fill: #939598;
+}
+svg.embeddedIcon g.icon.active:hover use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+svg.embeddedIcon g.icon.current-view rect {
+    fill: #518ecc;
+}
+svg.embeddedIcon g.icon.current-view use {
+    fill: white;
+}
+
+/* REFRESH */
+svg.embeddedIcon g.icon.refresh use {
+    fill: #cdeff2;
+}
+svg.embeddedIcon g.icon.refresh:hover use {
+    fill: #ce5b58;
+}
+svg.embeddedIcon g.icon.refresh.active use {
+    fill: #009fdb;
+}
+svg.embeddedIcon g.icon.refresh.active:hover use {
+    fill: #ce5b58;
+}
+
+
 /* ========== DARK Theme ========== */
 
 .dark div.close-btn svg.embeddedIcon g.icon .glyph {
@@ -61,11 +101,12 @@
 .dark table svg.embeddedIcon .icon .glyph {
     fill: #9999aa;
 }
-
+/*
 svg.embeddedIcon g.icon .glyph {
     fill: #007dc4;
 }
 
 svg.embeddedIcon:hover g.icon .glyph {
     fill: #20b2ff;
-}
\ No newline at end of file
+}
+*/
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip-theme.css b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip-theme.css
new file mode 100644
index 0000000..a703d1b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip-theme.css
@@ -0,0 +1,30 @@
+/*
+ * 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 -- Tooltip Service (theme) -- CSS file
+ */
+.light#tooltip {
+    background-color: #dbeffc;
+    color: #3c3a3a;
+    border-color: #c7c7c0;
+}
+
+.dark#tooltip {
+    background-color: #3a3a60;
+    border-color: #6c6a6a;
+    color: #c7c7c0;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/tooltip.service.ts b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
similarity index 62%
rename from web/gui2/src/main/webapp/app/fw/widget/tooltip.service.ts
rename to web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
index 6b08779..74a5443 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/tooltip.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
@@ -13,21 +13,18 @@
  * 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 { LogService } from '../../log.service';
 
-/**
- * ONOS GUI -- Widget -- Tooltip Service
+/*
+ ONOS GUI -- Tooltip Service (layout) -- CSS file
  */
-@Injectable()
-export class TooltipService {
 
-  constructor(
-      private fs: FnService,
-      private log: LogService,
-  ) {
-    this.log.debug('TooltipService constructed');
-  }
-
+#tooltip {
+    text-align: center;
+    font-size: 80%;
+    border: 1px solid;
+    padding: 5px;
+    position: absolute;
+    z-index: 5000;
+    display: none;
+    pointer-events: none;
 }
diff --git a/web/gui2/src/main/webapp/app/fw/svg/svg.module.ts b/web/gui2/src/main/webapp/app/fw/svg/svg.module.ts
index 9688c30..3174263 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/svg.module.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/svg.module.ts
@@ -25,7 +25,6 @@
 import { SpriteService } from './sprite.service';
 import { SpriteDataService } from './spritedata.service';
 import { SvgUtilService } from './svgutil.service';
-import { IconDirective } from './icon.directive';
 import { IconComponent } from './icon/icon.component';
 
 /**
@@ -33,7 +32,6 @@
  */
 @NgModule({
   exports: [
-    IconDirective,
     IconComponent
   ],
   imports: [
@@ -41,7 +39,6 @@
     UtilModule
   ],
   declarations: [
-    IconDirective,
     IconComponent
   ],
   providers: [
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 d355ce9..7b9ea24 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
@@ -415,6 +415,35 @@
     }
 
     /**
+     * returns true if the two objects have all the same properties
+     */
+    sameObjProps(obj1: Object, obj2: Object): boolean {
+        for (const key in obj1) {
+            if (obj1.hasOwnProperty(key)) {
+                if (!(obj1[key] === obj2[key])) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * returns true if the array contains the object
+     * does NOT use strict object reference equality,
+     * instead checks each property individually for equality
+     */
+    containsObj(arr: any[], obj: Object): boolean {
+        const len = arr.length;
+        for (let i = 0; i < len; i++) {
+            if (this.sameObjProps(arr[i], obj)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Return the given string with the first character capitalized.
      */
     cap(s: string): string {
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 5ab9f65..248fe51 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
@@ -34,7 +34,8 @@
 })
 export class LionService {
 
-    ubercache: any[];
+    ubercache: any[] = [];
+    loadCb; // Function
 
     /**
      * Handler for uberlion event from WSS
@@ -50,6 +51,10 @@
                 this.log.info('            :=> ', p);
             }
         }
+        if (this.loadCb) {
+            this.log.debug('Calling the load callback');
+            this.loadCb();
+        }
 
         this.log.debug('LION service: uber-lion bundle received:', data);
     }
@@ -69,17 +74,15 @@
      * returns a function that takes a string and returns a string
      */
     bundle(bundleId: string): (string) => string {
-        let bundle = this.ubercache[bundleId];
+        let bundleObj = this.ubercache[bundleId];
 
-        if (!bundle) {
+        if (!bundleObj) {
             this.log.warn('No lion bundle registered:', bundleId);
-            bundle = {};
+            bundleObj = {};
         }
 
-        return this.getKey;
-    }
-
-    getKey(key: string): string {
-        return this.bundle[key] || '%' + key + '%';
+        return (key) =>  {
+            return bundleObj[key] || '%' + key + '%';
+        };
     }
 }
diff --git a/web/gui2/src/main/webapp/app/fw/widget/button.service.ts b/web/gui2/src/main/webapp/app/fw/widget/button.service.ts
index da6cbab..4e22763 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/button.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/button.service.ts
@@ -17,7 +17,6 @@
 import { FnService } from '../util/fn.service';
 import { IconService } from '../svg/icon.service';
 import { LogService } from '../../log.service';
-import { TooltipService } from './tooltip.service';
 
 /**
  * ONOS GUI -- Widget -- Button Service
@@ -29,7 +28,6 @@
         private is: IconService,
         private fs: FnService,
         private log: LogService,
-        private tts: TooltipService
     ) {
         this.log.debug('ButtonService constructed');
     }
diff --git a/web/gui2/src/main/webapp/app/fw/widget/table-theme.css b/web/gui2/src/main/webapp/app/fw/widget/table-theme.css
new file mode 100644
index 0000000..1edfab0
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/widget/table-theme.css
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+/* ------ for summary-list tables (theme) ------ */
+
+.light div.summary-list, .table-header th {
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+.light div.summary-list, td {
+    color: #3c3a3a;
+}
+
+.light div.summary-list, tr:nth-child(even) {
+    background-color: #f4f4f4;
+}
+.light div.summary-list, tr:nth-child(odd) {
+    background-color: #fbfbfb;
+}
+
+.light div.summary-list, tr.selected {
+    background-color: #dbeffc !important;
+}
+
+
+.light div.summary-list, tr.data-change {
+    background-color: #FDFFDC;
+}
+
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+.light .ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #e0dfd6;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+.light .ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.light .ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+.light .ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
+    fill: #518ecc;
+}
+.light .ctrl-btns div.current-view svg.embeddedIcon g.icon use {
+    fill: white;
+}
+
+/* REFRESH */
+.light .ctrl-btns div.refresh svg.embeddedIcon g.icon use {
+    fill: #cdeff2;
+}
+.light .ctrl-btns div.refresh:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+.light .ctrl-btns div.refresh.active svg.embeddedIcon g.icon use {
+    fill: #009fdb;
+}
+.light .ctrl-btns div.refresh.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark div.summary-list .table-header td {
+    background-color: #222222;
+    color: #cccccc;
+}
+
+.dark div.summary-list td {
+    /* note: don't put background-color here */
+    color: #cccccc;
+}
+.dark div.summary-list tr.no-data td {
+    background-color: #333333;
+}
+
+.dark div.summary-list tr:nth-child(even) {
+    background-color: #333333;
+}
+.dark div.summary-list tr:nth-child(odd) {
+    background-color: #3a3a3a;
+}
+
+.dark div.summary-list tr.selected {
+    background-color: #304860 !important;
+}
+
+
+.dark div.summary-list tr.data-change {
+    background-color: #423708;
+}
+
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+.dark .ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #444444;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+.dark .ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.dark .ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+.dark .ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
+    fill: #518ecc;
+}
+.dark .ctrl-btns div.current-view svg.embeddedIcon g.icon use {
+    fill: #dddddd;
+}
+
+/* REFRESH */
+.dark .ctrl-btns div.refresh svg.embeddedIcon g.icon use {
+    fill: #364144;
+}
+.dark .ctrl-btns div.refresh:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+.dark .ctrl-btns div.refresh.active svg.embeddedIcon g.icon use {
+    fill: #0074a6;
+}
+.dark .ctrl-btns div.refresh.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/table.css b/web/gui2/src/main/webapp/app/fw/widget/table.css
new file mode 100644
index 0000000..3b761f6
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/widget/table.css
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+
+/* ------ for summary-list tables (layout) ------ */
+
+div.summary-list {
+    margin: 0 20px 16px 10px;
+    font-size: 10pt;
+    border-spacing: 0;
+}
+
+div.summary-list table {
+    border-collapse: collapse;
+    table-layout: fixed;
+    empty-cells: show;
+    margin: 0;
+}
+
+div.summary-list div.table-body {
+    overflow-y: scroll;
+}
+
+div.summary-list div.table-body::-webkit-scrollbar {
+    display: none;
+}
+
+div.summary-list tr.no-data td {
+    text-align: center;
+    font-style: italic;
+}
+
+
+/* highlighting */
+div.summary-list tr {
+    transition: background-color 500ms;
+}
+
+div.summary-list td {
+    padding: 4px;
+    text-align: left;
+    word-wrap: break-word;
+    font-size: 10pt;
+}
+
+div.summary-list td.table-icon {
+    width: 42px;
+    padding-top: 4px;
+    padding-bottom: 0px;
+    padding-left: 4px;
+    text-align: center;
+}
+
+div.summary-list .table-header th {
+    font-weight: bold;
+    font-variant: small-caps;
+    text-transform: uppercase;
+    font-size: 10pt;
+    padding-top: 8px;
+    padding-bottom: 8px;
+    letter-spacing: 0.02em;
+    cursor: pointer;
+}
+
+/* rows are selectable */
+div.summary-list .table-body td {
+    cursor: pointer;
+}
+
+/* Tabular view controls */
+
+div.tabular-header .search {
+    margin: 0 0 10px 10px;
+}
+
+
+div.tabular-header div.ctrl-btns {
+    display: inline-block;
+    float: right;
+    height: 44px;
+    margin-top: 24px;
+    margin-right: 20px;
+    position: absolute;
+    right: 0px;
+}
+
+div.tabular-header div.ctrl-btns div {
+    display: inline-block;
+    cursor: pointer;
+}
+
+div.tabular-header div.ctrl-btns div.separator  {
+    width: 0;
+    height: 40px;
+    padding: 0;
+    border-right: 1px solid #c7c7c0;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/tablebase.ts b/web/gui2/src/main/webapp/app/fw/widget/tablebase.ts
new file mode 100644
index 0000000..807a014
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/widget/tablebase.ts
@@ -0,0 +1,215 @@
+/*
+ * 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 { LoadingService } from '../layer/loading.service';
+import { LogService } from '../../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+const REFRESH_INTERVAL = 2000;
+
+/**
+ * Base model of table view - implemented by Table components
+ */
+export interface TableBase {
+    annots: TableAnnots;
+    autoRefresh: boolean;
+    autoRefreshTip: string;
+    changedData: any;
+    payloadParams: any;
+    selId: string;
+    sortParams: any;
+    tableData: any[];
+    toggleRefresh(): void;
+    selectCallback(event: any, selRow: any): void;
+    parentSelCb(event: any, selRow: any): void;
+    sortCallback(): void;
+    responseCallback(): void;
+}
+
+interface TableAnnots {
+    noRowsMsg: string;
+}
+
+/**
+ * A model of data returned in a TableResponse
+ *
+ * There is an interface extending from this one in the parent component
+ */
+export interface TableResponse {
+    annots: any;
+    // There will be other parts to the response depending on table type
+    // Expect one called tag+'s' e.g. devices or apps
+}
+
+/**
+ * ONOS GUI -- Widget -- Table Base class
+ */
+export class TableBaseImpl implements TableBase {
+    // attributes from the interface
+    public annots: TableAnnots;
+    autoRefresh: boolean = true;
+    autoRefreshTip: string = 'Toggle auto refresh'; // TODO: get LION string
+    changedData: string[] = [];
+    payloadParams: any;
+    selId: string = undefined;
+    sortParams: any;
+    tableData: any[] = [];
+    toggleRefresh; // Function
+    selectCallback; // Function
+    parentSelCb = null;
+    sortCallback; // Function
+    responseCallback; // Function
+
+    private root: string;
+    private req: string;
+    private resp: string;
+    private refreshPromise: any = null;
+    private handlers: string[] = [];
+
+    constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+        protected tag: string,
+        protected idKey: string = 'id',
+        protected query: string = '',
+        protected selCb = () => ({}) // Function
+    ) {
+        this.root = tag + 's';
+        this.req = tag + 'DataRequest';
+        this.resp = tag + 'DataResponse';
+
+        this.sortCallback = this.requestTableData;
+        this.selectCallback = this.rowSelectionCb;
+        this.toggleRefresh = () => {
+            this.autoRefresh = !this.autoRefresh;
+            this.autoRefresh ? this.startRefresh() : this.stopRefresh();
+        };
+    }
+
+    init() {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.resp, (data) => this.tableDataResponseCb(data)]
+        ]));
+        this.handlers.push(this.resp);
+
+        this.annots = <TableAnnots>{
+            noRowsMsg: ''
+        };
+
+        // Now send the WebSocket request and make it repeat every 2 seconds
+        this.requestTableData();
+        this.startRefresh();
+
+        this.log.debug('TableBase initialized');
+    }
+
+    destroy() {
+        this.wss.unbindHandlers(this.handlers);
+        this.stopRefresh();
+        this.ls.stop();
+    }
+
+    /**
+     * A callback that executes when the table data that was requested
+     * on WebSocketService arrives.
+     *
+     * Happens every 2 seconds
+     */
+    tableDataResponseCb(data: TableResponse) {
+        this.ls.stop();
+
+        const newTableData: any[] = Array.from(data[this.root]);
+        this.annots.noRowsMsg = data.annots.no_rows_msg;
+
+        // If the onResp() function is set then call it
+        if (this.responseCallback) {
+            this.responseCallback(data);
+        }
+        this.changedData = [];
+
+        // checks if data changed for row flashing
+        if (JSON.stringify(newTableData) !== JSON.stringify(this.tableData)) {
+            this.log.debug('table data has changed');
+            const oldTableData: any[] = this.tableData;
+            this.tableData = [ ...newTableData ]; // ES6 spread syntax
+            // only flash the row if the data already exists
+            if (oldTableData.length > 0) {
+                for (const idx in newTableData) {
+                    if (!this.fs.containsObj(oldTableData, newTableData[idx])) {
+                        this.changedData.push(newTableData[idx][this.idKey]);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Table Data Request
+     */
+    requestTableData() {
+        const p = Object.assign({}, this.sortParams, this.payloadParams, this.query);
+
+        // Allow it to sit in pending events
+        if (this.wss.isConnected()) {
+            if (this.fs.debugOn('table')) {
+                this.log.debug('Table data REQUEST:', this.req, p);
+            }
+            this.wss.sendEvent(this.req, p);
+            this.ls.start();
+        }
+    }
+
+    /**
+     * Row Selected
+     */
+    rowSelectionCb(event: any, selRow: any) {
+        const selId: string = selRow[this.idKey];
+        this.selId = (this.selId === selId) ? undefined : selId;
+        if (this.parentSelCb) {
+            this.log.debug('Parent called on Row', selId, 'selected');
+            this.parentSelCb(event, selRow);
+        }
+    }
+
+    /**
+     * autoRefresh functions
+     */
+    startRefresh() {
+        this.refreshPromise =
+            setInterval(() => {
+                if (!this.ls.waiting()) {
+                    if (this.fs.debugOn('table')) {
+                        this.log.debug('Refreshing ' + this.root + ' page');
+                    }
+                    this.requestTableData();
+                }
+            }, REFRESH_INTERVAL);
+    }
+
+    stopRefresh() {
+        if (this.refreshPromise) {
+            clearInterval(this.refreshPromise);
+            this.refreshPromise = null;
+        }
+    }
+
+    isChanged(id: string): boolean {
+        return (this.fs.inArray(id, this.changedData) === -1) ? false : true;
+    }
+}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/tablebuilder.service.ts b/web/gui2/src/main/webapp/app/fw/widget/tablebuilder.service.ts
deleted file mode 100644
index 6c804d1..0000000
--- a/web/gui2/src/main/webapp/app/fw/widget/tablebuilder.service.ts
+++ /dev/null
@@ -1,37 +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 { LoadingService } from '../layer/loading.service';
-import { LogService } from '../../log.service';
-import { WebSocketService } from '../remote/websocket.service';
-
-/**
- * ONOS GUI -- Widget -- Table Builder Service
- */
-@Injectable()
-export class TableBuilderService {
-
-  constructor(
-    private fs: FnService,
-    private ls: LoadingService,
-    private log: LogService,
-    private wss: WebSocketService
-  ) {
-    this.log.debug('TableBuilderService constructed');
-  }
-
-}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/tooltip.directive.ts b/web/gui2/src/main/webapp/app/fw/widget/tooltip.directive.ts
deleted file mode 100644
index 92f8ae3..0000000
--- a/web/gui2/src/main/webapp/app/fw/widget/tooltip.directive.ts
+++ /dev/null
@@ -1,35 +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 { Directive } from '@angular/core';
-import { FnService } from '../util/fn.service';
-import { LogService } from '../../log.service';
-
-/**
- * ONOS GUI -- Widget -- Tooltip Directive
- */
-@Directive({
-  selector: '[onosTooltip]'
-})
-export class TooltipDirective {
-
-    constructor(
-        private fs: FnService,
-        private log: LogService
-    ) {
-        this.log.debug('TooltipDirective constructed');
-    }
-
-}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/widget.module.ts b/web/gui2/src/main/webapp/app/fw/widget/widget.module.ts
index 2e7a0ea..02393a9 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/widget.module.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/widget.module.ts
@@ -23,11 +23,8 @@
 import { ButtonService } from './button.service';
 import { ChartBuilderService } from './chartbuilder.service';
 import { ListService } from './list.service';
-import { TableBuilderService } from './tablebuilder.service';
 import { TableDetailService } from './tabledetail.service';
 import { ToolbarService } from './toolbar.service';
-import { TooltipService } from './tooltip.service';
-import { TooltipDirective } from './tooltip.directive';
 import { SortableHeaderDirective } from './sortableheader.directive';
 import { TableResizeDirective } from './tableresize.directive';
 import { FlashChangesDirective } from './flashchanges.directive';
@@ -42,7 +39,6 @@
     // It's enough to import them in the OnosModule
   ],
   declarations: [
-    TooltipDirective,
     SortableHeaderDirective,
     TableResizeDirective,
     FlashChangesDirective
@@ -51,9 +47,7 @@
     ButtonService,
     ChartBuilderService,
     ListService,
-    TableBuilderService,
     TableDetailService,
-    TooltipService,
     ToolbarService
   ]
 })
diff --git a/web/gui2/src/main/webapp/app/onos.component.css b/web/gui2/src/main/webapp/app/onos.component.css
index 60933d8..e57f958 100644
--- a/web/gui2/src/main/webapp/app/onos.component.css
+++ b/web/gui2/src/main/webapp/app/onos.component.css
@@ -27,8 +27,4 @@
 #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 42d2bdd..18297ed 100644
--- a/web/gui2/src/main/webapp/app/onos.component.html
+++ b/web/gui2/src/main/webapp/app/onos.component.html
@@ -1,4 +1,4 @@
-<div style="text-align:center" onosDetectBrowser>
+<div id="view" onosDetectBrowser>
     <onos-mast></onos-mast>
     <onos-nav></onos-nav>
     <onos-veil #veil></onos-veil>
diff --git a/web/gui2/src/main/webapp/app/onos.component.ts b/web/gui2/src/main/webapp/app/onos.component.ts
index bbec955..1246314 100644
--- a/web/gui2/src/main/webapp/app/onos.component.ts
+++ b/web/gui2/src/main/webapp/app/onos.component.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
 import { LionService } from './fw/util/lion.service';
 import { LogService } from './log.service';
 import { KeyService } from './fw/util/key.service';
@@ -62,7 +62,7 @@
   templateUrl: './onos.component.html',
   styleUrls: ['./onos.component.css', './onos.common.css']
 })
-export class OnosComponent implements OnInit {
+export class OnosComponent implements OnInit, OnDestroy {
     public title = 'onos';
 
     // view ID to help page url map.. injected via the servlet
@@ -110,14 +110,19 @@
 
         this.onos.viewMap = this.viewMap;
 
-//        this.wss.createWebSocket({
-//            wsport: Window.location.search().wsport
-//        });
-
         // TODO: Enable this   this.saucy(this.ee, this.ks);
         this.log.debug('OnosComponent initialized');
     }
 
+    ngOnDestroy() {
+        if (this.wss.isConnected()) {
+            this.log.debug('Stopping Web Socket connection');
+            this.wss.closeWebSocket();
+        }
+
+        this.log.debug('OnosComponent destroyed');
+    }
+
     saucy(ee, ks) {
         const map = ee.genMap(sauce);
         Object.keys(map).forEach(function (k) {
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps.component.html b/web/gui2/src/main/webapp/app/view/apps/apps.component.html
index 90f9a16..6397f63 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps.component.html
+++ b/web/gui2/src/main/webapp/app/view/apps/apps.component.html
@@ -1,3 +1,100 @@
-<div id="ov-app">
-    <p>apps works!</p>
-</div>
\ No newline at end of file
+<div id="ov-app" filedrop on-file-drop="appDropped()">
+    <div class="tabular-header">
+        <h2>
+            {{lionFn('title_apps')}}
+            ({{ tableData.length }}
+            {{ lionFn('total') }})
+        </h2>
+        <div class="ctrl-btns">
+            <div class="refresh" (click)="toggleRefresh()">
+                <onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}"
+                           iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
+            </div>
+            <div class="separator"></div>
+
+            <!--<form id="inputFileForm">-->
+                <!--<input id="uploadFile"-->
+                       <!--type="file" size="50" accept=".oar,.jar"-->
+                       <!--file-model="appFile">-->
+            <!--</form>-->
+
+            <div class="active" trigger-form>
+                <onos-icon classes="{{ 'active upload' }}"
+                        iconId="upload" iconSize="42" toolTip="{{ uploadTip }}"></onos-icon>
+            </div>
+            <div (click)="appAction('activate')">
+                <onos-icon classes="{{ ctrlBtnState.installed?'active play':'play' }}"
+                           iconId="play" iconSize="42" toolTip="{{ activateTip }}"></onos-icon>
+            </div>
+            <div (click)="appAction('deactivate')">
+                <onos-icon classes="{{ ctrlBtnState.active?'active stop':'stop' }}"
+                        iconId="stop" iconSize="42" toolTip="{{ deactivateTip }}"></onos-icon>
+            </div>
+            <div (click)="appAction('uninstall')">
+                 <!--[ngClass]="{active: ctrlBtnState.selection}">-->
+                <!--tooltip tt-msg="uninstallTip"-->
+                <onos-icon classes="{{ ctrlBtnState.selection?'active garbage':'garbage' }}"
+                        iconId="garbage" iconSize="42" toolTip="{{ uninstallTip }}"></onos-icon>
+            </div>
+            <div (click)="downloadApp()">
+                <onos-icon classes="{{ ctrlBtnState.selection?'active download':'download' }}"
+                        iconId="download" iconSize="42" toolTip="{{ downloadTip }}"></onos-icon>
+            </div>
+        </div>
+
+        <!--<div class="search">-->
+            <!--<input type="text" ng-model="queryTxt" placeholder="Search"/>-->
+            <!--<select ng-model="queryBy">-->
+                <!--<option value="" disabled>Search By</option>-->
+                <!--<option value="$">All Fields</option>-->
+                <!--<option value="title">{{lionFn('title')}}</option>-->
+                <!--<option value="id">{{lionFn('app_id')}}</option>-->
+                <!--<option value="version">{{lionFn('version')}}</option>-->
+                <!--<option value="category">{{lionFn('category')}}</option>-->
+                <!--<option value="apporiginName">{{lionFn('origin')}}</option>-->
+
+            <!--</select>-->
+        <!--</div>-->
+
+
+    </div>
+
+    <div class="summary-list" onos-table-resize>
+        <table onos-flash-changes id-prop="id" width="100%">
+            <tr class="table-header">
+                <th colId="state" class="table-icon" sortable></th>
+                <th colId="icon" class="table-icon"></th>
+                <th colId="title" [ngClass]="{width: '340'}" sortable> {{lionFn('title')}} </th>
+                <th colId="id" [ngClass]="{width: '320px'}"sortable> {{lionFn('app_id')}} </th>
+                <th colId="version" [ngClass]="{width: '140px'}"sortable> {{lionFn('version')}} </th>
+                <th colId="category" [ngClass]="{width: '136px'}"sortable> {{lionFn('category')}} </th>
+                <th colId="origin" sortable> {{lionFn('origin')}} </th>
+            </tr>
+
+            <tr *ngIf="tableData.length === 0" class="no-data">
+                <td colspan="5">
+                    {{annots.no_rows_msg}}
+                </td>
+            </tr>
+            <!--&lt;!&ndash;TODO: Add back in  | filter:queryFilter&ndash;&gt;-->
+            <tr class="table-body" *ngFor="let app of tableData; trackBy $index"
+                (click)="selectCallback($event, app)"
+                [ngClass]="{selected: app.id === selId, 'data-change': isChanged(app.id)}">
+                <td class="table-icon">
+                    <onos-icon iconId="{{app._iconid_state}}"></onos-icon>
+                </td>
+                <td class="table-icon">
+                    <!--<img data-ng-src="./rs/applications/{{app.icon}}/icon"-->
+                                            <!--height="24px" width="24px" />-->
+                </td>
+                <td>{{ app.title }}</td>
+                <td>{{ app.id }}</td>
+                <td>{{ app.version }}</td>
+                <td>{{ app.category }}</td>
+                <td>{{ app.origin }}</td>
+            </tr>
+        </table>
+
+    </div>
+
+</div>
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps.component.ts b/web/gui2/src/main/webapp/app/view/apps/apps.component.ts
index 5eaa38f..b2bb38b 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps.component.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps.component.ts
@@ -13,46 +13,235 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
 import { DialogService } from '../../fw/layer/dialog.service';
 import { FnService } from '../../fw/util/fn.service';
 import { IconService } from '../../fw/svg/icon.service';
 import { KeyService } from '../../fw/util/key.service';
 import { LionService } from '../../fw/util/lion.service';
+import { LoadingService } from '../../fw/layer/loading.service';
 import { LogService } from '../../log.service';
 import { PanelService } from '../../fw/layer/panel.service';
-import { TableBuilderService } from '../../fw/widget/tablebuilder.service';
+import { TableBaseImpl, TableResponse } from '../../fw/widget/tablebase';
 import { UrlFnService } from '../../fw/remote/urlfn.service';
 import { WebSocketService } from '../../fw/remote/websocket.service';
 
+const INSTALLED = 'INSTALLED';
+const ACTIVE = 'ACTIVE';
+const appMgmtReq = 'appManagementRequest';
+const topPdg = 60;
+const panelWidth = 540;
+const pName = 'application-details-panel';
+const detailsReq = 'appDetailsRequest';
+const detailsResp = 'appDetailsResponse';
+const fileUploadUrl = 'applications/upload';
+const activateOption = '?activate=true';
+const appUrlPrefix = 'rs/applications/';
+const iconUrlSuffix = '/icon';
+const downloadSuffix = '/download';
+const dialogId = 'app-dialog';
+const dialogOpts = {
+    edge: 'right',
+    width: 400,
+};
+const strongWarning = {
+    'org.onosproject.drivers': true,
+};
+const propOrder = ['id', 'state', 'category', 'version', 'origin', 'role'];
+
+interface AppTableResponse extends TableResponse {
+    apps: Apps[];
+}
+
+interface Apps {
+    category: string;
+    desc: string;
+    features: string;
+    icon: string;
+    id: string;
+    origin: string;
+    permissions: string;
+    readme: string;
+    required_apps: string;
+    role: string;
+    state: string;
+    title: string;
+    url: string;
+    version: string;
+    _iconid_state: string;
+}
+
+interface CtrlBtnState {
+    installed: boolean;
+    selection: string;
+    active: boolean;
+}
+
 /**
  * ONOS GUI -- Apps View Component
  */
 @Component({
   selector: 'onos-apps',
   templateUrl: './apps.component.html',
-  styleUrls: ['./apps.component.css']
+  styleUrls: [
+    './apps.component.css', './apps.theme.css',
+    '../../fw/widget/table.css', '../../fw/widget/table-theme.css'
+    ]
 })
-export class AppsComponent implements OnInit {
+export class AppsComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    // deferred localization strings
+    lionFn; // Function
+    warnDeactivate: string;
+    warnOwnRisk: string;
+    friendlyProps: string[];
+    ctrlBtnState: CtrlBtnState;
+    detailsPanel: any;
 
     constructor(
-        private fs: FnService,
+        protected fs: FnService,
         private ds: DialogService,
         private is: IconService,
         private ks: KeyService,
-        private ls: LionService,
-        private log: LogService,
+        private lion: LionService,
+        protected ls: LoadingService,
+        protected log: LogService,
         private ps: PanelService,
-        private tbs: TableBuilderService,
         private ufs: UrlFnService,
-        private wss: WebSocketService,
-        private window: Window
+        protected wss: WebSocketService,
+        private window: Window,
     ) {
-        this.log.debug('AppsComponent constructed');
+        super(fs, null, log, wss, 'app');
+        this.responseCallback = this.appResponseCb;
+        this.sortParams = {
+            firstCol: 'state',
+            firstDir: 'desc',
+            secondCol: 'title',
+            secondDir: 'asc',
+        };
+        // We want doLion() to be called only after the Lion service is populated (from the WebSocket)
+        this.lion.loadCb = (() => this.doLion());
+        this.ctrlBtnState = <CtrlBtnState>{
+            installed: false,
+            active: false
+        };
+        if (this.lion.ubercache.length === 0) {
+            this.lionFn = this.dummyLion;
+        } else {
+            this.doLion();
+        }
     }
 
     ngOnInit() {
-        this.log.debug('AppsComponent initialized');
+        this.init();
+        this.log.debug('AppComponent initialized');
     }
 
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('AppComponent destroyed');
+    }
+
+    /**
+     * The callback called when App data returns from WSS
+     */
+    appResponseCb(data: AppTableResponse) {
+        this.log.debug('App response received for ', data.apps.length, 'apps');
+    }
+
+    refreshCtrls() {
+        let row;
+        let rowIdx;
+        if (this.ctrlBtnState.selection) {
+            rowIdx = this.fs.find(this.selId, this.tableData);
+            row = rowIdx >= 0 ? this.tableData[rowIdx] : null;
+
+            this.ctrlBtnState.installed = row && row.state === INSTALLED;
+            this.ctrlBtnState.active = row && row.state === ACTIVE;
+        } else {
+            this.ctrlBtnState.installed = false;
+            this.ctrlBtnState.active = false;
+        }
+    }
+
+    createConfirmationText(action, itemId) {
+//        let content = this.ds.createDiv();
+//        content.append('p').text(this.lionFn(action) + ' ' + itemId);
+//        if (strongWarning[itemId]) {
+//            content.append('p').html(
+//                this.fs.sanitize(this.warnDeactivate) +
+//                '<br>' +
+//                this.fs.sanitize(this.warnOwnRisk)
+//            ).classed('strong', true);
+//        }
+//        return content;
+    }
+
+    confirmAction(action): void {
+        const itemId = this.selId;
+        const spar = this.sortParams;
+
+        function dOk() {
+            this.log.debug('Initiating', action, 'of', itemId);
+            this.wss.sendEvent(appMgmtReq, {
+                action: action,
+                name: itemId,
+                sortCol: spar.sortCol,
+                sortDir: spar.sortDir,
+            });
+            if (action === 'uninstall') {
+                this.detailsPanel.hide();
+            } else {
+                this.wss.sendEvent(detailsReq, { id: itemId });
+            }
+        }
+
+        function dCancel() {
+            this.log.debug('Canceling', action, 'of', itemId);
+        }
+
+//        this.ds.openDialog(dialogId, dialogOpts)
+//            .setTitle(this.lionFn('dlg_confirm_action'))
+//            .addContent(this.createConfirmationText(action, itemId))
+//            .addOk(dOk)
+//            .addCancel(dCancel)
+//            .bindKeys();
+    }
+
+    appAction(action) {
+        if (this.ctrlBtnState.selection) {
+            this.confirmAction(action);
+        }
+    }
+
+    downloadApp() {
+        if (this.ctrlBtnState.selection) {
+            (<any>this.window).location = appUrlPrefix + this.selId + downloadSuffix;
+        }
+    }
+
+    /**
+     * Read the LION bundle for App - this should replace the dummyLion implementation
+     * of lionFn with a function from the LION Service
+     */
+    doLion() {
+        this.lionFn = this.lion.bundle('core.view.App');
+
+        this.warnDeactivate = this.lionFn('dlg_warn_deactivate');
+        this.warnOwnRisk = this.lionFn('dlg_warn_own_risk');
+
+        this.friendlyProps = [
+            this.lionFn('app_id'), this.lionFn('state'),
+            this.lionFn('category'), this.lionFn('version'),
+            this.lionFn('origin'), this.lionFn('role'),
+        ];
+    }
+
+    /**
+     * A dummy implementation of the lionFn until the response is received and the LION
+     * bundle is received from the WebSocket
+     */
+    dummyLion(key: string): string {
+        return '%' + key + '%';
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps.module.ts b/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
index 3083b55..3e3d5c4 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
@@ -17,6 +17,7 @@
 import { CommonModule } from '@angular/common';
 import { AppsComponent } from './apps.component';
 import { TriggerFormDirective } from './triggerform.directive';
+import { SvgModule } from '../../fw/svg/svg.module';
 
 /**
  * ONOS GUI -- Apps View Module
@@ -30,7 +31,8 @@
         AppsComponent
     ],
     imports: [
-        CommonModule
+        CommonModule,
+        SvgModule
     ],
     declarations: [
         AppsComponent,
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.css b/web/gui2/src/main/webapp/app/view/device/device.component.css
index c578112..4d8454d 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.css
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.css
@@ -17,12 +17,15 @@
 /*
  ONOS GUI -- Device View (layout) -- CSS file
  */
+#ov-device .tabular-header {
+    text-align: left;
+}
 
 #ov-device h2 {
     display: inline-block;
 }
 
-#ov-device div.ctrl-btns {
+#ov-device, div.ctrl-btns {
 }
 
 
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.html b/web/gui2/src/main/webapp/app/view/device/device.component.html
index febc99a..e2d3fda 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.html
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.html
@@ -1,3 +1,99 @@
 <div id="ov-device">
-    <p>device works!</p>
+    <div class="tabular-header">
+        <h2>Devices ({{ tableData.length }} total)</h2>
+        <div class="ctrl-btns">
+            <div class="refresh" (click)="toggleRefresh()">
+                <!-- See icon.theme.css for the defintions of the classes active and refresh-->
+                <onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}"
+                           iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
+            </div>
+            <div class="separator"></div>
+
+            <div>
+                <onos-icon classes="{{ selId ? 'current-view':undefined }}"
+                           iconId="deviceTable" iconSize="42"></onos-icon>
+            </div>
+
+            <div routerLink="/flow" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                           iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/port" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/group" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/meter" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/pipeconf" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
+            </div>
+        </div>
+    </div>
+
+    <div class="summary-list" onos-table-resize>
+        <table onos-flash-changes id-prop="id" width="100%">
+            <tr class="table-header">
+                <th colId="available" class="table-icon" sortable></th>
+                <th colId="type" class="table-icon"></th>
+                <th colId="name" sortable>Friendly Name </th>
+                <th colId="id" sortable>Device ID </th>
+                <th colId="masterid" [ngClass]="{width: '130px'}" sortable>Master </th>
+                <th colId="num_ports" [ngClass]="{width: '70px'}" sortable>Ports </th>
+                <th colId="mfr" sortable>Vendor </th>
+                <th colId="hw" sortable>H/W Version </th>
+                <th colId="sw" sortable>S/W Version </th>
+                <th colId="protocol" [ngClass]="{width: '100px'}" sortable>Protocol </th>
+            </tr>
+
+            <tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
+                <td colspan="9">{{ annots.noRowsMsg }}</td>
+            </tr>
+
+
+            <tr class="table-body" *ngFor="let dev of tableData; trackBy $index"
+                (click)="selectCallback($event, dev)"
+                [ngClass]="{selected: dev.id === selId, 'data-change': isChanged(dev.id)}">
+                <td class="table-icon">
+                    <!--[ngClass]="{width: devAvail.getBBox().width}"-->
+                    <onos-icon iconId="{{dev._iconid_available}}"></onos-icon>
+                </td>
+                <td class="table-icon">
+                    <onos-icon iconId="{{dev._iconid_type}}"></onos-icon>
+                </td>
+                <td>{{ dev.name }}</td>
+                <td>{{ dev.id }}</td>
+                <td>{{ dev.masterid }}</td>
+                <td>{{ dev.num_ports }}</td>
+                <td>{{ dev.mfr }}</td>
+                <td>{{ dev.hw }}</td>
+                <td>{{ dev.sw }}</td>
+                <td>{{ dev.protocol }}</td>
+            </tr>
+        </table>
+    </div>
+    <small>
+    <p>TODO (21 Jun 18): Add in:</p>
+    <ul>
+        <li>Scrolling for long lists of devices</li>
+        <li>Sorting by column</li>
+        <li>Left align header columns</li>
+        <li>Move tooltip to underneath icon</li>
+        <li>Correct width and icon colour of active and device icon columns</li>
+        <li>Add device details panel</li>
+        <li>Add more unit tests</li>
+        <li>Make icon for #undefined work (e.g. for device type olt or unknown)</li>
+        <li>Change loading service to fade in and out and have a threshold of </li>
+    </ul>
+    </small>
 </div>
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 6929f9b..f4a6fbe 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
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
 import { DetailsPanelService } from '../../fw/layer/detailspanel.service';
 import { FnService } from '../../fw/util/fn.service';
 import { IconService } from '../../fw/svg/icon.service';
@@ -23,42 +23,78 @@
 import { MastService } from '../../fw/mast/mast.service';
 import { NavService } from '../../fw/nav/nav.service';
 import { PanelService } from '../../fw/layer/panel.service';
-import { TableBuilderService } from '../../fw/widget/tablebuilder.service';
+import { TableBaseImpl, TableResponse } from '../../fw/widget/tablebase';
 import { TableDetailService } from '../../fw/widget/tabledetail.service';
 import { WebSocketService } from '../../fw/remote/websocket.service';
 
+interface DeviceTableResponse extends TableResponse {
+    devices: Device[];
+}
+
+interface Device {
+    available: boolean;
+    chassisid: string;
+    hw: string;
+    id: string;
+    masterid: string;
+    mfr: string;
+    name: string;
+    num_ports: number;
+    protocol: string;
+    serial: string;
+    sw: string;
+    _iconid_available: string;
+    _iconid_type: string;
+}
+
+
 /**
  * ONOS GUI -- Device View Component
  */
 @Component({
   selector: 'onos-device',
   templateUrl: './device.component.html',
-  styleUrls: ['./device.component.css']
+  styleUrls: ['./device.component.css', './device.theme.css', '../../fw/widget/table.css', '../../fw/widget/table-theme.css']
 })
-export class DeviceComponent implements OnInit {
+export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    // TODO: Update for LION
+    flowTip = 'Show flow view for selected device';
+    portTip = 'Show port view for selected device';
+    groupTip = 'Show group view for selected device';
+    meterTip = 'Show meter view for selected device';
+    pipeconfTip = 'Show pipeconf view for selected device';
 
     constructor(
         private dps: DetailsPanelService,
-        private fs: FnService,
+        protected fs: FnService,
+        protected ls: LoadingService,
         private is: IconService,
         private ks: KeyService,
-        private log: LogService,
+        protected log: LogService,
         private mast: MastService,
         private nav: NavService,
         private ps: PanelService,
-        private tbs: TableBuilderService,
         private tds: TableDetailService,
-        private wss: WebSocketService,
-        private ls: LoadingService, // TODO: Remove - already added through tbs
-        private window: Window
+        protected wss: WebSocketService,
+        private window: Window,
     ) {
-        this.log.debug('DeviceComponent constructed');
+        super(fs, ls, log, wss, 'device');
+        this.responseCallback = this.deviceResponseCb;
     }
 
     ngOnInit() {
+        this.init();
         this.log.debug('DeviceComponent initialized');
-        // TODO: Remove this - it's only for demo purposes
-//        this.ls.startAnim();
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('DeviceComponent destroyed');
+    }
+
+    deviceResponseCb(data: DeviceTableResponse) {
+        this.log.debug('Device response received for ', data.devices.length, 'devices');
     }
 
 }
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 8f0f351..99b15bd 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
@@ -15,9 +15,11 @@
  */
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
+import { RouterModule, Routes } from '@angular/router';
 import { DeviceComponent } from './device.component';
 import { DeviceDetailsPanelDirective } from './devicedetailspanel.directive';
-import { RemoteModule } from '../../fw/remote/remote.module';
+import { SvgModule } from '../../fw/svg/svg.module';
+
 /**
  * ONOS GUI -- Device View Module
  */
@@ -27,7 +29,8 @@
   ],
   imports: [
     CommonModule,
-    RemoteModule
+    RouterModule,
+    SvgModule
   ],
   declarations: [
     DeviceComponent,
diff --git a/web/gui2/src/main/webapp/onos.theme.css b/web/gui2/src/main/webapp/onos.theme.css
index dc14c80..a2ac0aa 100644
--- a/web/gui2/src/main/webapp/onos.theme.css
+++ b/web/gui2/src/main/webapp/onos.theme.css
@@ -41,6 +41,10 @@
 
 #view h2 {
     color: #3c3a3a;
+    margin: 32px 0 4px 16px;
+    padding: 0;
+    font-size: 18pt;
+    font-weight: lighter;
 }
 
 a {
diff --git a/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
index 5c8d6b7..391b62c 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
@@ -33,8 +33,6 @@
 
 class MockGlyphService {}
 
-class MockWSock {}
-
 /**
  * ONOS GUI -- Remote -- Web Socket Service - Unit Tests
  */
@@ -103,7 +101,7 @@
             'noHandlersWarn', 'resetState',
             'createWebSocket', 'bindHandlers', 'unbindHandlers',
             'addOpenListener', 'removeOpenListener', 'sendEvent',
-            'setVeilDelegate', 'setLoadingDelegate'
+            'setVeilDelegate', 'setLoadingDelegate', 'isConnected', 'closeWebSocket'
         ])).toBeTruthy();
     });
 
@@ -228,9 +226,7 @@
     });
 
     it('should warn if no arguments, unbindHandlers', () => {
-        expect(wss.unbindHandlers(
-            new Map<string, (data) => void>([])
-        )).toBeNull();
+        expect(wss.unbindHandlers([])).toBeNull();
         expect(logServiceSpy.warn).toHaveBeenCalledWith(
             'WSS.unbindHandlers(): no event handlers'
         );
diff --git a/web/gui2/src/main/webapp/tests/app/fw/remote/wsock.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/remote/wsock.service.spec.ts
deleted file mode 100644
index 61d5ab4..0000000
--- a/web/gui2/src/main/webapp/tests/app/fw/remote/wsock.service.spec.ts
+++ /dev/null
@@ -1,41 +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 { TestBed, inject } from '@angular/core/testing';
-
-import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { WSock } from '../../../../app/fw/remote/wsock.service';
-
-/**
- * ONOS GUI -- Remote -- WSock Service - Unit Tests
- */
-describe('WSock', () => {
-    let log: LogService;
-
-    beforeEach(() => {
-        log = new ConsoleLoggerService();
-
-        TestBed.configureTestingModule({
-            providers: [WSock,
-                { provide: LogService, useValue: log },
-            ]
-        });
-    });
-
-    it('should be created', inject([WSock], (service: WSock) => {
-        expect(service).toBeTruthy();
-    }));
-});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/svg/icon.directive.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/svg/icon.directive.spec.ts
deleted file mode 100644
index 4c5c252..0000000
--- a/web/gui2/src/main/webapp/tests/app/fw/svg/icon.directive.spec.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- *  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.
- */
-import { TestBed, inject } from '@angular/core/testing';
-
-import { ElementRef } from '@angular/core';
-import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { IconDirective } from '../../../../app/fw/svg/icon.directive';
-import { IconService } from '../../../../app/fw/svg/icon.service';
-import { GlyphService } from '../../../../app/fw/svg/glyph.service';
-import { SvgUtilService } from '../../../../app/fw/svg/svgutil.service';
-import { FnService } from '../../../../app/fw//util/fn.service';
-import { ActivatedRoute, Router} from '@angular/router';
-
-class MockFnService {}
-
-class MockGlyphService {}
-
-class MockIconService {}
-
-/**
- * ONOS GUI -- SVG -- Icon Directive - Unit Tests
- */
-describe('IconDirective', () => {
-    let log: LogService;
-    const elementMock = <any>{ };
-
-    beforeEach(() => {
-        log = new ConsoleLoggerService();
-
-        TestBed.configureTestingModule({
-            providers: [ IconDirective,
-                { provide: FnService, useClass: MockFnService },
-                { provide: LogService, useValue: log },
-                { provide: ElementRef, useValue: elementMock },
-                { provide: GlyphService, useClass: MockGlyphService },
-                { provide: IconService, useClass: MockIconService },
-            ]
-        });
-    });
-
-    afterEach(() => {
-        log = null;
-    });
-
-    it('should create an instance', inject([IconDirective], (directive: IconDirective) => {
-        expect(directive).toBeTruthy();
-    }));
-});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
index 84b5f094..bf75091 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
@@ -235,7 +235,7 @@
             'isFirefox', 'parseDebugFlags',
             'debugOn', 'debug', 'find', 'inArray', 'removeFromArray',
             'isEmptyObject', 'cap', 'noPx', 'noPxStyle', 'endsWith',
-            'inEvilList', 'analyze', 'sanitize'
+            'inEvilList', 'analyze', 'sanitize', 'sameObjProps', 'containsObj'
 //            'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
 //            'eecode', 'noPx', 'noPxStyle', 'endsWith', 'addToTrie', 'removeFromTrie', 'trieLookup',
 //            'classNames', 'extend', 'sanitize'
diff --git a/web/gui2/src/main/webapp/tests/app/fw/widget/button.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/widget/button.service.spec.ts
index cc8113b..c3e501d 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/widget/button.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/widget/button.service.spec.ts
@@ -20,14 +20,11 @@
 import { ButtonService } from '../../../../app/fw/widget/button.service';
 import { FnService } from '../../../../app/fw/util/fn.service';
 import { IconService } from '../../../../app/fw/svg/icon.service';
-import { TooltipService } from '../../../../app/fw/widget/tooltip.service';
 
 class MockIconService {}
 
 class MockFnService {}
 
-class MockTooltipService {}
-
 /**
  * ONOS GUI -- Widget -- Button Service - Unit Tests
  */
@@ -42,7 +39,6 @@
                 { provide: LogService, useValue: log },
                 { provide: IconService, useClass: MockIconService },
                 { provide: FnService, useClass: MockFnService },
-                { provide: TooltipService, useClass: MockTooltipService },
             ]
         });
     });
diff --git a/web/gui2/src/main/webapp/tests/app/fw/widget/tablebuilder.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/widget/tablebuilder.service.spec.ts
deleted file mode 100644
index 1126d28..0000000
--- a/web/gui2/src/main/webapp/tests/app/fw/widget/tablebuilder.service.spec.ts
+++ /dev/null
@@ -1,54 +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 { TestBed, inject } from '@angular/core/testing';
-
-import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { TableBuilderService } from '../../../../app/fw/widget/tablebuilder.service';
-import { FnService } from '../../../../app/fw//util/fn.service';
-import { LoadingService } from '../../../../app/fw/layer/loading.service';
-import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
-
-class MockFnService {}
-
-class MockLoadingService {}
-
-class MockWebSocketService {}
-
-/*
- ONOS GUI -- Widget -- Table Builder Service - Unit Tests
- */
-describe('TableBuilderService', () => {
-    let log: LogService;
-
-    beforeEach(() => {
-        log = new ConsoleLoggerService();
-
-        TestBed.configureTestingModule({
-            providers: [TableBuilderService,
-                { provide: FnService, useClass: MockFnService },
-                { provide: LoadingService, useClass: MockLoadingService },
-                { provide: LogService, useValue: log },
-                { provide: WebSocketService, useClass: MockWebSocketService },
-
-            ]
-        });
-    });
-
-    it('should be created', inject([TableBuilderService], (service: TableBuilderService) => {
-        expect(service).toBeTruthy();
-    }));
-});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/widget/tooltip.directive.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/widget/tooltip.directive.spec.ts
deleted file mode 100644
index 7445a05..0000000
--- a/web/gui2/src/main/webapp/tests/app/fw/widget/tooltip.directive.spec.ts
+++ /dev/null
@@ -1,49 +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 { TestBed, inject } from '@angular/core/testing';
-
-import { TooltipDirective } from '../../../../app/fw/widget/tooltip.directive';
-import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { FnService } from '../../../../app/fw/util/fn.service';
-
-class MockFnService {}
-
-/**
- * ONOS GUI -- Widget -- Tooltip Directive - Unit Tests
- */
-describe('TooltipDirective', () => {
-    let log: LogService;
-
-    beforeEach(() => {
-        log = new ConsoleLoggerService();
-
-        TestBed.configureTestingModule({
-            providers: [ TooltipDirective,
-                { provide: FnService, useClass: MockFnService },
-                { provide: LogService, useValue: log },
-            ]
-        });
-    });
-
-    afterEach(() => {
-        log = null;
-    });
-
-    it('should create an instance', inject([TooltipDirective], (directive: TooltipDirective) => {
-        expect(directive).toBeTruthy();
-    }));
-});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/widget/tooltip.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/widget/tooltip.service.spec.ts
deleted file mode 100644
index 98a5a18..0000000
--- a/web/gui2/src/main/webapp/tests/app/fw/widget/tooltip.service.spec.ts
+++ /dev/null
@@ -1,45 +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 { TestBed, inject } from '@angular/core/testing';
-
-import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { TooltipService } from '../../../../app/fw/widget/tooltip.service';
-import { FnService } from '../../../../app/fw/util/fn.service';
-
-class MockFnService {}
-
-/**
- * ONOS GUI -- Widget -- Tooltip Service - Unit Tests
- */
-describe('TooltipService', () => {
-    let log: LogService;
-
-    beforeEach(() => {
-        log = new ConsoleLoggerService();
-
-        TestBed.configureTestingModule({
-            providers: [TooltipService,
-                { provide: LogService, useValue: log },
-                { provide: FnService, useClass: MockFnService },
-            ]
-        });
-    });
-
-    it('should be created', inject([TooltipService], (service: TooltipService) => {
-        expect(service).toBeTruthy();
-    }));
-});
diff --git a/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts b/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
index 7a15504..8dcb9e0 100644
--- a/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
@@ -59,6 +59,8 @@
 
 class MockKeyService {}
 
+class MockLionService {}
+
 class MockNavService {}
 
 class MockOnosService {}
@@ -73,6 +75,11 @@
 
 class MockVeilComponent {}
 
+class MockWebSocketService {
+    createWebSocket() {}
+    isConnected() { return false; }
+}
+
 /**
  * ONOS GUI -- Onos Component - Unit Tests
  */
@@ -81,6 +88,8 @@
     let fs: FnService;
     let ar: MockActivatedRoute;
     let windowMock: Window;
+    let fixture;
+    let app;
 
     beforeEach(async(() => {
         log = new ConsoleLoggerService();
@@ -117,6 +126,7 @@
                 { provide: GlyphService, useClass: MockGlyphService },
                 { provide: IconService, useClass: MockIconService },
                 { provide: KeyService, useClass: MockKeyService },
+                { provide: LionService, useClass: MockLionService },
                 { provide: LogService, useValue: log },
                 { provide: NavService, useClass: MockNavService },
                 { provide: OnosService, useClass: MockOnosService },
@@ -124,20 +134,22 @@
                 { provide: PanelService, useClass: MockPanelService },
                 { provide: SpriteService, useClass: MockSpriteService },
                 { provide: ThemeService, useClass: MockThemeService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
                 { provide: Window, useFactory: (() => windowMock ) },
             ]
         }).compileComponents();
+
+        fixture = TestBed.createComponent(OnosComponent);
+        app = fixture.componentInstance;
     }));
 
     it('should create the app', async(() => {
-        const fixture = TestBed.createComponent(OnosComponent);
-        const app = fixture.debugElement.componentInstance;
         expect(app).toBeTruthy();
     }));
 
-    it(`should have as title 'onos'`, async(() => {
-        const fixture = TestBed.createComponent(OnosComponent);
-        const app = fixture.debugElement.componentInstance;
-        expect(app.title).toEqual('onos');
-    }));
+//    it(`should have as title 'onos'`, async(() => {
+//        const fixture = TestBed.createComponent(OnosComponent);
+//        const app = fixture.componentInstance;
+//        expect(app.title).toEqual('onos');
+//    }));
 });
diff --git a/web/gui2/src/main/webapp/tests/app/view/apps/apps.component.spec.ts b/web/gui2/src/main/webapp/tests/app/view/apps/apps.component.spec.ts
index a998cb0..6682e41 100644
--- a/web/gui2/src/main/webapp/tests/app/view/apps/apps.component.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/view/apps/apps.component.spec.ts
@@ -14,72 +14,126 @@
  * limitations under the License.
  */
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
+import { ActivatedRoute, Params } from '@angular/router';
 import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
 import { AppsComponent } from '../../../../app/view/apps/apps.component';
 import { DialogService } from '../../../../app/fw/layer/dialog.service';
 import { FnService } from '../../../../app/fw/util/fn.service';
+import { IconComponent } from '../../../../app/fw/svg/icon/icon.component';
 import { IconService } from '../../../../app/fw/svg/icon.service';
 import { KeyService } from '../../../../app/fw/util/key.service';
 import { LionService } from '../../../../app/fw/util/lion.service';
+import { LoadingService } from '../../../../app/fw/layer/loading.service';
 import { PanelService } from '../../../../app/fw/layer/panel.service';
-import { TableBuilderService } from '../../../../app/fw/widget/tablebuilder.service';
+import { ThemeService } from '../../../../app/fw/util/theme.service';
 import { UrlFnService } from '../../../../app/fw/remote/urlfn.service';
 import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
+import { of } from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
 
 class MockDialogService {}
 
 class MockFnService {}
 
-class MockIconService {}
+class MockIconService {
+    loadIconDef() {}
+}
 
 class MockKeyService {}
 
-class MockLionService {}
+class MockLoadingService {
+    startAnim() {}
+    stop() {}
+    waiting() {}
+}
 
 class MockPanelService {}
 
 class MockTableBuilderService {}
 
+class MockThemeService {}
+
 class MockUrlFnService {}
 
-class MockWebSocketService {}
+class MockWebSocketService {
+    createWebSocket() {}
+    isConnected() { return false; }
+    unbindHandlers() {}
+    bindHandlers() {}
+}
 
 /**
  * ONOS GUI -- Apps View -- Unit Tests
  */
 describe('AppsComponent', () => {
-    let log: LogService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
     let component: AppsComponent;
     let fixture: ComponentFixture<AppsComponent>;
-    const windowMock = <any>{ location: <any> { hostname: 'localhost' } };
+    const bundleObj = {
+        'core.view.App': {
+            test: 'test1'
+        }
+    };
+    const mockLion = (key) =>  {
+        return bundleObj[key] || '%' + key + '%';
+    };
 
     beforeEach(async(() => {
-        log = new ConsoleLoggerService();
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({'debug': 'txrx'});
+
+        windowMock = <any>{
+            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, windowMock);
 
         TestBed.configureTestingModule({
-            declarations: [ AppsComponent ],
+            declarations: [ AppsComponent, IconComponent ],
             providers: [
                 { provide: DialogService, useClass: MockDialogService },
-                { provide: FnService, useClass: MockFnService },
+                { provide: FnService, useValue: fs },
                 { provide: IconService, useClass: MockIconService },
                 { provide: KeyService, useClass: MockKeyService },
-                { provide: LionService, useClass: MockLionService },
-                { provide: LogService, useValue: log },
+                { provide: LionService, useFactory: (() => {
+                        return {
+                            bundle: ((bundleId) => mockLion),
+                            ubercache: new Array()
+                        };
+                    })
+                },
+                { provide: LoadingService, useClass: MockLoadingService },
+                { provide: LogService, useValue: logSpy },
                 { provide: PanelService, useClass: MockPanelService },
-                { provide: TableBuilderService, useClass: MockTableBuilderService },
+                { provide: ThemeService, useClass: MockThemeService },
                 { provide: UrlFnService, useClass: MockUrlFnService },
                 { provide: WebSocketService, useClass: MockWebSocketService },
                 { provide: Window, useValue: windowMock },
             ]
         })
         .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
     }));
 
     beforeEach(() => {
         fixture = TestBed.createComponent(AppsComponent);
-        component = fixture.componentInstance;
+        component = fixture.debugElement.componentInstance;
         fixture.detectChanges();
     });
 
diff --git a/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts b/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
index 960d241..6d54ac4 100644
--- a/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
@@ -14,46 +14,50 @@
  * limitations under the License.
  */
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
+import { ActivatedRoute, Params } from '@angular/router';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
 import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
 import { DeviceComponent } from '../../../../app/view/device/device.component';
 
 import { DetailsPanelService } from '../../../../app/fw/layer/detailspanel.service';
 import { FnService, WindowSize } from '../../../../app/fw/util/fn.service';
 import { IconService } from '../../../../app/fw/svg/icon.service';
 import { GlyphService } from '../../../../app/fw/svg/glyph.service';
+import { IconComponent } from '../../../../app/fw/svg/icon/icon.component';
 import { KeyService } from '../../../../app/fw/util/key.service';
 import { LoadingService } from '../../../../app/fw/layer/loading.service';
 import { NavService } from '../../../../app/fw/nav/nav.service';
 import { MastService } from '../../../../app/fw/mast/mast.service';
 import { PanelService } from '../../../../app/fw/layer/panel.service';
 import { SvgUtilService } from '../../../../app/fw/svg/svgutil.service';
-import { TableBuilderService } from '../../../../app/fw/widget/tablebuilder.service';
 import { TableDetailService } from '../../../../app/fw/widget/tabledetail.service';
+import { ThemeService } from '../../../../app/fw/util/theme.service';
 import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
+import { of } from 'rxjs';
 
-class MockDetailsPanelService {}
-
-class MockFnService {
-    windowSize(offH: number = 0, offW: number = 0): WindowSize {
-        return {
-            height: 123,
-            width: 456
-        };
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
     }
 }
 
-class MockIconService {}
+class MockDetailsPanelService {}
+
+class MockFnService {}
+
+class MockIconService {
+    loadIconDef() {}
+}
 
 class MockGlyphService {}
 
 class MockKeyService {}
 
 class MockLoadingService {
-    startAnim() {
-        // Do nothing
-    }
+    startAnim() {}
+    stop() {}
 }
 
 class MockNavService {}
@@ -66,49 +70,81 @@
 
 class MockTableDetailService {}
 
-class MockWebSocketService {}
+class MockThemeService {}
+
+class MockWebSocketService {
+    createWebSocket() {}
+    isConnected() { return false; }
+    unbindHandlers() {}
+    bindHandlers() {}
+}
 
 /**
  * ONOS GUI -- Device View Module - Unit Tests
  */
 describe('DeviceComponent', () => {
-    let log: LogService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
     let component: DeviceComponent;
     let fixture: ComponentFixture<DeviceComponent>;
-    const windowMock = <any>{ location: <any> { hostname: 'localhost' } };
 
     beforeEach(async(() => {
-        log = new ConsoleLoggerService();
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({'debug': 'txrx'});
+
+        windowMock = <any>{
+            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, windowMock);
+
 
         TestBed.configureTestingModule({
-            declarations: [ DeviceComponent ],
+            declarations: [ DeviceComponent, IconComponent ],
             providers: [
                 { provide: DetailsPanelService, useClass: MockDetailsPanelService },
-                { provide: FnService, useClass: MockFnService },
+                { provide: FnService, useValue: fs },
                 { provide: IconService, useClass: MockIconService },
                 { provide: GlyphService, useClass: MockGlyphService },
                 { provide: KeyService, useClass: MockKeyService },
                 { provide: LoadingService, useClass: MockLoadingService },
                 { provide: MastService, useClass: MockMastService },
                 { provide: NavService, useClass: MockNavService },
-                { provide: LogService, useValue: log },
+                { provide: LogService, useValue: logSpy },
                 { provide: PanelService, useClass: MockPanelService },
-                { provide: TableBuilderService, useClass: MockTableBuilderService },
                 { provide: TableDetailService, useClass: MockTableDetailService },
+                { provide: ThemeService, useClass: MockThemeService },
                 { provide: WebSocketService, useClass: MockWebSocketService },
                 { provide: Window, useValue: windowMock },
              ]
         })
         .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
     }));
 
     beforeEach(() => {
         fixture = TestBed.createComponent(DeviceComponent);
-        component = fixture.componentInstance;
+        component = fixture.debugElement.componentInstance;
         fixture.detectChanges();
     });
 
     it('should create', () => {
         expect(component).toBeTruthy();
     });
+
+    it('should have .table-header with "Friendly Name..."', () => {
+        const appDe: DebugElement = fixture.debugElement;
+        const divDe = appDe.query(By.css('.table-header'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('Friendly Name Device ID Master Ports Vendor H/W Version S/W Version Protocol ');
+    });
 });