GUI2 implementation of device/flow/port/group/meter/host/link/tunnel view

Review comments incorporated.

Change-Id: I45dd6570961cc3e0f4ffddb7acbf02cd7d860de5
diff --git a/web/gui2/src/main/webapp/app/README.txt b/web/gui2/src/main/webapp/app/README.txt
index cffc8a1..820f36e 100644
--- a/web/gui2/src/main/webapp/app/README.txt
+++ b/web/gui2/src/main/webapp/app/README.txt
@@ -4,4 +4,20 @@
 
 fw/ contains framework related code
 
-view/ contains view related code
\ No newline at end of file
+view/ contains view related code
+
+Device View -   Shows the registered devices and navigates to/from port, flow, group, meter view
+                DeviceDetails view on device row selection
+                Added search option based on device id, name, etc.
+                Details panel view on row selection of port and flow view
+
+                Navigation to pipeconf view on device view isn't implemented yet
+                Editing friendly name into the details panel isn't implemented yet
+
+Host View -     Shows the hosts attached with the registered devices
+                HostDetails view on host row selection
+                Editing friendly name into the details panel isn't implemented yet
+
+Link View -     Shows the links between the devices
+
+Tunnel View -   Shows the tunnels created between two end-points
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
index caa52ec..79e4153 100644
--- a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
@@ -22,7 +22,7 @@
 
 import { VeilComponent } from './veil.component';
 import { ConsoleLoggerService } from '../../../consolelogger.service';
-import { FnService } from '../../../fw/util/fn.service';
+import { FnService } from '../../util/fn.service';
 import { LogService } from '../../../log.service';
 import { KeyService } from '../../util/key.service';
 import { GlyphService } from '../../svg/glyph.service';
diff --git a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html
index cb48492..f807c9a 100644
--- a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html
+++ b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html
@@ -16,13 +16,22 @@
 <nav id="nav" [@navState]="ns.showNav">
     <div class="nav-hdr">{{ lionFn('cat_platform') }}</div>
 
-    <a (click)="ns.hideNav()" routerLink="/apps" routerLinkActive="active">
-        <onos-icon iconId="nav_apps"></onos-icon>Apps</a>
+    <a (click)="ns.hideNav()" routerLink="/app" routerLinkActive="active">
+        <onos-icon iconId="nav_apps"></onos-icon> Apps</a>
 
     <div class="nav-hdr">{{ lionFn('cat_network') }}</div>
 
-    <a (click)="ns.hideNav()" routerLink="/devices" routerLinkActive="active">
-        <onos-icon iconId="nav_devs"></onos-icon>Devices</a>
+    <a (click)="ns.hideNav()" routerLink="/device" routerLinkActive="active">
+        <onos-icon iconId="nav_devs"></onos-icon> Devices</a>
+
+    <a (click)="ns.hideNav()" routerLink="/link" routerLinkActive="active">
+        <onos-icon iconId="nav_links"></onos-icon> Links</a>
+
+    <a (click)="ns.hideNav()" routerLink="/host" routerLinkActive="active">
+        <onos-icon iconId="nav_hosts"></onos-icon> Hosts</a>
+
+    <a (click)="ns.hideNav()" routerLink="/tunnel" routerLinkActive="active">
+        <onos-icon iconId="nav_tunnels"></onos-icon> Tunnels</a>
 
     <div class="nav-hdr">{{ lionFn('cat_other') }}</div>
 </nav>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts
index e2bd999..7079b15 100644
--- a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts
@@ -21,7 +21,7 @@
 import { of } from 'rxjs';
 
 import { ConsoleLoggerService } from '../../../consolelogger.service';
-import { FnService } from '../../../fw/util/fn.service';
+import { FnService } from '../../util/fn.service';
 import { IconComponent } from '../../svg/icon/icon.component';
 import { IconService } from '../../svg/icon.service';
 import { LionService } from '../../util/lion.service';
@@ -127,6 +127,6 @@
         const appDe: DebugElement = fixture.debugElement;
         const divDe = appDe.query(By.css('nav#nav a'));
         const div: HTMLElement = divDe.nativeElement;
-        expect(div.textContent).toEqual('Apps');
+        expect(div.textContent).toEqual(' Apps');
     });
 });
diff --git a/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts b/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts
index 7e8adb4..0c1eff3 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 import { Injectable } from '@angular/core';
-import { FnService } from '../../fw/util/fn.service';
+import { FnService } from '../util/fn.service';
 import { LogService } from '../../log.service';
 import * as gds from './glyphdata.service';
 import * as d3 from 'd3';
+import { SvgUtilService } from './svgutil.service';
 
 // constants
 const msgGS = 'GlyphService.';
@@ -31,14 +32,25 @@
 export class GlyphService {
     // internal state
     glyphs = d3.map();
+    api: Object;
 
     constructor(
         private fs: FnService,
-//        private gd: GlyphDataService,
-        private log: LogService
+        //        private gd: GlyphDataService,
+        private log: LogService,
+        private sus: SvgUtilService
     ) {
         this.clear();
         this.init();
+        this.api = {
+            registerGlyphs: this.registerGlyphs,
+            registerGlyphSet: this.registerGlyphSet,
+            ids: this.ids,
+            glyph: this.glyph,
+            glyphDefined: this.glyphDefined,
+            loadDefs: this.loadDefs,
+            addGlyph: this.addGlyph,
+        };
         this.log.debug('GlyphService constructed');
     }
 
@@ -121,7 +133,7 @@
         }
 
         for (const [key, value] of data.entries()) {
-//        angular.forEach(data, function (value, key) {
+            //        angular.forEach(data, function (value, key) {
             if (key[0] !== '_') {
                 this.addToMap(key, value, vb, overwrite, dups);
             }
@@ -167,11 +179,28 @@
                     }
                 }
                 defs.append('symbol')
-                      .attr('id', g.id)
-                      .attr('viewBox', g.vb)
-                      .append('path')
-                      .attr('d', g.d);
+                    .attr('id', g.id)
+                    .attr('viewBox', g.vb)
+                    .append('path')
+                    .attr('d', g.d);
             }
         });
     }
+
+    addGlyph(elem: any, glyphId: string, size: number, overlay: any, trans: any) {
+        const sz = size || 40,
+            ovr = !!overlay,
+            xns = this.fs.isA(trans),
+            atr = {
+                width: sz,
+                height: sz,
+                'class': 'glyph',
+                'xlink:href': '#' + glyphId,
+            };
+
+        if (xns) {
+            atr.class = this.sus.translate(trans);
+        }
+        return elem.append('use').attr(atr).classed('overlay', ovr);
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
index 813589f..de3672c 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
@@ -40,6 +40,8 @@
     ['nonzero', 'nonzero'],
     ['close', 'xClose'],
 
+    ['m_ports', 'm_ports'],
+
     ['topo', 'topo'],
 
     ['refresh', 'refresh'],
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css
index 347837a..d2d6f56 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css
@@ -30,16 +30,58 @@
     fill: none;
 }
 
-svg.embeddedIcon g.icon rect {
-    stroke: none;
-    fill: none;
+.ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #e0dfd6;
 }
 
-svg.embeddedIcon g.icon .glyph {
-    stroke: none;
-    fill-rule: evenodd;
+.ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
 }
 
-svg.embeddedIcon .icon.appInactive .glyph {
-    fill: none !important;
+.ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
+    fill: #518ecc;
 }
+.ctrl-btns div.current-view svg.embeddedIcon g.icon use {
+    fill: white;
+}
+
+svg.embeddedIcon .icon.active .glyph {
+    fill: #04bf34;
+}
+
+svg.embeddedIcon .icon.inactive .glyph {
+    fill: #c0242b;
+}
+
+svg.embeddedIcon .icon.active-rect .glyph {
+    fill:#939598;
+}
+
+svg.embeddedIcon .icon.active-sort .glyph {
+    fill:#333333;
+}
+
+svg.embeddedIcon g.icon.active-rect:hover use {
+    fill: #ce5b58;
+}
+
+svg.embeddedIcon g.icon.active-type .glyph {
+    fill: #3c3a3a;
+}
+
+svg.embeddedIcon g.icon.active-close:hover use {
+    fill: #ce5b58;
+}
+
+svg.embeddedIcon g.icon.active-close .glyph {
+    fill: #333333;
+}
+
+svg.embeddedIcon g.icon.details-icon .glyph {
+    fill: #0071bd;;
+}
+
+
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 5be4c19..2243dc2 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,6 +13,7 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
+<div class="tooltip">
 <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>
@@ -20,4 +21,8 @@
     </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>
+
+<!-- <p id="tooltip" class="light" *ngIf="toolTip" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none'}">{{ toolTipDisp }}</p> -->
+
+    <span class="tooltiptext" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none'}">{{toolTipDisp}}</span>
+</div>
\ No newline at end of file
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 3e6f601..ca6e32e 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
@@ -18,28 +18,15 @@
  ONOS GUI -- Icon Service (theme) -- CSS file
  */
 
-.light div.close-btn svg.embeddedIcon g.icon .glyph {
+div.close-btn svg.embeddedIcon g.icon .glyph {
     fill: #333333;
 }
 
 /* Sortable table headers */
-.light div.tableColSort svg.embeddedIcon .icon .glyph {
+div.tableColSort svg.embeddedIcon .icon .glyph {
     fill: #353333;
 }
 
-/* active / inactive (check/xmark) icons */
-.light svg.embeddedIcon .icon.active .glyph {
-    fill: #04bf34;
-}
-
-.light svg.embeddedIcon .icon.inactive .glyph {
-    fill: #c0242b;
-}
-
-.light table svg.embeddedIcon .icon .glyph {
-    fill: #3c3a3a;
-}
-
 /* --- Control Buttons --- */
 
 /* INACTIVE */
@@ -50,9 +37,10 @@
 
 
 /* ACTIVE */
-svg.embeddedIcon g.icon.active use {
+.ctrl-btns div.active svg.embeddedIcon g.icon use {
     fill: #939598;
 }
+
 svg.embeddedIcon g.icon.active:hover use {
     fill: #ce5b58;
 }
@@ -82,25 +70,25 @@
 
 /* ========== DARK Theme ========== */
 
-.dark div.close-btn svg.embeddedIcon g.icon .glyph {
+ div.close-btn svg.embeddedIcon g.icon .glyph {
     fill: #8d8d8d;
 }
 
-.dark div.tableColSort svg.embeddedIcon .icon .glyph {
+ div.tableColSort svg.embeddedIcon .icon .glyph {
     fill: #888888;
 }
 
-.dark svg.embeddedIcon .icon.active .glyph {
+ /*svg.embeddedIcon .icon.active .glyph {
     fill: #04bf34;
 }
-
-.dark svg.embeddedIcon .icon.inactive .glyph {
+ svg.embeddedIcon .icon.inactive .glyph {
     fill: #c0242b;
-}
+}*/
 
-.dark table svg.embeddedIcon .icon .glyph {
+ table svg.embeddedIcon .icon .glyph {
     fill: #9999aa;
 }
+
 /*
 svg.embeddedIcon g.icon .glyph {
     fill: #007dc4;
@@ -109,4 +97,12 @@
 svg.embeddedIcon:hover g.icon .glyph {
     fill: #20b2ff;
 }
-*/
\ No newline at end of file
+*/
+
+svg.embeddedIcon g.icon.devIcon_SWITCH .glyph {
+    fill: #0071bd;;
+}
+
+svg.embeddedIcon g.icon.hostIcon_endstation .glyph {
+    fill: #0071bd;;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
index 74a5443..890f1f8 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
@@ -25,6 +25,37 @@
     padding: 5px;
     position: absolute;
     z-index: 5000;
-    display: none;
+    display: inline-block;
     pointer-events: none;
+    top: 40px;
+    right: auto;
+    /* width: 240px; */
+}
+
+.tooltip {
+    position: relative;
+    display: inline-block;
+}
+
+.tooltip .tooltiptext {
+    display: inline-block;
+    visibility: hidden;
+    background-color: #dbeffc;
+    color: #3c3a3a;
+    border-color: #c7c7c0;
+    text-align: center;
+    border-radius: 6px;
+    font-size: 80%;
+    padding: 5px;
+
+    /* Position the tooltip */
+    position: absolute;
+    z-index: 5000;
+    top: 42px;
+    right: 10%;
+    white-space: nowrap;
+}
+
+.tooltip:hover .tooltiptext {
+    visibility: visible;
 }
diff --git a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
index 9cba079..23759ab 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
@@ -23,7 +23,7 @@
  * The SVG Util Service provides a miscellany of utility functions.
  */
 @Injectable({
-  providedIn: 'root',
+    providedIn: 'root',
 })
 export class SvgUtilService {
 
diff --git a/web/gui2/src/main/webapp/app/fw/util/key.service.ts b/web/gui2/src/main/webapp/app/fw/util/key.service.ts
index 1eaa895..4c6f492 100644
--- a/web/gui2/src/main/webapp/app/fw/util/key.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/key.service.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 import { Injectable } from '@angular/core';
-import { FnService } from '../util/fn.service';
+import { FnService } from './fn.service';
 import { LogService } from '../../log.service';
 
 /**
diff --git a/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts b/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts
index f696125..991df7f 100644
--- a/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 import { Injectable } from '@angular/core';
-import { FnService } from '../util/fn.service';
+import { FnService } from './fn.service';
 import { LogService } from '../../log.service';
 import { WebSocketService } from '../remote/websocket.service';
 
@@ -22,16 +22,96 @@
  * ONOS GUI -- Util -- User Preference Service
  */
 @Injectable({
-  providedIn: 'root',
+    providedIn: 'root',
 })
 export class PrefsService {
-
+    protected Prefs;
+    protected handlers: string[] = [];
+    cache: any;
+    listeners: any;
     constructor(
-        private fs: FnService,
-        private log: LogService,
-        private wss: WebSocketService
+        protected fs: FnService,
+        protected log: LogService,
+        protected wss: WebSocketService
     ) {
+        this.cache = {};
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.Prefs, (data) => this.updatePrefs(data)]
+        ]));
+        this.handlers.push(this.Prefs);
+
         this.log.debug('PrefsService constructed');
     }
 
+    setPrefs(name: string, obj: any) {
+        // keep a cached copy of the object and send an update to server
+        this.cache[name] = obj;
+        this.wss.sendEvent('updatePrefReq', { key: name, value: obj });
+    }
+    updatePrefs(data: any) {
+        this.cache = data;
+        this.listeners.forEach(function (lsnr) { lsnr(); });
+    }
+
+    asNumbers(obj: any, keys?: any, not?: any) {
+        if (!obj) {
+            return null;
+        }
+
+        const skip = {};
+        if (not) {
+            keys.forEach(k => {
+                skip[k] = 1;
+            }
+            );
+        }
+
+        if (!keys || not) {
+            // do them all
+            Array.from(obj).forEach((v, k) => {
+                if (!not || !skip[k]) {
+                    obj[k] = Number(obj[k]);
+                }
+            });
+        } else {
+            // do the explicitly named keys
+            keys.forEach(k => {
+                obj[k] = Number(obj[k]);
+            });
+        }
+        return obj;
+    }
+
+    getPrefs(name: string, defaults: any, qparams?: string) {
+        const obj = Object.assign({}, defaults || {}, this.cache[name] || {});
+
+        // if query params are specified, they override...
+        if (this.fs.isO(qparams)) {
+            obj.forEach(k => {
+                if (qparams.hasOwnProperty(k)) {
+                    obj[k] = qparams[k];
+                }
+            });
+        }
+        return obj;
+    }
+
+    // merge preferences:
+    // The assumption here is that obj is a sparse object, and that the
+    //  defined keys should overwrite the corresponding values, but any
+    //  existing keys that are NOT explicitly defined here should be left
+    //  alone (not deleted).
+    mergePrefs(name: string, obj: any) {
+        const merged = this.cache[name] || {};
+        this.setPrefs(name, Object.assign(merged, obj));
+    }
+
+    addListener(listener: any) {
+        this.listeners.push(listener);
+    }
+
+    removeListener(listener: any) {
+        this.listeners = this.listeners.filter(function (obj) { return obj === listener; });
+    }
+
 }
diff --git a/web/gui2/src/main/webapp/app/fw/util/random.service.ts b/web/gui2/src/main/webapp/app/fw/util/random.service.ts
index d808e48..1b6e466 100644
--- a/web/gui2/src/main/webapp/app/fw/util/random.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/random.service.ts
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 import { Injectable } from '@angular/core';
-import { FnService } from '../util/fn.service';
 import { LogService } from '../../log.service';
 
 /**
@@ -26,7 +25,6 @@
 export class RandomService {
 
     constructor(
-        private fs: FnService,
         private log: LogService
     ) {
         this.log.debug('RandomService constructed');
diff --git a/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts b/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts
index d7cf7b8..59f85c5 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts
@@ -19,6 +19,7 @@
 import { WebSocketService } from '../remote/websocket.service';
 
 import { PanelBaseImpl } from './panel.base';
+import { Output, EventEmitter, Input } from '@angular/core';
 
 /**
  * A generic model of the data returned from the *DetailsResponse
@@ -37,6 +38,9 @@
  */
 export abstract class DetailsPanelBaseImpl extends PanelBaseImpl {
 
+    @Input() id: string;
+    @Output() closeEvent = new EventEmitter<string>();
+
     private root: string;
     private req: string;
     private resp: string;
@@ -85,16 +89,10 @@
     }
 
     /**
-     * Details Panel Data Request - should be called whenever id changes
-     * If id is empty, no request is made
+     * Details Panel Data Request - should be called whenever row id changes
      */
-    requestDetailsPanelData(id: string) {
-        if (id === '') {
-            return;
-        }
+    requestDetailsPanelData(query: any) {
         this.closed = false;
-        const query = {'id': id};
-
         // Do not send if the Web Socket hasn't opened
         if (this.wss.isConnected()) {
             if (this.fs.debugOn('panel')) {
@@ -109,5 +107,8 @@
      */
     close(): void {
         this.closed = true;
+        this.id = null;
+        this.closeEvent.emit(this.id);
     }
+
 }
diff --git a/web/gui2/src/main/webapp/app/fw/widget/panel.css b/web/gui2/src/main/webapp/app/fw/widget/panel.css
index 34d127f..48530ac 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/panel.css
+++ b/web/gui2/src/main/webapp/app/fw/widget/panel.css
@@ -22,9 +22,9 @@
     position: absolute;
     z-index: 100;
     display: block;
-    top: 120px;
-    width: 500px;
-    right: -505px;
+    top: 160px;
+    width: 544px;
+    right: -550px;
     opacity: 100;
 
     padding: 2px;
diff --git a/web/gui2/src/main/webapp/app/fw/widget/table.base.ts b/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
index 0093f72..cc29878 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
@@ -67,6 +67,11 @@
     secondDir: SortDir;
 }
 
+export interface PayloadParams {
+    devId: string;
+}
+
+
 /**
  * ONOS GUI -- Widget -- Table Base class
  */
@@ -74,7 +79,7 @@
     // attributes from the interface
     protected annots: TableAnnots;
     protected changedData: string[] = [];
-    protected payloadParams: any;
+    protected payloadParams: PayloadParams;
     protected sortParams: SortParams;
     protected selectCallback; // Function
     protected parentSelCb = null;
@@ -164,7 +169,7 @@
         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
+            this.tableData = [...newTableData]; // ES6 spread syntax
             // only flash the row if the data already exists
             if (oldTableData.length > 0) {
                 for (const idx in newTableData) {
@@ -282,4 +287,12 @@
             return '';
         }
     }
+
+    /**
+     * De-selects the row
+     */
+    deselectRow(event) {
+        this.log.debug('Details panel close event');
+        this.selId = event;
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/fw/widget/table.css b/web/gui2/src/main/webapp/app/fw/widget/table.css
index 9df99ef..1ed43b4 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/table.css
+++ b/web/gui2/src/main/webapp/app/fw/widget/table.css
@@ -31,14 +31,13 @@
 
 div.summary-list div.table-body {
     overflow-y: scroll;
-    max-height:70vh;
 }
 
 div.summary-list div.table-body::-webkit-scrollbar {
     display: none;
 }
 
-div.summary-list tr.no-data td {
+div.summary-list div.table-body tr.no-data td {
     text-align: center;
     font-style: italic;
 }
diff --git a/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts b/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts
index fef5123..21ff59c 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts
@@ -13,9 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Directive, ElementRef } from '@angular/core';
+import { AfterContentChecked, Directive, ElementRef, Inject } from '@angular/core';
 import { FnService } from '../util/fn.service';
 import { LogService } from '../../log.service';
+import { MastService } from '../mast/mast.service';
+import { HostListener } from '@angular/core';
+import * as d3 from 'd3';
 
 /**
  * ONOS GUI -- Widget -- Table Resize Directive
@@ -23,20 +26,58 @@
 @Directive({
     selector: '[onosTableResize]',
 })
-export class TableResizeDirective {
+export class TableResizeDirective implements AfterContentChecked {
 
-    constructor(
-        private fs: FnService,
-        public log: LogService,
-        private el: ElementRef,
-    ) {
+    pdg = 22;
+    tables: any;
 
-        this.windowSize();
-        this.log.debug('TableResizeDirective constructed');
+    constructor(protected fs: FnService,
+        protected log: LogService,
+        protected mast: MastService,
+        protected el: ElementRef,
+        @Inject('Window') private w: Window) {
+
+        log.info('TableResizeDirective constructed');
     }
 
-    windowSize() {
+    ngAfterContentChecked() {
+        this.tables = {
+            thead: d3.select('div.table-header').select('table'),
+            tbody: d3.select('div.table-body').select('table')
+        };
+        this.windowSize(this.tables);
+    }
+
+    windowSize(tables: any) {
         const wsz = this.fs.windowSize(0, 30);
-        this.el.nativeElement.style.width = wsz.width + 'px';
+        this.adjustTable(tables, wsz.width, wsz.height);
     }
+
+    @HostListener('window:resize', ['event'])
+    onResize(event: any) {
+        this.windowSize(this.tables);
+        return {
+            h: this.w.innerHeight,
+            w: this.w.innerWidth
+        };
+    }
+
+    adjustTable(tables: any, width: number, height: number) {
+        this._width(tables.thead, width + 'px');
+        this._width(tables.tbody, width + 'px');
+
+        this.setHeight(tables.thead, d3.select('div.table-body'), height);
+    }
+
+    _width(elem, width) {
+        elem.style('width', width);
+    }
+
+    setHeight(thead: any, body: any, height: number) {
+        const h = height - (this.mast.mastHeight +
+            this.fs.noPxStyle(d3.select('.tabular-header'), 'height') +
+            this.fs.noPxStyle(thead, 'height') + this.pdg);
+        body.style('height', h + 'px');
+    }
+
 }
diff --git a/web/gui2/src/main/webapp/app/onos-routing.module.ts b/web/gui2/src/main/webapp/app/onos-routing.module.ts
index 5d4af2b..040139b 100644
--- a/web/gui2/src/main/webapp/app/onos-routing.module.ts
+++ b/web/gui2/src/main/webapp/app/onos-routing.module.ts
@@ -22,16 +22,44 @@
  */
 const onosRoutes: Routes = [
     {
-        path: 'apps',
+        path: 'app',
         loadChildren: 'app/view/apps/apps.module#AppsModule'
     },
     {
-        path: 'devices',
+        path: 'device',
         loadChildren: 'app/view/device/device.module#DeviceModule'
     },
     {
+        path: 'link',
+        loadChildren: 'app/view/link/link.module#LinkModule'
+    },
+    {
+        path: 'host',
+        loadChildren: 'app/view/host/host.module#HostModule'
+    },
+    {
+        path: 'tunnel',
+        loadChildren: 'app/view/tunnel/tunnel.module#TunnelModule'
+    },
+    {
+        path: 'flow',
+        loadChildren: 'app/view/flow/flow.module#FlowModule'
+    },
+    {
+        path: 'port',
+        loadChildren: 'app/view/port/port.module#PortModule'
+    },
+    {
+        path: 'group',
+        loadChildren: 'app/view/group/group.module#GroupModule'
+    },
+    {
+        path: 'meter',
+        loadChildren: 'app/view/meter/meter.module#MeterModule'
+    },
+    {
         path: '',
-        redirectTo: 'devices', // Default to devices view - change to topo in future
+        redirectTo: 'device', // Default to devices view - change to topo in future
         pathMatch: 'full'
     }
 ];
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 738ee18..0e79a2b 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * Copyright 2018-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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
index 3096bae..636fb0c 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * Copyright 2018-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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
index 943e26b..37826fa 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
@@ -36,24 +36,24 @@
             <div class="separator"></div>
 
             <div class="active" (click)="triggerForm()">
-                <onos-icon classes="{{ 'active upload' }}"
+                <onos-icon classes="{{ 'active-rect upload' }}"
                            iconId="upload" iconSize="42" toolTip="{{ uploadTip }}"></onos-icon>
             </div>
 
-            <div (click)="confirmAction(AppActionEnum.ACTIVATE)">
-                <onos-icon classes="{{ ctrlBtnState.installed?'active play':'play' }}"
+            <div (click)="(!!selId) ? confirmAction(AppActionEnum.ACTIVATE) : ''">
+                <onos-icon classes="{{ ctrlBtnState.installed?'active-rect play':'play' }}"
                            iconId="play" iconSize="42" toolTip="{{ activateTip }}"></onos-icon>
             </div>
-            <div (click)="confirmAction(AppActionEnum.DEACTIVATE)">
-                <onos-icon classes="{{ ctrlBtnState.active?'active stop':'stop' }}"
+            <div (click)="(!!selId) ? confirmAction(AppActionEnum.DEACTIVATE) : ''">
+                <onos-icon classes="{{ ctrlBtnState.active?'active-rect stop':'stop' }}"
                         iconId="stop" iconSize="42" toolTip="{{ deactivateTip }}"></onos-icon>
             </div>
-            <div (click)="confirmAction(AppActionEnum.UNINSTALL)">
-                <onos-icon classes="{{ ctrlBtnState.selection?'active garbage':'garbage' }}"
+            <div (click)="(!!selId) ? confirmAction(AppActionEnum.UNINSTALL) : ''">
+                <onos-icon classes="{{ ctrlBtnState.selection?'active-rect garbage':'garbage' }}"
                         iconId="garbage" iconSize="42" toolTip="{{ uninstallTip }}"></onos-icon>
             </div>
-            <div (click)="downloadApp()">
-                <onos-icon classes="{{ ctrlBtnState.selection?'active download':'download' }}"
+            <div (click)="(!!selId) ? downloadApp() : ''">
+                <onos-icon classes="{{ ctrlBtnState.selection?'active-rect download':'download' }}"
                         iconId="download" iconSize="42" toolTip="{{ downloadTip }}"></onos-icon>
             </div>
         </div>
@@ -74,34 +74,34 @@
 
     </div>
 
-    <div id="summary-list" class="summary-list">
+    <div id="summary-list" class="summary-list" onosTableResize>
         <div class="table-header">
-            <table onosTableResize>
+            <table>
                 <tr>
                     <th colId="state" [ngStyle]="{width: '32px'}" class="table-icon" (click)="onSort('state')">
-                        <onos-icon classes="active" [iconId]="sortIcon('state')"></onos-icon>
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
                     </th>
                     <th colId="icon" [ngStyle]="{width: '32px'}" class="table-icon"></th>
                     <th colId="title"  (click)="onSort('title')">{{lionFn('title')}}
-                        <onos-icon classes="active" [iconId]="sortIcon('title')"></onos-icon>
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('title')"></onos-icon>
                     </th>
                     <th colId="id" (click)="onSort('id')">{{lionFn('app_id')}}
-                        <onos-icon classes="active" [iconId]="sortIcon('id')"></onos-icon>
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
                     </th>
                     <th colId="version" (click)="onSort('version')"> {{lionFn('version')}}
-                        <onos-icon classes="active" [iconId]="sortIcon('version')"></onos-icon>
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('version')"></onos-icon>
                     </th>
                     <th colId="category" (click)="onSort('category')"> {{lionFn('category')}}
-                        <onos-icon classes="active" [iconId]="sortIcon('category')"></onos-icon>
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('category')"></onos-icon>
                     </th>
                     <th colId="origin" (click)="onSort('origin')"> {{lionFn('origin')}}
-                        <onos-icon classes="active" [iconId]="sortIcon('origin')"></onos-icon>
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('origin')"></onos-icon>
                     </th>
                 </tr>
             </table>
         </div>
         <div class="table-body">
-            <table onosTableResize>
+            <table>
                 <tr *ngIf="tableData.length === 0" class="no-data">
                     <td colspan="5">
                         {{annots.no_rows_msg}}
@@ -137,6 +137,6 @@
      The advantage in 2) is that panel can be animated in and out, as it is not
      killed every time the selection changes.
      -->
-    <onos-appsdetails  class="floatpanels" id="{{ selId }}"></onos-appsdetails>
+    <onos-appsdetails class="floatpanels" id="{{ selId }}" (closeEvent)="deselectRow($event)"></onos-appsdetails>
 
 </div>
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
index cddf9ae..e022ebc 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
@@ -38,6 +38,7 @@
 import { UrlFnService } from '../../../fw/remote/urlfn.service';
 import { WebSocketService } from '../../../fw/remote/websocket.service';
 import { of } from 'rxjs';
+import { } from 'jasmine';
 
 class MockActivatedRoute extends ActivatedRoute {
     constructor(params: Params) {
@@ -46,33 +47,33 @@
     }
 }
 
-class MockDialogService {}
+class MockDialogService { }
 
-class MockFnService {}
+class MockFnService { }
 
 class MockHttpClient {}
 
 class MockIconService {
-    loadIconDef() {}
+    loadIconDef() { }
 }
 
-class MockKeyService {}
+class MockKeyService { }
 
 class MockLoadingService {
-    startAnim() {}
-    stop() {}
-    waiting() {}
+    startAnim() { }
+    stop() { }
+    waiting() { }
 }
 
-class MockThemeService {}
+class MockThemeService { }
 
-class MockUrlFnService {}
+class MockUrlFnService { }
 
 class MockWebSocketService {
-    createWebSocket() {}
+    createWebSocket() { }
     isConnected() { return false; }
-    unbindHandlers() {}
-    bindHandlers() {}
+    unbindHandlers() { }
+    bindHandlers() { }
 }
 
 /**
@@ -90,21 +91,21 @@
             test: 'test1'
         }
     };
-    const mockLion = (key) =>  {
+    const mockLion = (key) => {
         return bundleObj[key] || '%' + key + '%';
     };
 
     beforeEach(async(() => {
         const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
-        ar = new MockActivatedRoute({'debug': 'txrx'});
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
 
         windowMock = <any>{
-            location: <any> {
+            location: <any>{
                 hostname: 'foo',
                 host: 'foo',
                 port: '80',
                 protocol: 'http',
-                search: { debug: 'true'},
+                search: { debug: 'true' },
                 href: 'ws://foo:123/onos/ui/websock/path',
                 absUrl: 'ws://foo:123/onos/ui/websock/path'
             }
@@ -127,7 +128,8 @@
                 { provide: HttpClient, useClass: MockHttpClient },
                 { provide: IconService, useClass: MockIconService },
                 { provide: KeyService, useClass: MockKeyService },
-                { provide: LionService, useFactory: (() => {
+                {
+                    provide: LionService, useFactory: (() => {
                         return {
                             bundle: ((bundleId) => mockLion),
                             ubercache: new Array(),
@@ -143,7 +145,7 @@
                 { provide: 'Window', useValue: windowMock },
             ]
         })
-        .compileComponents();
+            .compileComponents();
         logServiceSpy = TestBed.get(LogService);
     }));
 
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
index ffd7b37..b2f6231 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
@@ -337,4 +337,13 @@
         evt.preventDefault();
         evt.stopPropagation();
     }
+
+    deselectRow(event) {
+        this.log.debug('Details panel close event');
+        this.selId = event;
+        this.ctrlBtnState = <CtrlBtnState>{
+            installed: undefined,
+            active: undefined
+        };
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css b/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
index 13f1847..a705d45 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-present Open Networking Foundation
+ * Copyright 2018-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.
@@ -41,7 +41,6 @@
 
 #ov-app div.summary-list .table-body {
     overflow:scroll;
-    max-height:70vh;
 }
 #ov-app h2 {
     display: inline-block;
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
index ce9af3a..90d4b14 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * Copyright 2018-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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html
index a78bdd9..d43c264 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html
@@ -1,5 +1,5 @@
 <!--
-~ Copyright 2014-present Open Networking Foundation
+~ Copyright 2018-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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts
index f890cde..75f5f8e 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts
@@ -21,10 +21,10 @@
 
 import { LogService } from '../../../log.service';
 import { AppsDetailsComponent } from './appsdetails.component';
-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 { LionService } from '../../../../app/fw/util/lion.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { IconService } from '../../../fw/svg/icon.service';
+import { LionService } from '../../../fw/util/lion.service';
 import { UrlFnService } from '../../../fw/remote/urlfn.service';
 import { WebSocketService } from '../../../fw/remote/websocket.service';
 import { of } from 'rxjs';
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts
index 1e6ed29..c0d2e02 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts
@@ -101,8 +101,20 @@
         this.log.debug('App Details Component destroyed');
     }
 
+    /**
+     * Details Panel Data Request on row selection changes
+     * Should be called whenever id changes
+     * If id is empty, no request is made
+     */
     ngOnChanges() {
-        this.requestDetailsPanelData(this.id);
+        if (this.id === '') {
+            return '';
+        } else {
+            const query = {
+                'id': this.id
+            };
+            this.requestDetailsPanelData(query);
+        }
     }
 
     iconUrl(appId: string): string {
diff --git a/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts b/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts
index 3110eca..64bcb92 100644
--- a/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts
@@ -15,8 +15,7 @@
  */
 import { NgModule } from '@angular/core';
 import { Routes, RouterModule } from '@angular/router';
-import { DeviceComponent } from './device.component';
-
+import { DeviceComponent } from './device/device.component';
 
 const deviceRoutes: Routes = [
     {
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
deleted file mode 100644
index 0e09d69..0000000
--- a/web/gui2/src/main/webapp/app/view/device/device.component.html
+++ /dev/null
@@ -1,114 +0,0 @@
-<!--
-~ Copyright 2014-present Open Networking Foundation
-~
-~ Licensed under the Apache License, Version 2.0 (the "License");
-~ you may not use this file except in compliance with the License.
-~ You may obtain a copy of the License at
-~
-~     http://www.apache.org/licenses/LICENSE-2.0
-~
-~ Unless required by applicable law or agreed to in writing, software
-~ distributed under the License is distributed on an "AS IS" BASIS,
-~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-~ See the License for the specific language governing permissions and
-~ limitations under the License.
--->
-<div id="ov-device">
-    <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"
-                (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.spec.ts b/web/gui2/src/main/webapp/app/view/device/device.component.spec.ts
deleted file mode 100644
index 066a636..0000000
--- a/web/gui2/src/main/webapp/app/view/device/device.component.spec.ts
+++ /dev/null
@@ -1,141 +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 { 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 '../../log.service';
-import { DeviceComponent } from './device.component';
-
-import { FnService, WindowSize } from '../../fw/util/fn.service';
-import { IconService } from '../../fw/svg/icon.service';
-import { GlyphService } from '../../fw/svg/glyph.service';
-import { IconComponent } from '../../fw/svg/icon/icon.component';
-import { KeyService } from '../../fw/util/key.service';
-import { LoadingService } from '../../fw/layer/loading.service';
-import { NavService } from '../../fw/nav/nav.service';
-import { MastService } from '../../fw/mast/mast.service';
-import { SvgUtilService } from '../../fw/svg/svgutil.service';
-import { ThemeService } from '../../fw/util/theme.service';
-import { WebSocketService } from '../../fw/remote/websocket.service';
-import { of } from 'rxjs';
-
-class MockActivatedRoute extends ActivatedRoute {
-    constructor(params: Params) {
-        super();
-        this.queryParams = of(params);
-    }
-}
-
-class MockDetailsPanelService {}
-
-class MockFnService {}
-
-class MockIconService {
-    loadIconDef() {}
-}
-
-class MockGlyphService {}
-
-class MockKeyService {}
-
-class MockLoadingService {
-    startAnim() {}
-    stop() {}
-}
-
-class MockNavService {}
-
-class MockMastService {}
-
-class MockTableBuilderService {}
-
-class MockTableDetailService {}
-
-class MockThemeService {}
-
-class MockWebSocketService {
-    createWebSocket() {}
-    isConnected() { return false; }
-    unbindHandlers() {}
-    bindHandlers() {}
-}
-
-/**
- * ONOS GUI -- Device View Module - Unit Tests
- */
-describe('DeviceComponent', () => {
-    let fs: FnService;
-    let ar: MockActivatedRoute;
-    let windowMock: Window;
-    let logServiceSpy: jasmine.SpyObj<LogService>;
-    let component: DeviceComponent;
-    let fixture: ComponentFixture<DeviceComponent>;
-
-    beforeEach(async(() => {
-        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, IconComponent ],
-            providers: [
-                { 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: logSpy },
-                { provide: ThemeService, useClass: MockThemeService },
-                { provide: WebSocketService, useClass: MockWebSocketService },
-                { provide: 'Window', useValue: windowMock },
-             ]
-        })
-        .compileComponents();
-        logServiceSpy = TestBed.get(LogService);
-    }));
-
-    beforeEach(() => {
-        fixture = TestBed.createComponent(DeviceComponent);
-        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 ');
-    });
-});
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 7840292..9ad5456 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * Copyright 2018-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.
@@ -16,22 +16,26 @@
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { DeviceRoutingModule } from './device-routing.module';
-import { DeviceComponent } from './device.component';
-import { DeviceDetailsPanelDirective } from './devicedetailspanel.directive';
+import { DeviceComponent } from './device/device.component';
 import { SvgModule } from '../../fw/svg/svg.module';
+import { WidgetModule } from '../../fw/widget/widget.module';
+import { FormsModule } from '@angular/forms';
+import { DeviceDetailsComponent } from './devicedetails/devicedetails.component';
 
 /**
  * ONOS GUI -- Device View Module
  */
 @NgModule({
-  imports: [
-    CommonModule,
-    DeviceRoutingModule,
-    SvgModule
-  ],
-  declarations: [
-    DeviceComponent,
-    DeviceDetailsPanelDirective
-  ]
+    imports: [
+        CommonModule,
+        DeviceRoutingModule,
+        SvgModule,
+        WidgetModule,
+        FormsModule
+    ],
+    declarations: [
+        DeviceComponent,
+        DeviceDetailsComponent
+    ]
 })
 export class DeviceModule { }
diff --git a/web/gui2/src/main/webapp/app/view/device/device.theme.css b/web/gui2/src/main/webapp/app/view/device/device.theme.css
deleted file mode 100644
index df0f139..0000000
--- a/web/gui2/src/main/webapp/app/view/device/device.theme.css
+++ /dev/null
@@ -1,31 +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.
- */
-
-/*
- ONOS GUI -- Device View (theme) -- CSS file
- */
-
-.light #device-details-panel .bottom th {
-    background-color: #e5e5e6;
-}
-
-.light #device-details-panel .bottom tr:nth-child(odd) {
-    background-color: #fbfbfb;
-}
-.light #device-details-panel .bottom tr:nth-child(even) {
-    background-color: #f4f4f4;
-}
-
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.css b/web/gui2/src/main/webapp/app/view/device/device/device.component.css
similarity index 97%
rename from web/gui2/src/main/webapp/app/view/device/device.component.css
rename to web/gui2/src/main/webapp/app/view/device/device/device.component.css
index 4d8454d..5ed16e8 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.css
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.css
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * Copyright 2018-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.
diff --git a/web/gui2/src/main/webapp/app/view/device/device/device.component.html b/web/gui2/src/main/webapp/app/view/device/device/device.component.html
new file mode 100644
index 0000000..7a22076
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.html
@@ -0,0 +1,121 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="ov-device">
+    <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 (click)="navto('/flow')">
+                <onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
+            </div>
+
+            <div (click)="navto('/port')">
+                <onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
+            </div>
+
+            <div (click)="navto('/group')">
+                <onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
+            </div>
+
+            <div (click)="navto('/meter')">
+                <onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
+            </div>
+
+            <div (click)="navto('/pipeconf')">
+                <onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
+            </div>
+        </div>
+        <div class="search">
+            <input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
+            <select [(ngModel)]="tableDataFilter.queryBy">
+                <option value="" disabled>Search By</option>
+                <option value="$">All Fields</option>
+                <option value="id">Device-Id</option>
+                <option value="name">Name</option>
+                <option value="protocol">Protocol</option>
+            </select>
+        </div>
+    </div>
+
+    <div id="summary-list" class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="available" class="table-icon"></td>
+                    <td colId="type" class="table-icon"></td>
+                    <td colId="name" (click)="onSort('name')">Friendly Name
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('name')"></onos-icon>
+                    </td>
+                    <td colId="id" (click)="onSort('id')">Device ID
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
+                    </td>
+                    <td colId="masterid" [ngClass]="{width: '130px'}" (click)="onSort('masterid')">Master
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('masterid')"></onos-icon>
+                    </td>
+                    <td colId="num_ports" [ngClass]="{width: '70px'}" (click)="onSort('num_ports')">Ports
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('num_ports')"></onos-icon>
+                    </td>
+                    <td colId="mfr" (click)="onSort('mfr')">Vendor
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('mfr')"></onos-icon>
+                    </td>
+                    <td colId="hw" (click)="onSort('hw')">H/W Version
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('hw')"></onos-icon>
+                    </td>
+                    <td colId="sw" (click)="onSort('sw')">S/W Version
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('sw')"></onos-icon>
+                    </td>
+                    <td colId="protocol" [ngClass]="{width: '100px'}" (click)="onSort('protocol')">Protocol
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('protocol')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+        <div class="table-body">
+            <table>
+                <tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="9">{{ annots.noRowsMsg }}</td>
+                </tr>
+                <tr *ngFor="let dev of tableData | filter : tableDataFilter" (click)="selectCallback($event, dev)" [ngClass]="{selected: dev.id === selId, 'data-change': isChanged(dev.id)}">
+                    <td class="table-icon">
+                        <onos-icon classes="{{ dev._iconid_available}}" iconId={{dev._iconid_available}}></onos-icon>
+                    </td>
+                    <td class="table-icon">
+                        <onos-icon classes="{{dev._iconid_type? 'active-type':undefined}}" 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>
+    </div>
+
+    <onos-devicedetails class="floatpanels" id="{{ selId }}" (closeEvent)="deselectRow($event)"></onos-devicedetails>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/device/device/device.component.spec.ts b/web/gui2/src/main/webapp/app/view/device/device/device.component.spec.ts
new file mode 100644
index 0000000..207dde9
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.spec.ts
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2018-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 { 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 '../../../log.service';
+import { DeviceComponent } from './device.component';
+import { } from 'jasmine';
+
+import { FnService } from '../../../fw/util/fn.service';
+import { IconService } from '../../../fw/svg/icon.service';
+import { GlyphService } from '../../../fw/svg/glyph.service';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { KeyService } from '../../../fw/util/key.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { NavService } from '../../../fw/nav/nav.service';
+import { MastService } from '../../../fw/mast/mast.service';
+import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
+import { ThemeService } from '../../../fw/util/theme.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { of } from 'rxjs';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { FormsModule } from '@angular/forms';
+import { DeviceDetailsComponent } from './../devicedetails/devicedetails.component';
+import { RouterTestingModule } from '@angular/router/testing';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockGlyphService { }
+
+class MockKeyService { }
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+}
+
+class MockNavService { }
+
+class MockMastService { }
+
+class MockThemeService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+/**
+ * ONOS GUI -- Device View Module - Unit Tests
+ */
+describe('DeviceComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: DeviceComponent;
+    let fixture: ComponentFixture<DeviceComponent>;
+
+    beforeEach(async(() => {
+        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({
+            imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
+            declarations: [DeviceComponent, IconComponent, TableFilterPipe, DeviceDetailsComponent],
+            providers: [
+                { 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: logSpy },
+                { provide: ThemeService, useClass: MockThemeService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        }).compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(DeviceComponent);
+        component = fixture.debugElement.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div.tabular-header inside a div#ov-device', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div#ov-device div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have .table-header with "Friendly Name..."', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div#ov-device div.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 ');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div#ov-device div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+
+    it('should have a div.table-body ', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div#ov-device  div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.ts b/web/gui2/src/main/webapp/app/view/device/device/device.component.ts
similarity index 61%
rename from web/gui2/src/main/webapp/app/view/device/device.component.ts
rename to web/gui2/src/main/webapp/app/view/device/device/device.component.ts
index ecccc34..b6f1d95 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.ts
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * Copyright 2018-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.
@@ -13,16 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
-import { FnService } from '../../fw/util/fn.service';
-import { IconService } from '../../fw/svg/icon.service';
-import { KeyService } from '../../fw/util/key.service';
-import { LoadingService } from '../../fw/layer/loading.service';
-import { LogService } from '../../log.service';
-import { MastService } from '../../fw/mast/mast.service';
-import { NavService } from '../../fw/nav/nav.service';
-import { TableBaseImpl, TableResponse } from '../../fw/widget/table.base';
-import { WebSocketService } from '../../fw/remote/websocket.service';
+import { Component, OnInit, OnDestroy} from '@angular/core';
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { LogService } from '../../../log.service';
+import { TableBaseImpl, TableResponse, SortDir } from '../../../fw/widget/table.base';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { ActivatedRoute, Router } from '@angular/router';
 
 /**
  * Model of the response from WebSocket
@@ -55,9 +52,9 @@
  * ONOS GUI -- Device View Component
  */
 @Component({
-  selector: 'onos-device',
-  templateUrl: './device.component.html',
-  styleUrls: ['./device.component.css', './device.theme.css', '../../fw/widget/table.css', '../../fw/widget/table.theme.css']
+    selector: 'onos-device',
+    templateUrl: './device.component.html',
+    styleUrls: ['./device.component.css', './device.theme.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
 })
 export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy {
 
@@ -71,16 +68,29 @@
     constructor(
         protected fs: FnService,
         protected ls: LoadingService,
-        private is: IconService,
-        private ks: KeyService,
         protected log: LogService,
-        private mast: MastService,
-        private nav: NavService,
+        protected as: ActivatedRoute,
+        protected router: Router,
         protected wss: WebSocketService,
-        @Inject('Window') private window: Window,
     ) {
         super(fs, ls, log, wss, 'device');
         this.responseCallback = this.deviceResponseCb;
+
+        this.as.queryParams.subscribe(params => {
+            this.selId = params['devId'];
+
+        });
+
+        this.payloadParams = {
+            devId: this.selId
+        };
+
+        this.sortParams = {
+            firstCol: 'name',
+            firstDir: SortDir.asc,
+            secondCol: 'id',
+            secondDir: SortDir.desc,
+        };
     }
 
     ngOnInit() {
@@ -97,4 +107,11 @@
         this.log.debug('Device response received for ', data.devices.length, 'devices');
     }
 
+    navto(path) {
+        this.log.debug('navigate');
+        if (this.selId) {
+            this.router.navigate([path], { queryParams: { devId: this.selId } });
+        }
+    }
+
 }
diff --git a/web/gui2/src/main/webapp/app/view/device/device/device.theme.css b/web/gui2/src/main/webapp/app/view/device/device/device.theme.css
new file mode 100644
index 0000000..d9b2c07
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.theme.css
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018-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 -- Device View (theme) -- CSS file
+ */
+
+.light #device-details-panel .bottom th {
+    background-color: #e5e5e6;
+}
+
+.light #device-details-panel .bottom tr:nth-child(odd) {
+    background-color: #fbfbfb;
+}
+.light #device-details-panel .bottom tr:nth-child(even) {
+    background-color: #f4f4f4;
+}
+#ov-device .tabular-header {
+    text-align: left;
+}
+#ov-device div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+#ov-device div.summary-list .table-body {
+    overflow:scroll;
+}
+#ov-device h2 {
+    display: inline-block;
+}
+
+#ov-device, div.ctrl-btns {
+}
+
+#ov-device th, td {
+    text-align: left;
+    padding:  8px;
+}
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.css b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css
similarity index 75%
copy from web/gui2/src/main/webapp/app/view/device/device.component.css
copy to web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css
index 4d8454d..a3903b0 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.css
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * Copyright 2018-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.
@@ -14,36 +14,24 @@
  * limitations under the License.
  */
 
-/*
- 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 {
-}
-
-
-/* More in generic panel.css */
-
 #device-details-panel.floatpanel {
     z-index: 0;
+    font-size: 10pt;
+    top: 185px;
 }
 
+#device-details-panel.floatpanel a {
+    font-weight: bold;
+}
 
 #device-details-panel .container {
-    padding: 8px 12px;
+    padding: 0 30px;
 }
 
 #device-details-panel .close-btn {
     position: absolute;
-    right: 12px;
-    top: 12px;
+    right:5px;
+    top: 5px;
     cursor: pointer;
 }
 
@@ -56,33 +44,15 @@
 #device-details-panel h2 {
     display: inline-block;
     margin: 8px 0;
+    font-weight: bold;
+    font-size: 16pt;
 }
 
-
 #device-details-panel h2 input {
     font-size: 0.90em;
     width: 106%;
 }
 
-#device-details-panel .top-tables {
-    font-size: 10pt;
-    white-space: nowrap;
-}
-
-#device-details-panel .top div.left {
-    float: left;
-    padding: 0 18px 0 0;
-}
-#device-details-panel .top div.right {
-    display: inline-block;
-}
-
-#device-details-panel td.label {
-    font-weight: bold;
-    text-align: right;
-    padding-right: 6px;
-}
-
 #device-details-panel .actionBtns div {
     padding: 12px 6px;
 }
@@ -92,8 +62,23 @@
     margin: 2px auto;
 }
 
+#device-details-panel .top-tables {
+    font-size: 10pt;
+    white-space: nowrap;
+}
+
+#device-details-panel td.label {
+    font-weight: bold;
+    text-align: right;
+    padding-right: 6px;
+}
+
 #device-details-panel .bottom table {
     border-spacing: 0;
+    height: 358px;
+    width: 520px;
+    overflow: auto;
+    display: block;
 }
 
 #device-details-panel .bottom th {
@@ -106,3 +91,24 @@
     text-align: center;
 }
 
+#device-details-panel .top div.left {
+    float: left;
+    text-align: left;
+    padding: 0 10px 0 0;
+}
+
+#device-details-panel .top div.right {
+    display: inline-block;
+}
+
+#device-details-panel .editable {
+    border-bottom: 1px dashed #ca504b;
+}
+
+#device-details-panel .clickable {
+    cursor: pointer;
+}
+
+#device-details-panel .bottom thead tr {
+    background-color: #e5e5e6;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.html b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.html
new file mode 100644
index 0000000..034b63b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.html
@@ -0,0 +1,111 @@
+<!--
+~ Copyright 2018-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.
+-->
+
+<div id="device-details-panel" class="floatpanel" [@deviceDetailsState]="id!=='' && !closed">
+  <div class="container">
+    <div class="top">
+      <div class="close-btn">
+        <onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
+      </div>
+      <div class="dev-icon">
+        <onos-icon classes="{{detailsData._iconid_type? 'details-icon':undefined}}" iconId="{{detailsData._iconid_type}}" [iconSize]="40"></onos-icon>
+      </div>
+      <h2 class="editable clickable">{{detailsData.id}}</h2>
+      <div class="top-content">
+        <div class="top-tables">
+          <div class="left">
+            <table>
+              <tbody>
+                <tr>
+                  <td class="label" width="110">URI :</td>
+                  <td class="value" width="80">{{detailsData.id}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">Type :</td>
+                  <td class="value" width="80">{{detailsData.type}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">Master ID :</td>
+                  <td class="value" width="80">{{detailsData.masterid}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">Chassis ID :</td>
+                  <td class="value" width="80">{{detailsData.chassisid}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">Vendor :</td>
+                  <td class="value" width="80">{{detailsData.mfr}}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+          <div class="right">
+            <table>
+              <tbody>
+                <tr>
+                  <td class="label" width="110">H/W Version :</td>
+                  <td class="value" width="80">{{detailsData.hw}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">S/W Version :</td>
+                  <td class="value" width="80">{{detailsData.sw}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">Protocol :</td>
+                  <td class="value" width="80">{{detailsData.protocol}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">Serial # :</td>
+                  <td class="value" width="80">{{detailsData.serial}}</td>
+                </tr>
+                <tr>
+                  <td class="label" width="110">Pipeconf :</td>
+                  <td class="value" width="80">{{detailsData.pipeconf}}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </div>
+      <hr>
+    </div>
+    <div class="bottom">
+      <h2 class="ports-title">Ports</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Enabled</th>
+            <th>ID</th>
+            <th>Speed</th>
+            <th>Type</th>
+            <th>Egress Links</th>
+            <th>Name</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr *ngFor="let port of detailsData.ports">
+            <td>{{port.enabled}}</td>
+            <td>{{port.id}}</td>
+            <td>{{port.speed}}</td>
+            <td>{{port.type}}</td>
+            <td>{{port.elinks_dest}}</td>
+            <td>{{port.name}}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.spec.ts
new file mode 100644
index 0000000..5daa2ef
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.spec.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { DeviceDetailsComponent } from './devicedetails.component';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs/index';
+import { FnService } from '../../../fw/util/fn.service';
+import { LogService } from '../../../log.service';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { IconService } from '../../../fw/svg/icon.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { } from 'jasmine';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    classes = 'active-close';
+    loadIconDef() { }
+}
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+describe('DeviceDetailsComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: DeviceDetailsComponent;
+    let fixture: ComponentFixture<DeviceDetailsComponent>;
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'panel' });
+        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({
+            imports: [BrowserAnimationsModule],
+            declarations: [DeviceDetailsComponent, IconComponent],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: LogService, useValue: logSpy },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+
+        }).compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(DeviceDetailsComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have an div.close-btn div.top inside a div.container', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div.container div.top div.close-btn'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.dev-icon inside a div.top inside a div.container', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div.container div.top div.dev-icon'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('');
+    });
+
+    it('should have a div.top-content inside a div.top inside a div.container', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div.container div.top div.top-content'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a dev.left inside a div.top-tables inside a div.top-content', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div.top-content div.top-tables div.left'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('URI :Type :Master ID :Chassis ID :Vendor :');
+    });
+
+    it('should have a dev.right inside a div.top-tables inside a div.top-content', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div.top-content div.top-tables div.right'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('H/W Version :S/W Version :Protocol :Serial # :Pipeconf :');
+    });
+
+    it('should have a div.bottom inside a div.container', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div.container div.bottom'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2.ports-title inside a div.bottom inside a div.container', () => {
+        const devDe: DebugElement = fixture.debugElement;
+        const divDe = devDe.query(By.css('div.container div.bottom h2.ports-title'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.ts b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.ts
new file mode 100644
index 0000000..9e30eb7
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Input, OnInit, OnDestroy, OnChanges } from '@angular/core';
+import { trigger, state, style, animate, transition } from '@angular/animations';
+
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { LogService } from '../../../log.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+
+import { DetailsPanelBaseImpl } from '../../../fw/widget/detailspanel.base';
+import { IconService } from '../../../fw/svg/icon.service';
+
+/**
+ * The details view when a device row is clicked from the Device view
+ *
+ * This is expected to be passed an 'id' and it makes a call
+ * to the WebSocket with an deviceDetailsRequest, and gets back an
+ * deviceDetailsResponse.
+ *
+ * The animated fly-in is controlled by the animation below
+ * The deviceDetailsState is attached to device-details-panel
+ * and is false (flies out) when id='' and true (flies in) when
+ * id has a value
+ */
+@Component({
+    selector: 'onos-devicedetails',
+    templateUrl: './devicedetails.component.html',
+    styleUrls: ['./devicedetails.component.css',
+        '../../../fw/widget/panel.css', '../../../fw/widget/panel-theme.css'
+    ],
+    animations: [
+        trigger('deviceDetailsState', [
+            state('true', style({
+                transform: 'translateX(-100%)',
+                opacity: '100'
+            })),
+            state('false', style({
+                transform: 'translateX(0%)',
+                opacity: '0'
+            })),
+            transition('0 => 1', animate('100ms ease-in')),
+            transition('1 => 0', animate('100ms ease-out'))
+        ])
+    ]
+})
+
+
+export class DeviceDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
+    @Input() id: string;
+
+    constructor(protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected is: IconService,
+        protected wss: WebSocketService
+    ) {
+        super(fs, ls, log, wss, 'device');
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('App Details Component initialized:', this.id);
+    }
+
+    /**
+     * Stop listening to appDetailsResponse on WebSocket
+     */
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('App Details Component destroyed');
+    }
+
+    /**
+     * Details Panel Data Request on row selection changes
+     * Should be called whenever id changes
+     * If id is empty, no request is made
+     */
+    ngOnChanges() {
+        if (this.id === '') {
+            return '';
+        } else {
+            const query = {
+                'id': this.id
+            };
+            this.requestDetailsPanelData(query);
+        }
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetailspanel.directive.ts b/web/gui2/src/main/webapp/app/view/device/devicedetailspanel.directive.ts
deleted file mode 100644
index d81a67d..0000000
--- a/web/gui2/src/main/webapp/app/view/device/devicedetailspanel.directive.ts
+++ /dev/null
@@ -1,39 +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, Inject } from '@angular/core';
-import { KeyService } from '../../fw/util/key.service';
-import { LogService } from '../../log.service';
-
-/**
- * ONOS GUI -- Device Details Panel Directive
- *
- * TODO: figure out if this should be a directive or a component. In the old
- * code it was a directive, but was referred to in device.html like a component
- * would be
- */
-@Directive({
-  selector: '[onosDeviceDetailsPanel]'
-})
-export class DeviceDetailsPanelDirective {
-
-    constructor(
-        private ks: KeyService,
-        private log: LogService
-    ) {
-        this.log.debug('DeviceDetailsPanelDirective constructed');
-    }
-
-}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow-routing.module.ts b/web/gui2/src/main/webapp/app/view/flow/flow-routing.module.ts
new file mode 100644
index 0000000..4ddf65f
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow-routing.module.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { FlowComponent } from './flow/flow.component';
+
+const flowRoutes: Routes = [
+    {
+        path: '',
+        component: FlowComponent
+    }
+];
+
+/**
+ * ONOS GUI -- Flows Tabular View Feature Routing Module - allows it to be lazy loaded
+ *
+ * See https://angular.io/guide/lazy-loading-ngmodules
+ */
+@NgModule({
+    imports: [RouterModule.forChild(flowRoutes)],
+    exports: [RouterModule]
+})
+export class FlowRoutingModule { }
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow.module.ts b/web/gui2/src/main/webapp/app/view/flow/flow.module.ts
new file mode 100644
index 0000000..55d3c96
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow.module.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FlowComponent } from './flow/flow.component';
+import { SvgModule } from '../../fw/svg/svg.module';
+import { WidgetModule } from '../../fw/widget/widget.module';
+import { FlowRoutingModule } from './flow-routing.module';
+import { FormsModule } from '@angular/forms';
+import { FlowDetailsComponent } from './flowdetails/flowdetails/flowdetails.component';
+
+/**
+ * ONOS GUI -- Flow View Module
+ */
+@NgModule({
+    imports: [
+        CommonModule,
+        SvgModule,
+        FlowRoutingModule,
+        FormsModule,
+        WidgetModule
+    ],
+    declarations: [
+        FlowComponent,
+        FlowDetailsComponent
+    ]
+})
+export class FlowModule { }
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css
new file mode 100644
index 0000000..3ed4d16
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2018-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 -- Flow View (layout) -- CSS file
+ */
+
+#ov-flow h2 {
+    display: inline-block;
+}
+
+#ov-flow div.ctrl-btns {
+}
+
+#ov-flow td {
+    text-align: center;
+}
+#ov-flow td.right {
+    text-align: right;
+}
+#ov-flow td.selector,
+#ov-flow td.treatment {
+    text-align: left;
+    padding-left: 36px;
+}
+
+#ov-flow .tabular-header {
+    text-align: left;
+}
+#ov-flow div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+/* More in generic panel.css */
+
+#flow-details-panel.floatpanel {
+    z-index: 0;
+}
+
+
+#flow-details-panel .container {
+    padding: 8px 12px;
+}
+
+#flow-details-panel .close-btn {
+    position: absolute;
+    right: 12px;
+    top: 12px;
+    cursor: pointer;
+}
+
+#flow-details-panel .dev-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#flow-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+    font-size: 16pt;
+    font-weight: lighter;
+}
+
+#flow-details-panel h3 {
+    display: inline-block;
+    margin: 8px 0;
+    font-size: 11pt;
+    font-variant: small-caps;
+    text-transform: uppercase;
+}
+
+#flow-details-panel .top-content table {
+    font-size: 10pt;
+}
+
+#flow-details-panel td.label {
+    font-weight: bold;
+    text-align: right;
+    padding-right: 6px;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html
new file mode 100644
index 0000000..a1b23df
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html
@@ -0,0 +1,136 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="ov-flow" xmlns="http://www.w3.org/1999/html">
+    <div class="tabular-header">
+        <h2>
+            {{lionFn('title_flows')}} {{id}} ({{ tableData.length }} {{ lionFn('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>
+            <span *ngIf="brief" (click)="briefToggle()">
+                <div>
+                    <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="plus" iconSize="42" toolTip="{{detailTip}}"></onos-icon>
+                </div>
+            </span>
+
+            <span *ngIf="!brief" (click)="briefToggle()">
+                <div>
+                    <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="minus" iconSize="42" toolTip="{{briefTip}}"></onos-icon>
+                </div>
+            </span>
+            <div class="separator"></div>
+            <div routerLink="/device" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
+            </div>
+
+            <div>
+                <onos-icon classes="{{ id ? 'current-view' :undefined}}" iconId="flowTable" iconSize="42"></onos-icon>
+            </div>
+
+            <div routerLink="/port" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/group" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/meter" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/pipeconf" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
+            </div>
+        </div>
+        <div class="search">
+            <input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
+            <select [(ngModel)]="tableDataFilter.queryBy">
+                <option value="" disabled>Search By</option>
+                <option value="$">All Fields</option>
+                <option value="priority">{{lionFn('priority')}}</option>
+                <option value="tableName">{{lionFn('tableName')}}</option>
+                <option value="selector">{{lionFn('selector')}}</option>
+                <option value="treatment">{{lionFn('treatment')}}</option>
+                <option value="appName">{{lionFn('appName')}}</option>
+            </select>
+        </div>
+    </div>
+
+    <div class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="state" (click)="onSort('state')">{{lionFn('state')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
+                    </td>
+                    <td colId="packets" (click)="onSort('packets')">{{lionFn('packets')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('packets')"></onos-icon>
+                    </td>
+                    <td colId="duration" (click)="onSort('duration')">{{lionFn('duration')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('duration')"></onos-icon>
+                    </td>
+                    <td colId="priority" (click)="onSort('priority')">{{lionFn('priority')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('priority')"></onos-icon>
+                    </td>
+                    <td colId="tableName" (click)="onSort('tableName')">{{lionFn('tableName')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('tableName')"></onos-icon>
+                    </td>
+                    <td colId="selector" (click)="onSort('selector')">{{lionFn('selector')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('selector')"></onos-icon>
+                    </td>
+                    <td colId="treatment" (click)="onSort('treatment')">{{lionFn('treatment')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('treatment')"></onos-icon>
+                    </td>
+                    <td colId="appName" (click)="onSort('appName')">{{lionFn('appName')}}
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('appName')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table>
+                <tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="9">{{annots.noRowsMsg}}</td>
+                </tr>
+                <ng-template ngFor let-flow [ngForOf]="tableData | filter : tableDataFilter">
+                    <tr (click)="selectCallback($event, flow)" [ngClass]="{selected: flow.id === selId, 'data-change': isChanged(flow.id)}">
+                        <td>{{flow.state}}</td>
+                        <td>{{flow.packets}}</td>
+                        <td>{{flow.duration}}</td>
+                        <td>{{flow.priority}}</td>
+                        <td>{{flow.tableName}}</td>
+                        <td>{{flow.selector_c}}</td>
+                        <td>{{flow.treatment_c}}</td>
+                        <td>{{flow.appName}}</td>
+                    </tr>
+                    <tr (click)="selectCallback($event, flow)" [ngClass]="{selected: flow.id === selId, 'data-change': isChanged(flow.id)}" [hidden]="brief">
+                        <td class="selector" colspan="8">{{flow.selector}} </td>
+                    </tr>
+                    <tr (click)="selectCallback($event, flow)" [ngClass]="{selected: flow.id === selId, 'data-change': isChanged(flow.id)}" [hidden]="brief">
+                        <td class="treatment" colspan="8">{{flow.treatment}}</td>
+                    </tr>
+                </ng-template>
+            </table>
+        </div>
+    </div>
+    <onos-flowdetails class="floatpanels" flowId="{{ selId }}" appId="{{ selRowAppId }}" (closeEvent)="deselectRow($event)"></onos-flowdetails>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.spec.ts b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.spec.ts
new file mode 100644
index 0000000..e6ed12a
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.spec.ts
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FlowComponent } from './flow.component';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs/index';
+import { LogService } from '../../../log.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { IconService } from '../../../fw/svg/icon.service';
+import { GlyphService } from '../../../fw/svg/glyph.service';
+import { KeyService } from '../../../fw/util/key.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { MastService } from '../../../fw/mast/mast.service';
+import { NavService } from '../../../fw/nav/nav.service';
+import { ThemeService } from '../../../fw/util/theme.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { LionService } from '../../../fw/util/lion.service';
+import { FlowDetailsComponent } from '../flowdetails/flowdetails/flowdetails.component';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockGlyphService { }
+
+class MockKeyService { }
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+}
+
+class MockNavService { }
+
+class MockMastService { }
+
+class MockThemeService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+/**
+ * ONOS GUI -- Flow View Module - Unit Tests
+ */
+
+describe('FlowComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: FlowComponent;
+    let fixture: ComponentFixture<FlowComponent>;
+
+    const bundleObj = {
+        'core.view.Flow': {
+            test: 'test1'
+        }
+    };
+    const mockLion = (key) => {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+    beforeEach(async(() => {
+        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({
+            imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
+            declarations: [FlowComponent, IconComponent, TableFilterPipe, FlowDetailsComponent],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: KeyService, useClass: MockKeyService },
+                {
+                    provide: LionService, useFactory: (() => {
+                        return {
+                            bundle: ((bundleId) => mockLion),
+                            ubercache: new Array(),
+                            loadCbs: new Map<string, () => void>([])
+                        };
+                    })
+                },
+                { provide: LoadingService, useClass: MockLoadingService },
+                { provide: MastService, useClass: MockMastService },
+                { provide: NavService, useClass: MockNavService },
+                { provide: LogService, useValue: logSpy },
+                { provide: ThemeService, useClass: MockThemeService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        }).compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(FlowComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div.tabular-header inside a div#ov-flow', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div#ov-flow div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.tabular-header', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div#ov-flow div.tabular-header h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual(' %title_flows%  (0 %total%) ');
+    });
+
+    it('should have .table-header with "State..."', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div#ov-flow div.table-header'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('%state% %packets% %duration% %priority% %tableName% %selector% %treatment% %appName% ');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div#ov-flow div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+
+    it('should have a div.table-body ', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div#ov-flow  div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts
new file mode 100644
index 0000000..96692a4
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { SortDir, TableBaseImpl, TableResponse } from '../../../fw/widget/table.base';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { LogService } from '../../../log.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { ActivatedRoute } from '@angular/router';
+import { LionService } from '../../../fw/util/lion.service';
+
+
+/**
+ * Model of the response from WebSocket
+ */
+interface FlowTableResponse extends TableResponse {
+    flows: Flow[];
+}
+
+/**
+ * Model of the flows returned from the WebSocket
+ */
+interface Flow {
+    state: string;
+    packets: string;
+    duration: string;
+    priority: string;
+    tableName: string;
+    selector: string;
+    treatment: string;
+    appName: string;
+}
+
+/**
+ * ONOS GUI -- Flow View Component
+ */
+@Component({
+    selector: 'onos-flow',
+    templateUrl: './flow.component.html',
+    styleUrls: ['./flow.component.css', './flow.theme.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class FlowComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    lionFn; // Function
+    id: string;
+    brief: boolean;
+    selRowAppId: string;
+
+    deviceTip: string;
+    detailTip: string;
+    briefTip: string;
+    portTip: string;
+    groupTip: string;
+    meterTip: string;
+    pipeconfTip: string;
+
+    constructor(protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected as: ActivatedRoute,
+        protected wss: WebSocketService,
+        protected lion: LionService,
+    ) {
+        super(fs, ls, log, wss, 'flow');
+        this.as.queryParams.subscribe(params => {
+            this.id = params['devId'];
+
+        });
+        this.brief = true;
+
+        this.payloadParams = {
+            devId: this.id
+        };
+
+        this.responseCallback = this.flowResponseCb;
+
+        this.sortParams = {
+            firstCol: 'state',
+            firstDir: SortDir.desc,
+            secondCol: 'packets',
+            secondDir: SortDir.asc,
+        };
+
+        // We want doLion() to be called only after the Lion
+        // service is populated (from the WebSocket)
+        // If lion is not ready we make do with a dummy function
+        // As soon a lion gets loaded this function will be replaced with
+        // the real thing
+        if (this.lion.ubercache.length === 0) {
+            this.lionFn = this.dummyLion;
+            this.lion.loadCbs.set('flows', () => this.doLion());
+        } else {
+            this.doLion();
+        }
+
+        this.parentSelCb = this.rowSelection;
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('FlowComponent initialized');
+    }
+
+    ngOnDestroy() {
+        this.lion.loadCbs.delete('flows');
+        this.destroy();
+        this.log.debug('FlowComponent destroyed');
+    }
+
+    flowResponseCb(data: FlowTableResponse) {
+        this.log.debug('Flow response received for ', data.flows.length, 'flow');
+    }
+
+    briefToggle() {
+        this.brief = !this.brief;
+    }
+
+    /**
+     * Read the LION bundle for App and set up the lionFn
+     */
+    doLion() {
+        this.lionFn = this.lion.bundle('core.view.Flow');
+
+        this.deviceTip = this.lionFn('tt_ctl_show_device');
+        this.detailTip = this.lionFn('tt_ctl_switcth_detailed');
+        this.briefTip = this.lionFn('tt_ctl_switcth_brief');
+        this.portTip = this.lionFn('tt_ctl_show_port');
+        this.groupTip = this.lionFn('tt_ctl_show_group');
+        this.meterTip = this.lionFn('tt_ctl_show_meter');
+        this.pipeconfTip = this.lionFn('tt_ctl_show_pipeconf');
+    }
+
+    rowSelection(event: any, selRow: any) {
+        this.selRowAppId = selRow.appId;
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css b/web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css
new file mode 100644
index 0000000..37738a9
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018-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 -- Flow View (theme) -- CSS file
+ */
+
+
+/* a "logical" row is made up of 3 "physical" rows -- color as such */
+ #ov-flow tr:nth-child(6n + 1),
+ #ov-flow tr:nth-child(6n + 2),
+ #ov-flow tr:nth-child(6n + 3) {
+    background-color: #fbfbfb;
+}
+ #ov-flow tr:nth-child(6n + 4),
+ #ov-flow tr:nth-child(6n + 5),
+ #ov-flow tr:nth-child(6n) {
+    background-color: #f4f4f4;
+}
+
+/* highlighted color */
+ #ov-flow tr:nth-child(6n + 1).data-change,
+ #ov-flow tr:nth-child(6n + 2).data-change,
+ #ov-flow tr:nth-child(6n + 3).data-change,
+ #ov-flow tr:nth-child(6n + 4).data-change,
+ #ov-flow tr:nth-child(6n + 5).data-change,
+ #ov-flow tr:nth-child(6n).data-change {
+    background-color: #FDFFDC;
+}
+
+#ov-flow td.selector,
+#ov-flow td.treatment {
+    opacity: 0.65;
+}
+
+/* ========== DARK Theme ========== */
+
+.dark #ov-flow tr:nth-child(6n + 1),
+.dark #ov-flow tr:nth-child(6n + 2),
+.dark #ov-flow tr:nth-child(6n + 3) {
+    background-color: #333333;
+}
+.dark #ov-flow tr:nth-child(6n + 4),
+.dark #ov-flow tr:nth-child(6n + 5),
+.dark #ov-flow tr:nth-child(6n) {
+    background-color: #3a3a3a;
+}
+
+.dark #ov-flow tr:nth-child(6n + 1).data-change,
+.dark #ov-flow tr:nth-child(6n + 2).data-change,
+.dark #ov-flow tr:nth-child(6n + 3).data-change,
+.dark #ov-flow tr:nth-child(6n + 4).data-change,
+.dark #ov-flow tr:nth-child(6n + 5).data-change,
+.dark #ov-flow tr:nth-child(6n).data-change {
+    background-color: #423708;
+}
+
+.light #flow-details-panel .bottom th {
+    background-color: #e5e5e6;
+}
+
+.light #flow-details-panel .bottom tr:nth-child(odd) {
+    background-color: #fbfbfb;
+}
+.light #flow-details-panel .bottom tr:nth-child(even) {
+    background-color: #f4f4f4;
+}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.css b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.css
new file mode 100644
index 0000000..2f11349
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.css
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018-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.
+ */
+
+ #flow-details-panel.floatpanel {
+    z-index: 0;
+    padding-top: 10px;
+    font-size: 10pt;
+    top: 185px;
+}
+
+#flow-details-panel .container {
+    padding: 8px 12px;
+}
+
+#flow-details-panel .close-btn {
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    cursor: pointer;
+}
+
+#flow-details-panel .flow-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#flow-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+    font-weight: bold;
+    font-size: 16pt;
+}
+
+#flow-details-panel hr {
+    clear: both;
+    width: 100%;
+    margin: 2px auto;
+}
+
+#flow-details-panel td.label {
+    font-weight: bold;
+    text-align: right;
+    padding-right: 6px;
+}
+
+#flow-details-panel .scroll {
+    border-spacing: 0;
+    height: 400px;
+    width: 520px;
+    overflow: auto;
+    display: block;
+}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.html b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.html
new file mode 100644
index 0000000..05d0a47
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.html
@@ -0,0 +1,116 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="flow-details-panel" class="floatpanel" [@flowDetailsState]="flowId!=='' && !closed">
+    <div class="container">
+        <div class="top">
+            <div class="close-btn">
+                <onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
+            </div>
+            <div class="flow-icon">
+                <onos-icon classes="details-icon" iconId="flowTable" [iconSize]="42"></onos-icon>
+            </div>
+            <h2>{{ flowId }}</h2>
+            <div class="scroll">
+                <div class="top-content">
+                    <table>
+                        <tbody>
+                            <tr>
+                                <td class="label">{{ lionFn('flowId') }} :</td>
+                                <td class="value">{{ flowId }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('state') }} :</td>
+                                <td class="value">{{ detailsData.state }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('bytes') }} :</td>
+                                <td class="value">{{ detailsData.bytes }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('packets') }} :</td>
+                                <td class="value">{{ detailsData.packets }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('duration') }} :</td>
+                                <td class="value">{{ detailsData.duration }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('priority') }} :</td>
+                                <td class="value">{{ detailsData.priority }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('tableName') }} :</td>
+                                <td class="value">{{ detailsData.tableName }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('appName') }} :</td>
+                                <td class="value">{{ detailsData.appName }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('appId') }} :</td>
+                                <td class="value">{{ detailsData.appId }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('groupId') }} :</td>
+                                <td class="value">{{ detailsData.groupId }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('idleTimeout') }} :</td>
+                                <td class="value">{{ detailsData.idleTimeout }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('hardTimeout') }} :</td>
+                                <td class="value">{{ detailsData.hardTimeout }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">{{ lionFn('permanent') }} :</td>
+                                <td class="value">{{ detailsData.permanent }}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+                <hr>
+                <h3>{{ lionFn('selector') }}</h3>
+                <div class="top-content">
+                    <table>
+                        <tbody>
+                            <tr>
+                                <td class="label">ETH_TYPE :</td>
+                                <td class="value">{{ detailsData.selector }}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+                <hr>
+                <h3>{{ lionFn('treatment') }}</h3>
+                <div class="top-content">
+                    <table>
+                        <tbody>
+                            <tr>
+                                <td class="label">[imm]OUTPUT :</td>
+                                <td class="value">{{ immed(detailsData.treatment) }}</td>
+                            </tr>
+                            <tr>
+                                <td class="label">Clear deferred :</td>
+                                <td class="value">{{ clearDef(detailsData.treatment) }}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.spec.ts
new file mode 100644
index 0000000..8bbf482
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.spec.ts
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FlowDetailsComponent } from './flowdetails.component';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { FnService } from '../../../../fw/util/fn.service';
+import { LogService } from '../../../../log.service';
+import { IconService } from '../../../../fw/svg/icon.service';
+import { WebSocketService } from '../../../../fw/remote/websocket.service';
+import { IconComponent } from '../../../../fw/svg/icon/icon.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    classes = 'active-close';
+    loadIconDef() { }
+}
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+describe('FlowDetailsComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: FlowDetailsComponent;
+    let fixture: ComponentFixture<FlowDetailsComponent>;
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'panel' });
+        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({
+            imports: [BrowserAnimationsModule],
+            declarations: [FlowDetailsComponent, IconComponent],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: LogService, useValue: logSpy },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        })
+            .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(FlowDetailsComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have an div.close-btn div.top inside a div.container', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div.container div.top div.close-btn'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.flow-icon inside a div.top inside a div.container', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div.container div.top div.flow-icon'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('');
+    });
+
+    it('should have a div.top-content inside a div.top inside a div.container', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div.container div.top div.top-content'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.scroll inside a div.container', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div.container div.scroll'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside a div.top inside a div.container', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div.container div.top h2'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h3 inside a div.scroll inside a div.top inside a div.container', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div.container div.top div.scroll h3'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a hr inside a div.scroll inside a div.top inside a div.container', () => {
+        const flowDe: DebugElement = fixture.debugElement;
+        const divDe = flowDe.query(By.css('div.container div.top div.scroll hr'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.ts b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.ts
new file mode 100644
index 0000000..3197285
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.ts
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit, OnDestroy, OnChanges, Input, Output, EventEmitter } from '@angular/core';
+import { DetailsPanelBaseImpl } from '../../../../fw/widget/detailspanel.base';
+import { FnService } from '../../../../fw/util/fn.service';
+import { LoadingService } from '../../../../fw/layer/loading.service';
+import { LogService } from '../../../../log.service';
+import { WebSocketService } from '../../../../fw/remote/websocket.service';
+import { LionService } from '../../../../fw/util/lion.service';
+import { trigger, state, style, transition, animate } from '@angular/animations';
+
+/**
+ * The details view when a flow is clicked from the flows view
+ *
+ * This is expected to be passed an 'id' and it makes a call
+ * to the WebSocket with a flowDetailsRequest, and gets back a
+ * flowDetailsResponse.
+ *
+ * The animated fly-in is controlled by the animation below
+ * The flowDetailsState is attached to flow-details-panel
+ * and is false (flies out) when id='' and true (flies in) when
+ * id has a value
+ */
+@Component({
+    selector: 'onos-flowdetails',
+    templateUrl: './flowdetails.component.html',
+    styleUrls: [
+        './flowdetails.component.css',
+        '../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css'
+    ],
+    animations: [
+        trigger('flowDetailsState', [
+            state('true', style({
+                transform: 'translateX(-100%)',
+                opacity: '100'
+            })),
+            state('false', style({
+                transform: 'translateX(0%)',
+                opacity: '0'
+            })),
+            transition('0 => 1', animate('100ms ease-in')),
+            transition('1 => 0', animate('100ms ease-out'))
+        ])
+    ]
+})
+export class FlowDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
+
+    @Input() flowId: string;
+    @Input() appId: string;
+
+    @Output() closeEvent = new EventEmitter<string>();
+
+    lionFn; // Function
+
+    constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+        protected lion: LionService,
+    ) {
+        super(fs, ls, log, wss, 'flow');
+        if (this.lion.ubercache.length === 0) {
+            this.lionFn = this.dummyLion;
+            this.lion.loadCbs.set('flowdetails', () => this.doLion());
+        } else {
+            this.doLion();
+        }
+    }
+
+    /**
+       * There is a possibility that a previous selection
+       * is already registered for call - if so wait 100ms
+       * for it to deregister - this is because in the list of
+       * flows we might have selected one higher up the list and
+       * it is now being processed here before an older selection
+       * farther down the list has been removed
+       */
+    ngOnInit() {
+        this.init();
+        this.log.debug('Flow Details Component initialized:', this.flowId);
+    }
+
+    /**
+     * Stop listening to flowDetailsResponse on WebSocket
+     */
+    ngOnDestroy() {
+        this.lion.loadCbs.delete('flowdetails');
+        this.destroy();
+        this.log.debug('Flow Details Component destroyed');
+    }
+
+    /**
+     * Details Panel Data Request on row selection changes
+     * Should be called whenever flow id changes
+     * If flowId or appId is empty, no request is made
+     */
+    ngOnChanges() {
+        if (this.flowId === '' || this.appId === '') {
+            return;
+        } else {
+            const query = {
+                'flowId': this.flowId,
+                'appId': this.appId
+            };
+            this.requestDetailsPanelData(query);
+        }
+    }
+
+    /**
+     * Read the LION bundle for Flow and set up the lionFn
+     */
+    doLion() {
+        this.lionFn = this.lion.bundle('core.view.Flow');
+    }
+
+    /**
+     * Return immediate value of flow treatment on flow details request
+     */
+    immed(treatmentData: any) {
+        if (treatmentData === undefined) {
+            return '';
+        } else {
+            return treatmentData.immed;
+        }
+    }
+
+    /**
+     * Return clear deferred value of flow treatment on flow details request
+     */
+    clearDef(treatmentData: any) {
+        if (treatmentData === undefined) {
+            return '';
+        } else {
+            return treatmentData.clearDef;
+        }
+    }
+
+    close() {
+        this.flowId = null;
+        this.appId = null;
+        this.closed = true;
+        this.closeEvent.emit(this.flowId);
+    }
+}
diff --git a/web/gui2/src/main/webapp/app/view/group/group-routing.module.ts b/web/gui2/src/main/webapp/app/view/group/group-routing.module.ts
new file mode 100644
index 0000000..9ce3153
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group-routing.module.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { GroupComponent } from './group/group.component';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: GroupComponent
+  }
+];
+
+/**
+ * ONOS GUI -- Groups Tabular View Feature Routing Module - allows it to be lazy loaded
+ *
+ * See https://angular.io/guide/lazy-loading-ngmodules
+ */
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class GroupRoutingModule { }
diff --git a/web/gui2/src/main/webapp/app/view/group/group.module.ts b/web/gui2/src/main/webapp/app/view/group/group.module.ts
new file mode 100644
index 0000000..343a971
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group.module.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { GroupRoutingModule } from './group-routing.module';
+import { GroupComponent } from './group/group.component';
+import { SvgModule } from '../../fw/svg/svg.module';
+import { WidgetModule } from '../../fw/widget/widget.module';
+import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    GroupRoutingModule,
+    SvgModule,
+    WidgetModule,
+    FormsModule,
+    RouterModule
+  ],
+  declarations: [GroupComponent],
+  exports: [GroupComponent]
+})
+export class GroupModule { }
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.css b/web/gui2/src/main/webapp/app/view/group/group/group.component.css
new file mode 100644
index 0000000..fbf19de
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.css
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018-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 -- Group View (layout) -- CSS file
+ */
+
+#ov-group h2 {
+    display: inline-block;
+}
+
+#ov-group div.ctrl-btns {
+}
+
+#ov-group td {
+    text-align: center;
+}
+#ov-group td.right {
+    text-align: right;
+}
+#ov-group td.buckets {
+    text-align: left;
+    padding-left: 36px;
+}
+
+#ov-group .tabular-header {
+    text-align: left;
+}
+#ov-group div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+/* More in generic panel.css */
+
+#group-details-panel.floatpanel {
+    z-index: 0;
+}
+
+
+#group-details-panel .container {
+    padding: 8px 12px;
+}
+
+#group-details-panel .close-btn {
+    position: absolute;
+    right: 12px;
+    top: 12px;
+    cursor: pointer;
+}
+
+#group-details-panel .dev-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#group-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+    font-size: 16pt;
+    font-weight: lighter;
+}
+
+#group-details-panel h3 {
+    display: inline-block;
+    margin: 8px 0;
+    font-size: 11pt;
+    font-variant: small-caps;
+    text-transform: uppercase;
+}
+
+#group-details-panel .top-content table {
+    font-size: 10pt;
+}
+
+#group-details-panel td.label {
+    font-weight: bold;
+    text-align: right;
+    padding-right: 6px;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.html b/web/gui2/src/main/webapp/app/view/group/group/group.component.html
new file mode 100644
index 0000000..4b0afbb
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.html
@@ -0,0 +1,124 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="ov-group" xmlns="http://www.w3.org/1999/html">
+    <div class="tabular-header">
+        <h2>
+            Groups for Device {{id}} ({{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>
+            <span *ngIf="brief" (click)="briefToggle()">
+                <div>
+                    <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="plus" iconSize="42" toolTip="{{detailTip}}"></onos-icon>
+                </div>
+            </span>
+
+            <span *ngIf="!brief" (click)="briefToggle()">
+                <div>
+                    <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="minus" iconSize="42" toolTip="{{briefTip}}"></onos-icon>
+                </div>
+            </span>
+            <div class="separator"></div>
+            <div routerLink="/device" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
+            </div>
+
+            <div routerLink="/flow" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/port" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
+            </div>
+
+            <div>
+                <onos-icon classes="{{ id ? 'current-view' :undefined}}" iconId="groupTable" iconSize="42"></onos-icon>
+            </div>
+
+            <div routerLink="/meter" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/pipeconf" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
+            </div>
+        </div>
+
+        <div class="search">
+            <input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
+            <select [(ngModel)]="tableDataFilter.queryBy">
+                <option value="" disabled>Search By</option>
+                <option value="$">All Fields</option>
+                <option value="id">Group Id</option>
+                <option value="app_id">App Id</option>
+                <option value="state">State</option>
+                <option value="type">Type</option>
+            </select>
+        </div>
+    </div>
+
+    <div class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="id" (click)="onSort('id')">Group Id
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
+                    </td>
+                    <td colId="app_id" (click)="onSort('app_id')">App Id
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('app_id')"></onos-icon>
+                    </td>
+                    <td colId="state" (click)="onSort('state')">State
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
+                    </td>
+                    <td colId="type" (click)="onSort('type')">Type
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('type')"></onos-icon>
+                    </td>
+                    <td colId="packets" (click)="onSort('packets')">Packets
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('packets')"></onos-icon>
+                    </td>
+                    <td colId="bytes" (click)="onSort('bytes')">Bytes
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table>
+                <tr class="no-data" *ngIf="tableData.length === 0">
+                    <td colspan="6">{{ annots.noRowsMsg }}</td>
+                </tr>
+                <ng-template ngFor let-group [ngForOf]="tableData | filter : tableDataFilter">
+                    <tr [ngClass]="{'data-change': isChanged(group.id)}">
+                        <td>{{group.id}}</td>
+                        <td>{{group.app_id}}</td>
+                        <td>{{group.state}}</td>
+                        <td>{{group.type}}</td>
+                        <td>{{group.packets}}</td>
+                        <td>{{group.bytes}}</td>
+                    </tr>
+                    <tr (click)="selectCallback($event, group)" [hidden]="brief" [ngClass]="{'data-change': isChanged(group.id)}">
+                        <td class="buckets" colspan="6" [innerHTML]="group.buckets"></td>
+                    </tr>
+                </ng-template>
+            </table>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.spec.ts b/web/gui2/src/main/webapp/app/view/group/group/group.component.spec.ts
new file mode 100644
index 0000000..bcf0ca2
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.spec.ts
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GroupComponent } from './group.component';
+import { LogService } from '../../../log.service';
+import { ConsoleLoggerService } from '../../../consolelogger.service';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+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 { LoadingService } from '../../../fw/layer/loading.service';
+import { ThemeService } from '../../../fw/util/theme.service';
+import { UrlFnService } from '../../../fw/remote/urlfn.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { FormsModule } from '@angular/forms';
+import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
+import { RouterTestingModule } from '@angular/router/testing';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockDialogService { }
+
+class MockFnService { }
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockKeyService { }
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+    waiting() { }
+}
+
+class MockThemeService { }
+
+class MockUrlFnService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+/**
+ * ONOS GUI -- Group View Module - Unit Tests
+ */
+describe('GroupComponent', () => {
+    let component: GroupComponent;
+    let fixture: ComponentFixture<GroupComponent>;
+    let log: LogService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    const bundleObj = {
+        'core.view.Group': {
+            test: 'test1',
+            tt_help: 'Help!'
+        }
+    };
+    const mockLion = (key) => {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+    beforeEach(async(() => {
+        log = new ConsoleLoggerService();
+        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, log, windowMock);
+
+        TestBed.configureTestingModule({
+            imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
+            declarations: [GroupComponent, IconComponent, TableFilterPipe],
+            providers: [
+                { provide: DialogService, useClass: MockDialogService },
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: KeyService, useClass: MockKeyService },
+                { provide: LoadingService, useClass: MockLoadingService },
+                { provide: LogService, useValue: log },
+                { provide: ThemeService, useClass: MockThemeService },
+                { provide: UrlFnService, useClass: MockUrlFnService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        })
+            .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(GroupComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div.tabular-header inside a div#ov-group', () => {
+        const groupDe: DebugElement = fixture.debugElement;
+        const divDe = groupDe.query(By.css('div#ov-group div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.tabular-header', () => {
+        const groupDe: DebugElement = fixture.debugElement;
+        const divDe = groupDe.query(By.css('div#ov-group div.tabular-header h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual(' Groups for Device  (0 total) ');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const groupDe: DebugElement = fixture.debugElement;
+        const divDe = groupDe.query(By.css('div#ov-group div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.summary-list inside a div#ov-group', () => {
+        const groupDe: DebugElement = fixture.debugElement;
+        const divDe = groupDe.query(By.css('div#ov-group div.summary-list'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-header inside a div.summary-list inside a div#ov-group', () => {
+        const groupDe: DebugElement = fixture.debugElement;
+        const divDe = groupDe.query(By.css('div#ov-group div.summary-list div.table-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-body inside a div.summary-list inside a div#ov-group', () => {
+        const groupDe: DebugElement = fixture.debugElement;
+        const divDe = groupDe.query(By.css('div#ov-group div.summary-list div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.ts b/web/gui2/src/main/webapp/app/view/group/group/group.component.ts
new file mode 100644
index 0000000..fc6e9bd
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { LogService } from '../../../log.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { ActivatedRoute } from '@angular/router';
+import { TableResponse, TableBaseImpl, SortDir } from '../../../fw/widget/table.base';
+
+/**
+ * Model of the response from WebSocket
+ */
+interface GroupTableResponse extends TableResponse {
+    groups: Group[];
+}
+
+/**
+ * Model of the flows returned from the WebSocket
+ */
+interface Group {
+    id: string;
+    app_id: string;
+    state: string;
+    type: string;
+    packets: string;
+    bytes: string;
+}
+
+/**
+ * ONOS GUI -- Group View Component
+ */
+@Component({
+    selector: 'onos-group',
+    templateUrl: './group.component.html',
+    styleUrls: ['./group.component.css', './group.theme.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class GroupComponent extends TableBaseImpl implements OnInit, OnDestroy {
+    id: string;
+    brief: boolean;
+
+    // TODO: Update for LION
+    deviceTip = 'Show device table';
+    detailTip = 'Switch to detailed view';
+    briefTip = 'Switch to brief view';
+    flowTip = 'Show flow view for selected device';
+    portTip = 'Show port view for selected device';
+    meterTip = 'Show meter view for selected device';
+    pipeconfTip = 'Show pipeconf view for selected device';
+
+    constructor(
+        protected log: LogService,
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected wss: WebSocketService,
+        protected ar: ActivatedRoute,
+    ) {
+        super(fs, ls, log, wss, 'group');
+        this.ar.queryParams.subscribe(params => {
+            this.id = params['devId'];
+        });
+        this.brief = true;
+
+        this.payloadParams = {
+            devId: this.id
+        };
+
+        this.responseCallback = this.groupResponseCb;
+
+        this.sortParams = {
+            firstCol: 'id',
+            firstDir: SortDir.desc,
+            secondCol: 'app_id',
+            secondDir: SortDir.asc,
+        };
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.info('GroupComponent initialized');
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.info('GroupComponent destroyed');
+    }
+
+    groupResponseCb(data: GroupTableResponse) {
+        this.log.debug('Group response received for ', data.groups.length, 'group');
+    }
+
+    briefToggle() {
+        this.brief = !this.brief;
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.theme.css b/web/gui2/src/main/webapp/app/view/group/group/group.theme.css
new file mode 100644
index 0000000..f90556c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.theme.css
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018-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 -- Group View (theme) -- CSS file
+ */
+
+/* a "logical" row is made up of 2 "physical" rows -- color as such */
+#ov-group tr:nth-child(4n + 1),
+#ov-group tr:nth-child(4n + 2) {
+    background-color: #fbfbfb;
+}
+#ov-group tr:nth-child(4n + 3),
+#ov-group tr:nth-child(4n) {
+    background-color: #f4f4f4;
+}
+
+/* highlighted color */
+#ov-group tr:nth-child(4n + 1).data-change,
+#ov-group tr:nth-child(4n + 2).data-change,
+#ov-group tr:nth-child(4n + 3).data-change,
+#ov-group tr:nth-child(4n).data-change {
+    background-color: #FDFFDC;
+}
+
+#ov-group td.selector,
+#ov-group td.treatment {
+    opacity: 0.65;
+}
+
+/* ========== DARK Theme ========== */
+
+.dark #ov-group tr:nth-child(4n + 1),
+.dark #ov-group tr:nth-child(4n + 2) {
+    background-color: #333333;
+}
+.dark #ov-group tr:nth-child(4n + 3),
+.dark #ov-group tr:nth-child(4n) {
+    background-color: #3a3a3a;
+}
+
+.dark #ov-group tr:nth-child(4n + 1).data-change,
+.dark #ov-group tr:nth-child(4n + 2).data-change,
+.dark #ov-group tr:nth-child(4n + 3).data-change,
+.dark #ov-group tr:nth-child(4n).data-change {
+    background-color: #423708;
+}
+
+.light #group-details-panel .bottom th {
+    background-color: #e5e5e6;
+}
+
+.light #group-details-panel .bottom tr:nth-child(odd) {
+    background-color: #fbfbfb;
+}
+.light #group-details-panel .bottom tr:nth-child(even) {
+    background-color: #f4f4f4;
+}
diff --git a/web/gui2/src/main/webapp/app/view/host/host-routing.module.ts b/web/gui2/src/main/webapp/app/view/host/host-routing.module.ts
new file mode 100644
index 0000000..62f10a1
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+* Copyright 2018-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 { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { HostComponent } from './host/host.component';
+
+const hostRoutes: Routes = [
+  {
+    path: '',
+    component: HostComponent
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(hostRoutes)],
+  exports: [RouterModule]
+})
+export class HostRoutingModule { }
diff --git a/web/gui2/src/main/webapp/app/view/host/host.module.ts b/web/gui2/src/main/webapp/app/view/host/host.module.ts
new file mode 100644
index 0000000..230b55a
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host.module.ts
@@ -0,0 +1,34 @@
+/*
+* Copyright 2018-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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { HostRoutingModule } from './host-routing.module';
+import { HostComponent } from './host/host.component';
+import { SvgModule } from '../../fw/svg/svg.module';
+import { WidgetModule } from '../../fw/widget/widget.module';
+import { HostDetailsComponent } from './hostdetails/hostdetails.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    HostRoutingModule,
+    WidgetModule,
+    SvgModule
+  ],
+  declarations: [HostComponent, HostDetailsComponent]
+})
+export class HostModule { }
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.css b/web/gui2/src/main/webapp/app/view/host/host/host.component.css
new file mode 100644
index 0000000..d306a23
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.css
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018-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 -- Hosts Panel (layout) -- CSS file
+ */
+
+#ov-host .tabular-header {
+    text-align: left;
+}
+#ov-host div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+}
+
+#ov-host h2 {
+    display: inline-block;
+}
+
+#ov-host th, td {
+    text-align: left;
+    padding:  8px;
+}
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.html b/web/gui2/src/main/webapp/app/view/host/host/host.component.html
new file mode 100644
index 0000000..7c7e32c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.html
@@ -0,0 +1,76 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="ov-host">
+    <div class="tabular-header">
+        <h2>Hosts ({{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>
+    </div>
+    <div class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="type" class="table-icon"></td>
+                    <td colId="name" (click)="onSort('name')">Friendly Name
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('name')"></onos-icon>
+                    </td>
+                    <td colId="id" (click)="onSort('id')">Host ID
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
+                    </td>
+                    <td colId="mac" (click)="onSort('mac')">MAC Address
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('mac')"></onos-icon>
+                    </td>
+                    <td colId="vlan" (click)="onSort('vlan')">VLAN ID
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('vlan')"></onos-icon>
+                    </td>
+                    <td colId="configured" (click)="onSort('configured')">Configured
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('configured')"></onos-icon>
+                    </td>
+                    <td colId="ips" (click)="onSort('ips')">IP Addresses
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('ips')"></onos-icon>
+                    </td>
+                    <td colId="location" (click)="onSort('location')">Location
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('location')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+        <div class="table-body">
+            <table>
+                <tr *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="8">{{ annots.noRowsMsg }}</td>
+                </tr>
+                <tr *ngFor="let host of tableData" (click)="selectCallback($event, host)" [ngClass]="{selected: host.id === selId, 'data-change': isChanged(host.id)}">
+                    <td class="table-icon">
+                        <onos-icon classes="{{host._iconid_type? 'active-type':undefined}}" iconId="{{host._iconid_type}}"></onos-icon>
+                    </td>
+                    <td>{{host.name}}</td>
+                    <td>{{host.id}}</td>
+                    <td>{{host.mac}}</td>
+                    <td>{{host.vlan}}</td>
+                    <td>{{host.configured}}</td>
+                    <td>{{host.ips}}</td>
+                    <td>{{host.location}}</td>
+                </tr>
+            </table>
+        </div>
+    </div>
+    <onos-hostdetails class="floatpanels" id="{{ selId }}" (closeEvent)="deselectRow($event)"></onos-hostdetails>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.spec.ts b/web/gui2/src/main/webapp/app/view/host/host/host.component.spec.ts
new file mode 100644
index 0000000..6d915c7
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.spec.ts
@@ -0,0 +1,176 @@
+/*
+* Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { FormsModule } from '@angular/forms';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+import { LogService } from '../../../log.service';
+import { HostComponent } from './host.component';
+import { HostDetailsComponent } from '../hostdetails/hostdetails.component';
+import { DialogService } from '../../../fw/layer/dialog.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { IconService } from '../../../fw/svg/icon.service';
+import { KeyService } from '../../../fw/util/key.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { ThemeService } from '../../../fw/util/theme.service';
+import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
+import { UrlFnService } from '../../../fw/remote/urlfn.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { of } from 'rxjs';
+import { } from 'jasmine';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockDialogService { }
+
+class MockFnService { }
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockKeyService { }
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+    waiting() { }
+}
+
+class MockThemeService { }
+
+class MockUrlFnService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+
+describe('HostComponent', () => {
+
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: HostComponent;
+    let fixture: ComponentFixture<HostComponent>;
+    const bundleObj = {
+        'core.view.Host': {
+            test: 'test1'
+        }
+    };
+    const mockLion = (key) => {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+
+    beforeEach(async(() => {
+        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({
+            imports: [BrowserAnimationsModule, FormsModule],
+            declarations: [HostComponent, HostDetailsComponent, IconComponent, TableFilterPipe],
+            providers: [
+                { provide: DialogService, useClass: MockDialogService },
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: KeyService, useClass: MockKeyService },
+                { provide: LoadingService, useClass: MockLoadingService },
+                { provide: LogService, useValue: logSpy },
+                { 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(HostComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div.tabular-header inside a div#ov-host', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-host div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.tabular-header', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-host div.tabular-header h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('Hosts (0 total)');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-host div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.summary-list inside a div#ov-host', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-host div.summary-list'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-header inside a div.summary-list inside a div#ov-host', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-host div.summary-list div.table-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-body inside a div.summary-list inside a div#ov-host', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-host div.summary-list div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+
+});
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.ts b/web/gui2/src/main/webapp/app/view/host/host/host.component.ts
new file mode 100644
index 0000000..c13e2f5
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.ts
@@ -0,0 +1,79 @@
+/*
+* Copyright 2018-present Open Networking Foundation
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { LogService } from '../../../log.service';
+import { TableBaseImpl, TableResponse, SortDir } from '../../../fw/widget/table.base';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+
+interface HostTableResponse extends TableResponse {
+    hosts: Host[];
+}
+
+interface Host {
+    name: boolean;
+    id: string;
+    hw: string;
+    vlanId: string;
+    configured: string;
+    address: string;
+    location: string;
+    _iconid_type: string;
+}
+
+/**
+ * ONOS GUI -- Host View Component
+ */
+@Component({
+    selector: 'onos-host',
+    templateUrl: './host.component.html',
+    styleUrls: ['./host.component.css',
+        '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class HostComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+    ) {
+        super(fs, ls, log, wss, 'host');
+        this.responseCallback = this.hostResponseCb;
+        this.sortParams = {
+            firstCol: 'name',
+            firstDir: SortDir.desc,
+            secondCol: 'id',
+            secondDir: SortDir.asc,
+        };
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('HostComponent initialized');
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('HostComponent destroyed');
+    }
+
+    hostResponseCb(data: HostTableResponse) {
+        this.log.debug('Host response received for ', data.hosts.length, 'host');
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.css b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.css
new file mode 100644
index 0000000..39b0131
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.css
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018-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 -- Hosts Details Panel (layout) -- CSS file
+ */
+
+#host-details-panel.floatpanel {
+    z-index: 0;
+    font-size: 10pt;
+    top: 145px;
+    height: 80vh;
+}
+
+#host-details-panel.floatpanel a {
+    font-weight: bold;
+}
+
+#host-details-panel .host-details {
+    padding: 0 30px;
+}
+
+#host-details-panel .close-btn {
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    cursor: pointer;
+}
+
+#host-details-panel .host-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#host-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+    font-weight: bold;
+    font-size: 16pt;
+}
+
+#host-details-panel h2 input {
+    font-size: 0.90em;
+    width: 106%;
+}
+
+#host-details-panel .actionBtns div {
+    padding: 12px 6px;
+}
+
+#host-details-panel hr {
+    width: 100%;
+    margin: 2px auto;
+}
+
+#host-details-panel td.label {
+    font-weight: bold;
+    text-align: right;
+    padding-right: 6px;
+}
+
+#host-details-panel .editable {
+    border-bottom: 1px dashed #ca504b;
+}
+
+#host-details-panel .clickable {
+    cursor: pointer;
+}
+
+#host-details-panel .bottom thead tr {
+    background-color: #e5e5e6;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.html b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.html
new file mode 100644
index 0000000..c7c9dba
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.html
@@ -0,0 +1,70 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="host-details-panel" class="floatpanel" [@hostDetailsState]="id!=='' && !closed">
+    <div class="container">
+        <div class="top">
+            <onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
+        </div>
+        <div class="host-icon">
+            <onos-icon classes="{{detailsData._iconid_type? 'hostIcon_endstation':undefined}}" iconId="{{detailsData._iconid_type}}"
+                [iconSize]="40"></onos-icon>
+        </div>
+        <h2 class="editable clickable">{{detailsData.name}}</h2>
+        <div class="top-content">
+            <div class="top-tables">
+                <div class="left">
+                    <table>
+                        <tbody>
+                            <tr>
+                                <td class="label" width="110">Host ID :</td>
+                                <td class="value" width="80">{{detailsData.id}}</td>
+                            </tr>
+                            <tr>
+                                <td class="label" width="110">IP Address :</td>
+                                <td class="value" width="80">{{detailsData.ips}}</td>
+                            </tr>
+                            <tr>
+                                <td class="label" width="110">MAC Address :</td>
+                                <td class="value" width="80">{{detailsData.mac}}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+                <div class="right">
+                    <table>
+                        <tbody>
+                            <tr>
+                                <td class="label" width="110">VLAN :</td>
+                                <td class="value" width="80">{{detailsData.vlan}}</td>
+                            </tr>
+                            <tr>
+                                <td class="label" width="110">Configured :</td>
+                                <td class="value" width="80">{{detailsData.configured}}</td>
+                            </tr>
+                            <tr>
+                                <td class="label" width="110">Location :</td>
+                                <td class="value" width="80">{{detailsData.location}}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        </div>
+        <hr>
+        <div class="bottom"></div>
+
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.spec.ts
new file mode 100644
index 0000000..0ce1ba0
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.spec.ts
@@ -0,0 +1,167 @@
+/*
+* Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+import { LogService } from '../../../log.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 { UrlFnService } from '../../../fw/remote/urlfn.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { of } from 'rxjs';
+import { } from 'jasmine';
+
+import { HostDetailsComponent } from './hostdetails.component';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockFnService { }
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockUrlFnService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+/**
+ * ONOS GUI -- Host Detail Panel View -- Unit Tests
+ */
+
+describe('HostdetailsComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: HostDetailsComponent;
+    let fixture: ComponentFixture<HostDetailsComponent>;
+
+    const bundleObj = {
+        'core.view.Hosts': {
+        }
+    };
+
+    const mockLion = (key) => {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+    beforeEach(async(() => {
+
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'panel' });
+
+        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({
+            imports: [BrowserAnimationsModule],
+            declarations: [HostDetailsComponent, IconComponent],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: LogService, useValue: logSpy },
+                { provide: UrlFnService, useClass: MockUrlFnService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        })
+            .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(HostDetailsComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have an onos-icon.close-btn inside a div.top inside a div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div.container div.top onos-icon.close-btn'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.host-icon inside a div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div.container div.host-icon'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#host-details-panel div.container h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('');
+    });
+
+    it('should have a div.top-content inside a div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div.container div.top-content'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.top-tables inside a div.top-content inside a div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div.container div.top-content div.top-tables'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.left inside a div.top-tables inside a div.top-content inside a div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div.container div.top-content div.top-tables div.left'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.right inside a div.top-tables inside a div.top-content inside a div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div.container div.top-content div.top-tables div.right'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.bottom inside a div.container', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div.container div.bottom'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.ts b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.ts
new file mode 100644
index 0000000..7d1132d
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.ts
@@ -0,0 +1,98 @@
+/*
+* Copyright 2018-present Open Networking Foundation
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, Input, OnInit, OnDestroy, OnChanges } from '@angular/core';
+import { trigger, state, style, animate, transition } from '@angular/animations';
+
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { LogService } from '../../../log.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+
+import { DetailsPanelBaseImpl } from '../../../fw/widget/detailspanel.base';
+
+/**
+ * The details view when a host row is clicked from the Host view
+ *
+ * This is expected to be passed an 'id' and it makes a call
+ * to the WebSocket with an hostDetailsRequest, and gets back an
+ * hostDetailsResponse.
+ *
+ * The animated fly-in is controlled by the animation below
+ * The hostDetailsState is attached to host-details-panel
+ * and is false (flies out) when id='' and true (flies in) when
+ * id has a value
+ */
+@Component({
+    selector: 'onos-hostdetails',
+    templateUrl: './hostdetails.component.html',
+    styleUrls: ['./hostdetails.component.css',
+        '../../../fw/widget/panel.css', '../../../fw/widget/panel-theme.css'
+    ],
+    animations: [
+        trigger('hostDetailsState', [
+            state('true', style({
+                transform: 'translateX(-100%)',
+                opacity: '100'
+            })),
+            state('false', style({
+                transform: 'translateX(0%)',
+                opacity: '0'
+            })),
+            transition('0 => 1', animate('100ms ease-in')),
+            transition('1 => 0', animate('100ms ease-out'))
+        ])
+    ]
+})
+export class HostDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
+    @Input() id: string;
+
+    constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected wss: WebSocketService
+    ) {
+        super(fs, ls, log, wss, 'host');
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('Hosts Details Component initialized:', this.id);
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('Hosts Details Component destroyed');
+    }
+
+    /**
+     * Details Panel Data Request on row selection changes
+     * Should be called whenever id changes
+     * If id is empty, no request is made
+     */
+    ngOnChanges() {
+        if (this.id === '') {
+            return '';
+        } else {
+            const query = {
+                'id': this.id
+            };
+            this.requestDetailsPanelData(query);
+        }
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/link/link-routing.module.ts b/web/gui2/src/main/webapp/app/view/link/link-routing.module.ts
new file mode 100644
index 0000000..a2ff939
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link-routing.module.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { LinkComponent } from './link/link.component';
+
+const linkRoutes: Routes = [
+    {
+        path: '',
+        component: LinkComponent
+    }
+];
+
+/**
+ * ONOS GUI -- Links Tabular View Feature Routing Module - allows it to be lazy loaded
+ *
+ * See https://angular.io/guide/lazy-loading-ngmodules
+ */
+@NgModule({
+    imports: [RouterModule.forChild(linkRoutes)],
+    exports: [RouterModule]
+})
+export class LinkRoutingModule { }
diff --git a/web/gui2/src/main/webapp/app/view/link/link.module.ts b/web/gui2/src/main/webapp/app/view/link/link.module.ts
new file mode 100644
index 0000000..92276b0
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link.module.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { LinkComponent } from './link/link.component';
+import { SvgModule } from '../../fw/svg/svg.module';
+import { LinkRoutingModule } from './link-routing.module';
+import { WidgetModule } from '../../fw/widget/widget.module';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    LinkRoutingModule,
+    SvgModule,
+    WidgetModule
+  ],
+  declarations: [
+    LinkComponent
+  ]
+})
+export class LinkModule { }
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.css b/web/gui2/src/main/webapp/app/view/link/link/link.component.css
new file mode 100644
index 0000000..a7c04e2
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.css
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018-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.
+ */
+ 
+#ov-link .tabular-header {
+    text-align: left;
+}
+#ov-link div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+}
+
+#ov-link h2 {
+    display: inline-block;
+}
+
+#ov-link th, td {
+    text-align: left;
+    padding:  8px;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.html b/web/gui2/src/main/webapp/app/view/link/link/link.component.html
new file mode 100644
index 0000000..e90ede8
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.html
@@ -0,0 +1,66 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="ov-link">
+    <div class="tabular-header">
+        <h2>Links ({{tableData.length}} 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>
+    </div>
+    <div class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="available" class="table-icon"></td>
+                    <td colId="one" (click)="onSort('one')">Port 1
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('one')"></onos-icon>
+                    </td>
+                    <td colId="two" (click)="onSort('two')">Port 2
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('two')"></onos-icon>
+                    </td>
+                    <td colId="type" (click)="onSort('type')">Type
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('type')"></onos-icon>
+                    </td>
+                    <td colId="direction" (click)="onSort('direction')">Direction
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('direction')"></onos-icon>
+                    </td>
+                    <td colId="durable" (click)="onSort('durable')">Durable
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('durable')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+        <div class="table-body">
+            <table>
+                <tr *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="6">{{ annots.noRowsMsg }}</td>
+                </tr>
+                <tr *ngFor="let link of tableData">
+                    <td class="table-icon">
+                        <onos-icon classes="{{link._iconid_state === 'active'? 'active':'inactive'}}" iconId="{{link._iconid_state}}"></onos-icon>
+                    </td>
+                    <td>{{link.one}}</td>
+                    <td>{{link.two}}</td>
+                    <td>{{link.type}}</td>
+                    <td [innerHtml]="link.direction"></td>
+                    <td>{{link.durable}}</td>
+                </tr>
+            </table>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.spec.ts b/web/gui2/src/main/webapp/app/view/link/link/link.component.spec.ts
new file mode 100644
index 0000000..699e535
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.spec.ts
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LinkComponent } from './link.component';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { FnService } from '../../../fw/util/fn.service';
+import { LogService } from '../../../log.service';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { IconService } from '../../../fw/svg/icon.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+    waiting() { }
+}
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+/**
+ * ONOS GUI -- Link View Module - Unit Tests
+ */
+describe('LinkComponent', () => {
+
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: LinkComponent;
+    let fixture: ComponentFixture<LinkComponent>;
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+        windowMock = <any>{
+            location: <any>{
+                linkname: 'foo',
+                link: '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: [LinkComponent, IconComponent],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: LoadingService, useClass: MockLoadingService },
+                { provide: LogService, useValue: logSpy },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+            ]
+        })
+            .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(LinkComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div.tabular-header inside a div#ov-link', () => {
+        const linkDe: DebugElement = fixture.debugElement;
+        const divDe = linkDe.query(By.css('div#ov-link div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.tabular-header', () => {
+        const linkDe: DebugElement = fixture.debugElement;
+        const divDe = linkDe.query(By.css('div#ov-link div.tabular-header h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('Links (0 total)');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const linkDe: DebugElement = fixture.debugElement;
+        const divDe = linkDe.query(By.css('div#ov-link div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.summary-list inside a div#ov-link', () => {
+        const linkDe: DebugElement = fixture.debugElement;
+        const divDe = linkDe.query(By.css('div#ov-link div.summary-list'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-header inside a div.summary-list inside a div#ov-link', () => {
+        const linkDe: DebugElement = fixture.debugElement;
+        const divDe = linkDe.query(By.css('div#ov-link div.summary-list div.table-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-body inside a div.summary-list inside a div#ov-link', () => {
+        const linkDe: DebugElement = fixture.debugElement;
+        const divDe = linkDe.query(By.css('div#ov-link div.summary-list div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.ts b/web/gui2/src/main/webapp/app/view/link/link/link.component.ts
new file mode 100644
index 0000000..eeb7c33
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { LogService } from '../../../log.service';
+import { TableBaseImpl, TableResponse, SortDir } from '../../../fw/widget/table.base';
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+
+/**
+ * Model of the response from WebSocket
+ */
+interface LinkTableResponse extends TableResponse {
+    links: Link[];
+}
+
+/**
+ * Model of the links returned from the WebSocket
+ */
+interface Link {
+    one: string;
+    two: string;
+    type: string;
+    direction: string;
+    durable: string;
+    _iconid_state: string;
+}
+
+/**
+ * ONOS GUI -- Link View Component
+ */
+@Component({
+    selector: 'onos-link',
+    templateUrl: './link.component.html',
+    styleUrls: ['./link.component.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class LinkComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+    ) {
+        super(fs, ls, log, wss, 'link');
+        this.responseCallback = this.linkResponseCb;
+        this.sortParams = {
+            firstCol: 'one',
+            firstDir: SortDir.desc,
+            secondCol: 'two',
+            secondDir: SortDir.asc,
+        };
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('LinkComponent initialized');
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('LinkComponent destroyed');
+    }
+
+    linkResponseCb(data: LinkTableResponse) {
+        this.log.debug('Link response received for ', data.links.length, 'links');
+    }
+}
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter-routing.module.ts b/web/gui2/src/main/webapp/app/view/meter/meter-routing.module.ts
new file mode 100644
index 0000000..83a432e
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter-routing.module.ts
@@ -0,0 +1,33 @@
+/*
+* Copyright 2018-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 { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { MeterComponent } from './meter/meter.component';
+
+
+const meterRoutes: Routes = [
+    {
+        path: '',
+        component: MeterComponent
+    }
+];
+
+@NgModule({
+    imports: [RouterModule.forChild(meterRoutes)],
+    exports: [RouterModule]
+})
+export class MeterRoutingModule { }
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter.module.ts b/web/gui2/src/main/webapp/app/view/meter/meter.module.ts
new file mode 100644
index 0000000..90263b2
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter.module.ts
@@ -0,0 +1,37 @@
+/*
+* Copyright 2018-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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SvgModule } from '../../fw/svg/svg.module';
+import { WidgetModule } from '../../fw/widget/widget.module';
+
+
+import { MeterRoutingModule } from './meter-routing.module';
+import { MeterComponent } from './meter/meter.component';
+
+import { FormsModule } from '@angular/forms';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    SvgModule,
+    MeterRoutingModule,
+    FormsModule,
+    WidgetModule
+  ],
+  declarations: [MeterComponent]
+})
+export class MeterModule { }
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.css b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.css
new file mode 100644
index 0000000..809ab49
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.css
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018-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 -- Meter View (layout) -- CSS file
+ */
+
+#ov-meter h2 {
+    display: inline-block;
+}
+
+#ov-meter div.ctrl-btns {
+}
+
+#ov-meter td {
+    text-align: center;
+}
+
+#ov-meter td.bands {
+    text-align: left;
+}
+
+#ov-meter td.right {
+    text-align: right;
+}
+
+#ov-meter .tabular-header {
+    text-align: left;
+}
+#ov-meter div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+#ov-meter div.summary-list td.bands {
+    padding-left: 36px;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.html b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.html
new file mode 100644
index 0000000..bfeb407
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.html
@@ -0,0 +1,101 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="ov-meter">
+    <div class="tabular-header">
+        <h2> Meter for Device {{id}} ({{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 routerLink="/device" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
+            </div>
+            <div routerLink="/flow" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
+            </div>
+            <div routerLink="/port" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
+            </div>
+            <div routerLink="/group" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
+            </div>
+            <div>
+                <onos-icon classes="{{ id ? 'current-view' :undefined}}" iconId="meterTable" iconSize="42"></onos-icon>
+            </div>
+            <div routerLink="/pipeconf" [queryParams]="{ devId: id }" routerLinkActive="active">
+                <onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
+            </div>
+        </div>
+
+        <div class="search">
+            <input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
+            <select [(ngModel)]="tableDataFilter.queryBy">
+                <option value="" disabled>Search By</option>
+                <option value="$">All Fields</option>
+                <option value="id">Meter ID</option>
+                <option value="app_id">App ID</option>
+                <option value="state">State</option>
+            </select>
+        </div>
+    </div>
+    <div class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="id" (click)="onSort('id')">Meter ID
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
+                    </td>
+                    <td colId="app_id" (click)="onSort('app_id')">App ID
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('app_id')"></onos-icon>
+                    </td>
+                    <td colId="state" (click)="onSort('state')">State
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
+                    </td>
+                    <td colId="packets" (click)="onSort('packets')">Packets
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('packets')"></onos-icon>
+                    </td>
+                    <td colId="bytes" (click)="onSort('bytes')">
+                        Bytes
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table>
+                <tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="5">{{annots.noRowsMsg}}</td>
+                </tr>
+                <ng-template ngFor let-meter [ngForOf]="tableData | filter : tableDataFilter">
+                    <tr (click)="selectCallback($event, meter)" [ngClass]="{selected: meter.id === selId, 'data-change': isChanged(meter.id)}">
+                        <td>{{meter.id}}</td>
+                        <td>{{meter.app_id}}</td>
+                        <td>{{meter.state}}</td>
+                        <td>{{meter.packets}}</td>
+                        <td>{{meter.bytes}}</td>
+                    </tr>
+                    <tr>
+                        <td class="bands" colspan="5" [innerHTML]="meter.bands"></td>
+                    </tr>
+                </ng-template>
+            </table>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.spec.ts b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.spec.ts
new file mode 100644
index 0000000..e17ff50
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.spec.ts
@@ -0,0 +1,196 @@
+/*
+* Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { FormsModule } from '@angular/forms';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { MeterComponent } from './meter.component';
+import { LogService } from '../../../log.service';
+import { DialogService } from '../../../fw/layer/dialog.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+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 { ThemeService } from '../../../fw/util/theme.service';
+import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
+import { UrlFnService } from '../../../fw/remote/urlfn.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { of, Subject } from 'rxjs';
+import { } from 'jasmine';
+import { RouterTestingModule } from '@angular/router/testing';
+
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+
+class MockDialogService { }
+
+class MockFnService { }
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockKeyService { }
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+    waiting() { }
+}
+
+class MockThemeService { }
+
+class MockUrlFnService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+
+/**
+ * ONOS GUI -- Meter Panel View -- Unit Tests
+ */
+
+describe('MeterComponent', () => {
+
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: MeterComponent;
+    let fixture: ComponentFixture<MeterComponent>;
+
+    const bundleObj = {
+        'core.view.Meter': {
+            test: 'test1'
+        }
+    };
+
+    const mockLion = (key) => {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+
+
+    beforeEach(async(() => {
+
+        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({
+            imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
+            declarations: [MeterComponent, IconComponent, TableFilterPipe],
+            providers: [
+                { provide: DialogService, useClass: MockDialogService },
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: KeyService, useClass: MockKeyService },
+                {
+                    provide: LionService, useFactory: (() => {
+                        return {
+                            bundle: ((bundleId) => mockLion),
+                            ubercache: new Array(),
+                            loadCbs: new Map<string, () => void>([])
+                        };
+                    })
+                },
+                { provide: LoadingService, useClass: MockLoadingService },
+                { provide: LogService, useValue: logSpy },
+                { 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(MeterComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+
+    it('should have a div.tabular-header inside a div#ov-meter', () => {
+        const metDe: DebugElement = fixture.debugElement;
+        const divDe = metDe.query(By.css('div#ov-meter div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.tabular-header', () => {
+        const metDe: DebugElement = fixture.debugElement;
+        const divDe = metDe.query(By.css('div#ov-meter div.tabular-header h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual(' Meter for Device  (0 Total )');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const metDe: DebugElement = fixture.debugElement;
+        const divDe = metDe.query(By.css('div#ov-meter div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+
+    it('should have a div.summary-list inside a div#ov-meter', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-meter div.summary-list'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-header inside a div.summary-list inside a div#ov-meter', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-meter div.summary-list div.table-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-body inside a div.summary-list inside a div#ov-meter', () => {
+        const hostDe: DebugElement = fixture.debugElement;
+        const divDe = hostDe.query(By.css('div#ov-meter div.summary-list div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts
new file mode 100644
index 0000000..e026e1c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts
@@ -0,0 +1,107 @@
+/*
+* Copyright 2018-present Open Networking Foundation
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { SortDir, TableBaseImpl, TableResponse } from '../../../fw/widget/table.base';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { LogService } from '../../../log.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { ActivatedRoute } from '@angular/router';
+
+/**
+* Model of the response from WebSocket
+*/
+interface MeterTableResponse extends TableResponse {
+    meters: Meter[];
+}
+
+/**
+* Model of the meter returned from the WebSocket
+*/
+interface Meter {
+    id: string;
+    appId: string;
+    state: string;
+    packets: string;
+    bytes: string;
+}
+
+/**
+ * ONOS GUI -- Meter View Component
+ */
+@Component({
+    selector: 'onos-meter',
+    templateUrl: './meter.component.html',
+    styleUrls: ['./meter.component.css', './meter.theme.css',
+        '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class MeterComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    id: string;
+    brief: boolean = true;
+
+    // TODO: Update for LION
+    deviceTip = 'Show device table';
+    detailTip = 'Switch to detail view';
+    flowTip = 'Show flow view for selected device';
+    portTip = 'Show port view for selected device';
+    groupTip = 'Show group view for selected device';
+    pipeconfTip = 'Show pipeconf view for selected device';
+
+    constructor(
+        protected fs: FnService,
+        protected log: LogService,
+        protected ls: LoadingService,
+        protected as: ActivatedRoute,
+        protected wss: WebSocketService,
+    ) {
+        super(fs, ls, log, wss, 'meter');
+        this.as.queryParams.subscribe(params => {
+            this.id = params['devId'];
+        });
+
+        this.payloadParams = {
+            devId: this.id
+        };
+
+        this.responseCallback = this.meterResponseCb;
+        this.sortParams = {
+            firstCol: 'id',
+            firstDir: SortDir.desc,
+            secondCol: 'app_id',
+            secondDir: SortDir.asc,
+        };
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('MeterComponent initialized');
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('MeterComponent destroyed');
+    }
+
+    meterResponseCb(data: MeterTableResponse) {
+        this.log.debug('Meter response received for ', data.meters.length, 'meter');
+    }
+
+    briefToggle() {
+        this.brief = !this.brief;
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.theme.css b/web/gui2/src/main/webapp/app/view/meter/meter/meter.theme.css
new file mode 100644
index 0000000..9f4dea4
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.theme.css
@@ -0,0 +1,59 @@
+/*
+ * 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 -- Meter View (theme) -- CSS file
+ */
+
+
+/* a "logical" row is made up of 2 "physical" rows -- color as such */
+#ov-meter tr:nth-child(4n + 1),
+#ov-meter tr:nth-child(4n + 2) {
+    background-color: #fbfbfb;
+}
+#ov-meter tr:nth-child(4n + 3),
+#ov-meter tr:nth-child(4n) {
+     background-color: #f4f4f4;
+ }
+
+/* highlighted color */
+#ov-meter tr:nth-child(4n + 1).data-change,
+#ov-meter tr:nth-child(4n + 2).data-change,
+#ov-meter tr:nth-child(4n + 3).data-change,
+#ov-meter tr:nth-child(4n).data-change {
+    background-color: #FDFFDC;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark #ov-meter tr:nth-child(4n + 1),
+.dark #ov-meter tr:nth-child(4n + 2) {
+    background-color: #333333;
+}
+.dark #ov-meter tr:nth-child(4n + 3),
+.dark #ov-meter tr:nth-child(4n) {
+    background-color: #3a3a3a;
+}
+
+.dark #ov-meter tr:nth-child(4n + 1).data-change,
+.dark #ov-meter tr:nth-child(4n + 2).data-change,
+.dark #ov-meter tr:nth-child(4n + 3).data-change,
+.dark #ov-meter tr:nth-child(4n).data-change {
+    background-color: #423708;
+}
+
+
diff --git a/web/gui2/src/main/webapp/app/view/port/port-routing.module.ts b/web/gui2/src/main/webapp/app/view/port/port-routing.module.ts
new file mode 100644
index 0000000..6ba681d
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port-routing.module.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { PortComponent } from './port/port.component';
+
+
+const portRoutes: Routes = [
+    {
+        path: '',
+        component: PortComponent
+    }
+];
+
+/**
+ * ONOS GUI -- Devices Tabular View Feature Routing Module - allows it to be lazy loaded
+ *
+ * See https://angular.io/guide/lazy-loading-ngmodules
+ */
+@NgModule({
+    imports: [RouterModule.forChild(portRoutes)],
+    exports: [RouterModule]
+})
+export class PortRoutingModule { }
diff --git a/web/gui2/src/main/webapp/app/view/port/port.module.ts b/web/gui2/src/main/webapp/app/view/port/port.module.ts
new file mode 100644
index 0000000..dd622dc
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port.module.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { PortComponent } from './port/port.component';
+import { SvgModule } from '../../fw/svg/svg.module';
+import { FormsModule } from '@angular/forms';
+import { WidgetModule } from '../../fw/widget/widget.module';
+import { PortRoutingModule } from './port-routing.module';
+import { PortDetailsComponent } from './portdetails/portdetails.component';
+
+@NgModule({
+    imports: [
+        CommonModule,
+        SvgModule,
+        PortRoutingModule,
+        FormsModule,
+        WidgetModule
+    ],
+    declarations: [PortComponent, PortDetailsComponent]
+})
+export class PortModule { }
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.css b/web/gui2/src/main/webapp/app/view/port/port/port.component.css
new file mode 100644
index 0000000..015d475
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.css
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018-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 -- Port View (layout) -- CSS file
+ */
+#ov-port .tabular-header {
+    text-align: left;
+}
+
+#ov-port h2 {
+    display: inline-block;
+}
+
+#ov-port div.ctrl-btns {
+}
+
+#ov-port div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+#ov-port td {
+    text-align: center;
+}
+
+#ov-port td.delta {
+    text-align: center;
+    font-weight: bold;
+}
+
+#ov-port td.delta:before {
+    content: "+";
+}
+
+#ov-port tr.no-data td {
+    text-align: center;
+}
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.html b/web/gui2/src/main/webapp/app/view/port/port/port.component.html
new file mode 100644
index 0000000..8625d1a
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.html
@@ -0,0 +1,136 @@
+<!--
+~ Copyright 2018-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.
+-->
+<!-- Port partial HTML -->
+<div id="ov-port">
+    <div class="tabular-header">
+        <h2>
+            Ports for Device {{devId}} ({{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 class="refresh" (click)="toggleNZState()">
+                <onos-icon classes="{{ isNz() ? 'refresh' :'active refresh'}}" iconId="nonzero" iconSize="42" toolTip="{{toggleNZTip}}">
+                </onos-icon>
+            </div>
+
+            <div class="refresh" (click)="toggleDeltaState()">
+                <onos-icon classes="{{ isDelta() ? 'active refresh' :'refresh'}}" iconId="delta" iconSize="42" toolTip="{{toggleDeltaTip}}"></onos-icon>
+            </div>
+
+            <div class="separator"></div>
+
+            <div routerLink="/device" [queryParams]="{ devId: devId }" routerLinkActive="active">
+                <onos-icon classes="{{ devId ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
+            </div>
+
+            <div routerLink="/flow" [queryParams]="{ devId: devId }" routerLinkActive="active">
+                <onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
+            </div>
+
+            <div>
+                <onos-icon classes="{{ devId ? 'current-view' :undefined}}" iconId="portTable" iconSize="42"></onos-icon>
+            </div>
+
+            <div routerLink="/group" [queryParams]="{ devId: devId }" routerLinkActive="active">
+                <onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/meter" [queryParams]="{ devId: devId }" routerLinkActive="active">
+                <onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/pipeconf" [queryParams]="{ devId: devId }" routerLinkActive="active">
+                <onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
+            </div>
+        </div>
+
+        <div class="search">
+            <input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
+            <select [(ngModel)]="tableDataFilter.queryBy">
+                <option value="" disabled>Search By</option>
+                <option value="$">All Fields</option>
+                <option value="id">Port ID</option>
+                <option value="pkt_rx">Pkts Received</option>
+                <option value="pkt_tx">Pkts Sent</option>
+                <option value="bytes_rx">Bytes Received</option>
+                <option value="bytes_tx">Bytes Sent</option>
+                <option value="pkt_rx_drp">Pkts RX Dropped</option>
+                <option value="pkt_rx_drp">Pkts TX Dropped</option>
+                <option value="duration">Duration (sec)</option>
+            </select>
+        </div>
+    </div>
+
+    <div class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="id" (click)="onSort('id')">Port ID
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
+                    </td>
+                    <td colId="pkt_rx" (click)="onSort('pkt_rx')">Pkts Received
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_rx')"></onos-icon>
+                    </td>
+                    <td colId="pkt_tx" (click)="onSort('pkt_tx')">Pkts Sent
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_tx')"></onos-icon>
+                    </td>
+                    <td colId="bytes_rx" (click)="onSort('bytes_rx')">Bytes Received
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes_rx')"></onos-icon>
+                    </td>
+                    <td colId="bytes_tx" (click)="onSort('bytes_tx')">Bytes Sent
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes_tx')"></onos-icon>
+                    </td>
+                    <td colId="pkt_rx_drp" (click)="onSort('pkt_rx_drp')">Pkts RX Dropped
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_rx_drp')"></onos-icon>
+                    </td>
+                    <td colId="pkt_tx_drp" (click)="onSort('pkt_tx_drp')">Pkts TX Dropped
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_tx_drp')"></onos-icon>
+                    </td>
+                    <td colId="duration" (click)="onSort('duration')">Duration (sec)
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('duration')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table>
+                <tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="9">{{annots.noRowsMsg}}</td>
+                </tr>
+
+                <tr *ngFor="let port of tableData | filter : tableDataFilter" (click)="selectCallback($event, port)" [ngClass]="{selected: port.id === selId, 'data-change': isChanged(port.id)}">
+                    <td>{{port.id}}</td>
+                    <td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_rx}}</td>
+                    <td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_tx}}</td>
+                    <td [ngClass]="(isDelta() ? 'delta' : '')">{{port.bytes_rx}}</td>
+                    <td [ngClass]="(isDelta() ? 'delta' : '')">{{port.bytes_tx}}</td>
+                    <td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_rx_drp}}</td>
+                    <td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_tx_drp}}</td>
+                    <td [ngClass]="(isDelta() ? 'delta' : '')">{{port.duration}}</td>
+                </tr>
+            </table>
+        </div>
+        <onos-portdetails class="floatpanels" id="{{ selId }}" devId="{{devId}}" (closeEvent)="deselectRow($event)"></onos-portdetails>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.spec.ts b/web/gui2/src/main/webapp/app/view/port/port/port.component.spec.ts
new file mode 100644
index 0000000..233b34e
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.spec.ts
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PortComponent } from './port.component';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs/index';
+import { FnService } from '../../../fw/util/fn.service';
+import { LogService } from '../../../log.service';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
+import { GlyphService } from '../../../fw/svg/glyph.service';
+import { IconService } from '../../../fw/svg/icon.service';
+import { KeyService } from '../../../fw/util/key.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { MastService } from '../../../fw/mast/mast.service';
+import { NavService } from '../../../fw/nav/nav.service';
+import { ThemeService } from '../../../fw/util/theme.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { PortDetailsComponent } from '../portdetails/portdetails.component';
+import { PrefsService } from '../../../fw/util/prefs.service';
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockPrefsService {
+    setPrefs() { }
+    getPrefs() { }
+    asNumbers() { }
+    updatePrefs() { }
+}
+
+class MockGlyphService { }
+
+class MockKeyService { }
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+}
+
+class MockNavService { }
+
+class MockMastService { }
+
+class MockThemeService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+    sendEvent() { }
+}
+
+/**
+ * ONOS GUI -- Flow View Module - Unit Tests
+ */
+
+
+describe('PortComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: PortComponent;
+    let fixture: ComponentFixture<PortComponent>;
+
+    beforeEach(async(() => {
+        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({
+            imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
+            declarations: [PortComponent, IconComponent, TableFilterPipe, PortDetailsComponent],
+            providers: [
+                { 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: PrefsService, useClass: MockPrefsService },
+                { provide: LogService, useValue: logSpy },
+                { provide: ThemeService, useClass: MockThemeService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        }).compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(PortComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div.tabular-header inside a div#ov-port', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div#ov-port div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.tabular-header', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div#ov-port div.tabular-header h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual(' Ports for Device  (0 Total) ');
+    });
+
+    it('should have .table-header with "Port ID..."', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div#ov-port div.table-header'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual(
+            'Port ID Pkts Received Pkts Sent Bytes Received Bytes Sent Pkts RX Dropped Pkts TX Dropped Duration (sec) ');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div#ov-port div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-body ', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div#ov-port  div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.ts b/web/gui2/src/main/webapp/app/view/port/port/port.component.ts
new file mode 100644
index 0000000..1c86c9c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.ts
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { SortDir, TableBaseImpl, TableResponse } from '../../../fw/widget/table.base';
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { LogService } from '../../../log.service';
+import { ActivatedRoute } from '@angular/router';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { PrefsService } from '../../../fw/util/prefs.service';
+
+/**
+ * Model of the response from WebSocket
+ */
+interface PortTableResponse extends TableResponse {
+    ports: Port[];
+}
+
+/**
+ * Model of the ports returned from the WebSocket
+ */
+interface Port {
+    id: string;
+    pktsRecieved: string;
+    pktsSent: string;
+    byteRecieved: string;
+    byteSent: string;
+    pktsRxDropped: string;
+    pktsTxDropped: string;
+    duration: string;
+}
+
+interface FilterToggleState {
+    devId: string;
+    nzFilter: boolean;
+    showDelta: boolean;
+}
+
+const defaultPortPrefsState = {
+    nzFilter: 1,
+    showDelta: 0,
+};
+
+/**
+ * ONOS GUI -- Port View Component
+ */
+@Component({
+    selector: 'onos-port',
+    templateUrl: './port.component.html',
+    styleUrls: ['./port.component.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class PortComponent extends TableBaseImpl implements OnInit, OnDestroy {
+    devId: string;
+    nzFilter: boolean = true;
+    showDelta: boolean = false;
+    prefsState = {};
+    toggleState: FilterToggleState;
+
+    restorePrefsConfig; // Function
+
+    deviceTip = 'Show device table';
+    flowTip = 'Show flow view for this device';
+    groupTip = 'Show group view for this device';
+    meterTip = 'Show meter view for selected device';
+    pipeconfTip = 'Show pipeconf view for selected device';
+    toggleDeltaTip = 'Toggle port delta statistics';
+    toggleNZTip = 'Toggle non zero port statistics';
+
+    constructor(protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected ar: ActivatedRoute,
+        protected wss: WebSocketService,
+        protected prefs: PrefsService,
+    ) {
+        super(fs, ls, log, wss, 'port');
+        this.ar.queryParams.subscribe(params => {
+            this.devId = params['devId'];
+
+        });
+
+        this.payloadParams = {
+            devId: this.devId
+        };
+
+        this.responseCallback = this.portResponseCb;
+        this.restorePrefsConfig = this.restoreConfigFromPrefs;
+
+        this.sortParams = {
+            firstCol: 'id',
+            firstDir: SortDir.desc,
+            secondCol: 'pkt_rx',
+            secondDir: SortDir.asc,
+        };
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('PortComponent initialized');
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('PortComponent destroyed');
+    }
+
+    portResponseCb(data: PortTableResponse) {
+        this.log.debug('Port response received for ', data.ports.length, 'port');
+    }
+
+    isNz(): boolean {
+        return this.nzFilter;
+    }
+
+    isDelta(): boolean {
+        return this.showDelta;
+    }
+
+    toggleNZState(b?: any) {
+        if (b === undefined) {
+            this.nzFilter = !this.nzFilter;
+        } else {
+            this.nzFilter = b;
+        }
+        this.payloadParams = this.filterToggleState();
+        this.updatePrefsState('nzFilter', this.nzFilter);
+        this.forceRefesh();
+    }
+
+    toggleDeltaState(b?: any) {
+        if (b === undefined) {
+            this.showDelta = !this.showDelta;
+        } else {
+            this.showDelta = b;
+        }
+
+        this.payloadParams = this.filterToggleState();
+        this.updatePrefsState('showDelta', this.showDelta);
+        this.forceRefesh();
+    }
+
+    updatePrefsState(what: any, b: any) {
+        this.prefsState[what] = b ? 1 : 0;
+        this.prefs.setPrefs('port_prefs', this.prefsState);
+    }
+
+    filterToggleState(): FilterToggleState {
+        return this.toggleState = {
+            devId: this.devId,
+            nzFilter: this.nzFilter,
+            showDelta: this.showDelta,
+        };
+    }
+
+    forceRefesh() {
+        this.requestTableData();
+    }
+
+    restoreConfigFromPrefs() {
+        this.prefsState = this.prefs.asNumbers(
+            this.prefs.getPrefs('port_prefs', defaultPortPrefsState, )
+        );
+
+        this.log.debug('Port - Prefs State:', this.prefsState);
+        this.toggleDeltaState(this.prefsState['showDelta']);
+        this.toggleNZState(this.prefsState['nzFilter']);
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.css b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.css
new file mode 100644
index 0000000..225679d
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.css
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018-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.
+ */
+
+#port-details-panel.floatpanel {
+    z-index: 0;
+    padding-top: 10px;
+    font-size: 10pt;
+    top: 185px;
+}
+
+#port-details-panel .container {
+    padding: 8px 12px;
+}
+
+#port-details-panel .close-btn {
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    cursor: pointer;
+}
+
+#port-details-panel .port-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#port-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+    font-weight: bold;
+    font-size: 16pt;
+}
+
+#port-details-panel h2 input {
+    font-size: 0.90em;
+    width: 106%;
+}
+
+#port-details-panel .actionBtns div {
+    padding: 12px 6px;
+}
+
+#port-details-panel hr {
+    clear: both;
+    width: 100%;
+    margin: 2px auto;
+}
+
+#port-details-panel .top-tables {
+    font-size: 10pt;
+    white-space: nowrap;
+}
+
+#port-details-panel td.label {
+    font-weight: bold;
+    text-align: right;
+    padding-right: 6px;
+}
+
+#port-details-panel .bottom {
+    border-spacing: 0;
+    height: 400px;
+    width: 520px;
+    overflow: auto;
+    display: block;
+}
+
+#port-details-panel .top div.left {
+    float: left;
+    text-align: left;
+    padding: 0 10px 0 0;
+}
+
+#port-details-panel .top div.right {
+    display: inline-block;
+}
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.html b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.html
new file mode 100644
index 0000000..36fa847
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.html
@@ -0,0 +1,60 @@
+<!--
+~ 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.
+-->
+<div id="port-details-panel" class="floatpanel" [@portDetailsState]="id!=='' && !closed">
+    <div class="container">
+        <div class="top">
+            <div class="close-btn">
+                <onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
+            </div>
+            <div class="port-icon">
+                <onos-icon classes="details-icon" iconId="m_ports" [iconSize]="42"></onos-icon>
+            </div>
+            <h2>{{detailsData.devId}} port {{detailsData.id}}</h2>
+            <div class="top-content">
+                <div class="top-tables">
+                    <div class="left">
+                        <table>
+                            <tbody>
+                                <tr>
+                                    <td class="label" width="110">ID :</td>
+                                    <td class="value" width="80">{{detailsData.id}}</td>
+                                </tr>
+                                <tr>
+                                    <td class="label" width="110">Device :</td>
+                                    <td class="value" width="80">{{detailsData.devId}}</td>
+                                </tr>
+                                <tr>
+                                    <td class="label" width="110">Type :</td>
+                                    <td class="value" width="80">{{detailsData.type}}</td>
+                                </tr>
+                                <tr>
+                                    <td class="label" width="110">Speed :</td>
+                                    <td class="value" width="80">{{detailsData.speed}}</td>
+                                </tr>
+                                <tr>
+                                    <td class="label" width="110">Enabled :</td>
+                                    <td class="value" width="80">{{detailsData.enabled}}</td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+            <hr>
+        </div>
+        <div class="bottom"></div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.spec.ts
new file mode 100644
index 0000000..18dc37b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.spec.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PortDetailsComponent } from './portdetails.component';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs/index';
+import { LogService } from '../../../log.service';
+import { FnService } from '../../../fw/util/fn.service';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { IconService } from '../../../fw/svg/icon.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    classes = 'active-close';
+    loadIconDef() { }
+}
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+describe('PortdetailsComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: PortDetailsComponent;
+    let fixture: ComponentFixture<PortDetailsComponent>;
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'panel' });
+        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({
+            imports: [BrowserAnimationsModule],
+            declarations: [PortDetailsComponent, IconComponent],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: LogService, useValue: logSpy },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+
+        }).compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(PortDetailsComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have an div.close-btn div.top inside a div.container', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div.container div.top div.close-btn'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.port-icon inside a div.top inside a div.container', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div.container div.top div.port-icon'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('');
+    });
+
+    it('should have a div.top-content inside a div.top inside a div.container', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div.container div.top div.top-content'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a dev.left inside a div.top-tables inside a div.top-content', () => {
+        const portDe: DebugElement = fixture.debugElement;
+        const divDe = portDe.query(By.css('div.top-content div.top-tables div.left'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('ID :Device :Type :Speed :Enabled :');
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.ts b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.ts
new file mode 100644
index 0000000..1fd62d3
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import { DetailsPanelBaseImpl } from '../../../fw/widget/detailspanel.base';
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { LogService } from '../../../log.service';
+import { IconService } from '../../../fw/svg/icon.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+
+/**
+ * The details view when a port row is clicked from the Port view
+ *
+ * This is expected to be passed an 'id' and it makes a call
+ * to the WebSocket with an portDetailsRequest, and gets back an
+ * portDetailsResponse.
+ *
+ * The animated fly-in is controlled by the animation below
+ * The portDetailsState is attached to port-details-panel
+ * and is false (flies out) when id='' and true (flies in) when
+ * id has a value
+ */
+@Component({
+    selector: 'onos-portdetails',
+    templateUrl: './portdetails.component.html',
+    styleUrls: ['./portdetails.component.css', '../../../fw/widget/panel.css', '../../../fw/widget/panel-theme.css'],
+    animations: [
+        trigger('portDetailsState', [
+            state('true', style({
+                transform: 'translateX(-100%)',
+                opacity: '100'
+            })),
+            state('false', style({
+                transform: 'translateX(0%)',
+                opacity: '0'
+            })),
+            transition('0 => 1', animate('100ms ease-in')),
+            transition('1 => 0', animate('100ms ease-out'))
+        ])
+    ]
+})
+export class PortDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
+    @Input() id: string;
+    @Input() devId: string;
+
+    constructor(protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected is: IconService,
+        protected wss: WebSocketService
+    ) {
+        super(fs, ls, log, wss, 'port');
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('App Details Component initialized:', this.id);
+    }
+
+    /**
+     * Stop listening to appDetailsResponse on WebSocket
+     */
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('App Details Component destroyed');
+    }
+
+    /**
+     * Details Panel Data Request on row selection changes
+     * Should be called whenever id changes
+     * If id or devId is empty, no request is made
+     */
+    ngOnChanges() {
+        if (this.id === '' || this.devId === '') {
+            return '';
+        } else {
+            const query = {
+                'id': this.id,
+                'devId': this.devId
+            };
+            this.requestDetailsPanelData(query);
+        }
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel-routing.module.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel-routing.module.ts
new file mode 100644
index 0000000..dbbee7f
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { TunnelComponent } from './tunnel/tunnel.component';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: TunnelComponent
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class TunnelRoutingModule { }
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.spec.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.spec.ts
new file mode 100644
index 0000000..694f645
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.spec.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2018-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 { TunnelModule } from './tunnel.module';
+
+describe('TunnelModule', () => {
+  let tunnelModule: TunnelModule;
+
+  beforeEach(() => {
+    tunnelModule = new TunnelModule();
+  });
+
+  it('should create an instance', () => {
+    expect(tunnelModule).toBeTruthy();
+  });
+});
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts
new file mode 100644
index 0000000..56e03b6
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018-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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { TunnelRoutingModule } from './tunnel-routing.module';
+import { TunnelComponent } from './tunnel/tunnel.component';
+import { SvgModule } from '../../fw/svg/svg.module';
+import { WidgetModule } from '../../fw/widget/widget.module';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    TunnelRoutingModule,
+    SvgModule,
+    WidgetModule
+  ],
+  declarations: [
+    TunnelComponent
+  ]
+})
+export class TunnelModule { }
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.css b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.css
new file mode 100644
index 0000000..73472ff
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.css
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018-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.
+ */
+ 
+ #ov-tunnel .tabular-header {
+    text-align: left;
+}
+#ov-tunnel div.summary-list .table-header td {
+    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;
+    background-color: #e5e5e6;
+}
+
+#ov-tunnel h2 {
+    display: inline-block;
+}
+
+#ov-tunnel th, td {
+    text-align: left;
+    padding:  8px;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.html b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.html
new file mode 100644
index 0000000..5829acf
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.html
@@ -0,0 +1,74 @@
+<!--
+~ Copyright 2018-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.
+-->
+<div id="ov-tunnel">
+    <div class="tabular-header">
+        <h2>Tunnels ({{tableData.length}} 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>
+    </div>
+    <div class="summary-list" onosTableResize>
+        <div class="table-header">
+            <table>
+                <tr>
+                    <td colId="id" (click)="onSort('id')">Id
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
+                    </td>
+                    <td colId="name" (click)="onSort('name')">Name
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('name')"></onos-icon>
+                    </td>
+                    <td colId="port1" (click)="onSort('port1')">Port 1
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('port1')"></onos-icon>
+                    </td>
+                    <td colId="port2" (click)="onSort('port2')">Port 2
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('port2')"></onos-icon>
+                    </td>
+                    <td colId="type" (click)="onSort('type')">Type
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('type')"></onos-icon>
+                    </td>
+                    <td colId="groupId" (click)="onSort('groupId')">Group Id
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('groupId')"></onos-icon>
+                    </td>
+                    <td colId="bandwidth" (click)="onSort('bandwidth')">Bandwidth
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bandwidth')"></onos-icon>
+                    </td>
+                    <td colId="path" (click)="onSort('path')">Path
+                        <onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('path')"></onos-icon>
+                    </td>
+                </tr>
+            </table>
+        </div>
+        <div class="table-body">
+            <table>
+                <tr *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="8">{{ annots.noRowsMsg }}</td>
+                </tr>
+                <tr *ngFor="let tunnel of tableData">
+                    <td>{{tunnel.id}}</td>
+                    <td>{{tunnel.name}}</td>
+                    <td>{{tunnel.one}}</td>
+                    <td>{{tunnel.two}}</td>
+                    <td>{{tunnel.type}}</td>
+                    <td>{{tunnel.group_id}}</td>
+                    <td>{{tunnel.bandwidth}}</td>
+                    <td>{{tunnel.path}}</td>
+                </tr>
+            </table>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.spec.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.spec.ts
new file mode 100644
index 0000000..3b2611e
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.spec.ts
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2018-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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TunnelComponent } from './tunnel.component';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { FnService } from '../../../fw/util/fn.service';
+import { LogService } from '../../../log.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+import { IconService } from '../../../fw/svg/icon.service';
+import { IconComponent } from '../../../fw/svg/icon/icon.component';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockLoadingService {
+    startAnim() { }
+    stop() { }
+    waiting() { }
+}
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+/**
+ * ONOS GUI -- Tunnel View Module - Unit Tests
+ */
+describe('TunnelComponent', () => {
+
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: TunnelComponent;
+    let fixture: ComponentFixture<TunnelComponent>;
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+        windowMock = <any>{
+            location: <any>{
+                tunnelname: 'foo',
+                tunnel: '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: [TunnelComponent, IconComponent],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: IconService, useClass: MockIconService },
+                { provide: LoadingService, useClass: MockLoadingService },
+                { provide: LogService, useValue: logSpy },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+            ]
+        })
+            .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(TunnelComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div.tabular-header inside a div#ov-tunnel', () => {
+        const tunnelDe: DebugElement = fixture.debugElement;
+        const divDe = tunnelDe.query(By.css('div#ov-tunnel div.tabular-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a h2 inside the div.tabular-header', () => {
+        const tunnelDe: DebugElement = fixture.debugElement;
+        const divDe = tunnelDe.query(By.css('div#ov-tunnel div.tabular-header h2'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('Tunnels (0 total)');
+    });
+
+    it('should have a refresh button inside the div.tabular-header', () => {
+        const tunnelDe: DebugElement = fixture.debugElement;
+        const divDe = tunnelDe.query(By.css('div#ov-tunnel div.tabular-header div.ctrl-btns div.refresh'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.summary-list inside a div#ov-tunnel', () => {
+        const tunnelDe: DebugElement = fixture.debugElement;
+        const divDe = tunnelDe.query(By.css('div#ov-tunnel div.summary-list'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-header inside a div.summary-list inside a div#ov-tunnel', () => {
+        const tunnelDe: DebugElement = fixture.debugElement;
+        const divDe = tunnelDe.query(By.css('div#ov-tunnel div.summary-list div.table-header'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.table-body inside a div.summary-list inside a div#ov-tunnel', () => {
+        const tunnelDe: DebugElement = fixture.debugElement;
+        const divDe = tunnelDe.query(By.css('div#ov-tunnel div.summary-list div.table-body'));
+        expect(divDe).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.ts
new file mode 100644
index 0000000..bbd4b67
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { TableResponse, TableBaseImpl, SortDir } from '../../../fw/widget/table.base';
+import { FnService } from '../../../fw/util/fn.service';
+import { LoadingService } from '../../../fw/layer/loading.service';
+import { LogService } from '../../../log.service';
+import { WebSocketService } from '../../../fw/remote/websocket.service';
+
+/**
+ * Model of the response from WebSocket
+ */
+interface TunnelTableResponse extends TableResponse {
+    tunnels: Tunnel[];
+}
+
+/**
+ * Model of the tunnels returned from the WebSocket
+ */
+interface Tunnel {
+    id: string;
+    name: string;
+    port1: string;
+    port2: string;
+    type: string;
+    groupId: string;
+    bandwidth: string;
+    path: string;
+}
+
+/**
+ * ONOS GUI -- Tunnel View Component
+ */
+@Component({
+    selector: 'onos-tunnel',
+    templateUrl: './tunnel.component.html',
+    styleUrls: ['./tunnel.component.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class TunnelComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+    ) {
+        super(fs, ls, log, wss, 'tunnel');
+        this.responseCallback = this.tunnelResponseCb;
+        this.sortParams = {
+            firstCol: 'id',
+            firstDir: SortDir.desc,
+            secondCol: 'name',
+            secondDir: SortDir.asc,
+        };
+    }
+
+    ngOnInit() {
+        this.init();
+        this.log.debug('TunnelComponent initialized');
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('TunnelComponent destroyed');
+    }
+
+    tunnelResponseCb(data: TunnelTableResponse) {
+        this.log.debug('Tunnel response received for ', data.tunnels.length, 'tunnels');
+    }
+
+}