Implemented table building functions

Change-Id: Ie4003080b13725561df22de41ec85f8c3f31c794
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 ');
+    });
 });