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/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');
+    }
+
 }