Added in summary panel of GUI 2 topo view

Change-Id: I5d325ff1dc2940e08ab9e6f970768b5fd0885e10
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.ts
index 56d62ab..c51af7a 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.ts
@@ -190,19 +190,22 @@
     }
 
     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,
-            };
+        const sz = size || 40;
+        const ovr = !!overlay;
+        const xns = this.fs.isA(trans);
+
+        const glyphUse = elem
+            .append('use')
+            .attr('width', sz)
+            .attr('height', sz)
+            .attr('class', 'glyph')
+            .attr('xlink:href', '#' + glyphId)
+            .classed('overlay', ovr);
 
         if (xns) {
-            atr.class = this.sus.translate(trans);
+            glyphUse.attr('transform', this.sus.translate(trans));
         }
-        return elem.append('use').attr(atr).classed('overlay', ovr);
+
+        return glyphUse;
     }
 }
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
index 6107d16..7510486 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
@@ -18,6 +18,34 @@
 import { LogService } from '../log.service';
 import * as d3 from 'd3';
 
+
+// --- Ordinal scales for 7 values.
+// TODO: migrate these colors to the theme service.
+
+// Colors per Mojo-Design's color palette.. (version one)
+//               blue       red        dk grey    steel      lt blue    lt red     lt grey
+// var lightNorm = ['#5b99d2', '#d05a55', '#716b6b', '#7e9aa8', '#66cef6', '#db7773', '#aeada8' ],
+//     lightMute = ['#a8cceb', '#f1a7a7', '#b9b5b5', '#bdcdd5', '#a8e9fd', '#f8c9c9', '#d7d6d4' ],
+
+// Colors per Mojo-Design's color palette.. (version two)
+//               blue       lt blue    red        green      brown      teal       lime
+const lightNorm: string[] = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
+const lightMute: string[] = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
+
+const darkNorm: string[] = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
+const darkMute: string[] = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
+
+const colors = {
+    light: {
+        norm: d3.scaleOrdinal().range(lightNorm),
+        mute: d3.scaleOrdinal().range(lightMute),
+    },
+    dark: {
+        norm: d3.scaleOrdinal().range(darkNorm),
+        mute: d3.scaleOrdinal().range(darkMute),
+    },
+};
+
 /**
  * ONOS GUI -- SVG -- Util Service
  *
@@ -27,44 +55,13 @@
     providedIn: 'root',
 })
 export class SvgUtilService {
-    lightNorm: string[];
-    lightMute: string[];
-    darkNorm: string[];
-    darkMute: string[];
-    colors: any;
 
     constructor(
         private fs: FnService,
         private log: LogService
     ) {
 
-        // --- Ordinal scales for 7 values.
-        // TODO: migrate these colors to the theme service.
 
-        // Colors per Mojo-Design's color palette.. (version one)
-        //               blue       red        dk grey    steel      lt blue    lt red     lt grey
-        // var lightNorm = ['#5b99d2', '#d05a55', '#716b6b', '#7e9aa8', '#66cef6', '#db7773', '#aeada8' ],
-        //     lightMute = ['#a8cceb', '#f1a7a7', '#b9b5b5', '#bdcdd5', '#a8e9fd', '#f8c9c9', '#d7d6d4' ],
-
-        // Colors per Mojo-Design's color palette.. (version two)
-        //               blue       lt blue    red        green      brown      teal       lime
-        this.lightNorm = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
-        this.lightMute = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
-
-        this.darkNorm = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
-        this.darkMute = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
-
-
-        this.colors = {
-            light: {
-                norm: d3.scaleOrdinal().range(this.lightNorm),
-                mute: d3.scaleOrdinal().range(this.lightMute),
-            },
-            dark: {
-                norm: d3.scaleOrdinal().range(this.darkNorm),
-                mute: d3.scaleOrdinal().range(this.darkMute),
-            },
-        };
 
         this.log.debug('SvgUtilService constructed');
     }
@@ -95,10 +92,10 @@
             // NOTE: since we are lazily assigning domain ids, we need to
             //       get the color from all 4 scales, to keep the domains
             //       in sync.
-            const ln = this.colors.light.norm(id);
-            const lm = this.colors.light.mute(id);
-            const dn = this.colors.dark.norm(id);
-            const dm = this.colors.dark.mute(id);
+            const ln = colors.light.norm(id);
+            const lm = colors.light.mute(id);
+            const dn = colors.dark.norm(id);
+            const dm = colors.dark.mute(id);
             if (theme === 'dark') {
                 return muted ? dm : dn;
             } else {
@@ -126,32 +123,27 @@
                     muted = k % 2;
                     what = muted ? ' muted' : ' normal';
                     theme = k < 2 ? 'light' : 'dark';
-                    dom.forEach(function (id, i) {
+                    dom.forEach((id, i) => {
                         const x = i * 20;
                         const y = k * 20;
-                        const f = getColor(id, muted, theme);
-                        g.append('circle').attr({
-                            cx: x,
-                            cy: y,
-                            r: 5,
-                            fill: f,
-                        });
+                        g.append('circle')
+                            .attr('cx', x)
+                            .attr('cy', y)
+                            .attr('r', 5)
+                            .attr('fill', getColor(id, muted, theme));
                     });
-                    g.append('rect').attr({
-                        x: 140,
-                        y: k * 20 - 5,
-                        width: 32,
-                        height: 10,
-                        rx: 2,
-                        fill: '#888',
-                    });
+                    g.append('rect')
+                        .attr('x', 140)
+                        .attr('y', k * 20 - 5)
+                        .attr('width', 32)
+                        .attr('height', 10)
+                        .attr('rx', 2)
+                        .attr('fill', '#888');
                     g.append('text').text(theme + what)
-                        .attr({
-                            x: 142,
-                            y: k * 20 + 2,
-                            fill: 'white',
-                        })
-                        .style('font-size', '4pt');
+                        .attr('x', 142)
+                        .attr('y', k * 20 + 2)
+                        .attr('fill', 'white');
+                        // .style('font-size', '4pt');
                 }
             }
         }
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
index fdf08f9..8cbca5a 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
@@ -117,7 +117,7 @@
 */
         // Remove zoom on double click (prevents a
         // false zoom navigating regions)
-        this.settings.svg.on('dblclick.zoom', null);
+        // this.settings.svg.on('dblclick.zoom', null);
 
         return this.zoomer;
     }
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts
index 0377c47..628f84f 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts
@@ -37,10 +37,10 @@
 
     on: boolean;
 
-    constructor(
+    protected constructor(
         protected fs: FnService,
         protected ls: LoadingService,
-        protected log: LogService,
+        protected log: LogService
     ) {
 //        this.log.debug('Panel base class constructed');
     }
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/table.base.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/table.base.ts
index 41ce239..e011057 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/table.base.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/table.base.ts
@@ -91,13 +91,13 @@
     autoRefresh: boolean = true;
     autoRefreshTip: string = 'Toggle auto refresh'; // TODO: get LION string
 
-    private root: string;
-    private req: string;
-    private resp: string;
+    readonly root: string;
+    readonly req: string;
+    readonly resp: string;
     private refreshPromise: any = null;
     private handlers: string[] = [];
 
-    constructor(
+    protected constructor(
         protected fs: FnService,
         protected ls: LoadingService,
         protected log: LogService,
diff --git a/web/gui2/BUILD b/web/gui2/BUILD
index 28b0624..0702802 100644
--- a/web/gui2/BUILD
+++ b/web/gui2/BUILD
@@ -93,7 +93,7 @@
             ":src/main/webapp/login.html",
             ":src/main/webapp/nav.html",
             ":src/main/webapp/not-ready.html",
-            ":src/main/webapp/onos.theme.css",
+            ":src/main/webapp/onos.global.css",
         ],
 )
 
diff --git a/web/gui2/angular.json b/web/gui2/angular.json
index 3563d9e..579fbf4 100644
--- a/web/gui2/angular.json
+++ b/web/gui2/angular.json
@@ -20,7 +20,7 @@
               "src/main/webapp/data",
               "src/main/webapp/app/fw/layer/loading.service.css"
             ],
-            "styles": ["src/main/webapp/onos.theme.css"],
+            "styles": ["src/main/webapp/onos.global.css"],
             "scripts": []
           },
           "configurations": {
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.html
deleted file mode 100644
index 6f1bb4b..0000000
--- a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!--
-~ 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.
--->
-<svg:g id="topo-zoomlayer">
-    <svg:g onos-backgroundsvg/>
-    <svg:g onos-forcesvg/>
-</svg:g>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.spec.ts
deleted file mode 100644
index 60a1997..0000000
--- a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.spec.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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 { of } from 'rxjs';
-
-import { ZoomLayerSvgComponent } from './zoomlayersvg.component';
-import {
-    FnService,
-    LogService,
-    ZoomService
-} from 'gui2-fw-lib';
-
-class MockActivatedRoute extends ActivatedRoute {
-    constructor(params: Params) {
-        super();
-        this.queryParams = of(params);
-    }
-}
-
-class MockZoomService {}
-
-/**
- * ONOS GUI -- Topology View Zoom Layer -- Unit Tests
- */
-describe('ZoomLayerSvgComponent', () => {
-    let fs: FnService;
-    let ar: MockActivatedRoute;
-    let windowMock: Window;
-    let logServiceSpy: jasmine.SpyObj<LogService>;
-    let component: ZoomLayerSvgComponent;
-    let fixture: ComponentFixture<ZoomLayerSvgComponent>;
-
-    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: [ ZoomLayerSvgComponent ],
-            providers: [
-                { provide: FnService, useValue: fs },
-                { provide: LogService, useValue: logSpy },
-                { provide: 'Window', useValue: windowMock },
-                { provide: ZoomService, useClass: MockZoomService }
-            ]
-        })
-        .compileComponents();
-        logServiceSpy = TestBed.get(LogService);
-    }));
-
-    beforeEach(() => {
-        fixture = TestBed.createComponent(ZoomLayerSvgComponent);
-        component = fixture.componentInstance;
-        fixture.detectChanges();
-    });
-
-    it('should create', () => {
-        expect(component).toBeTruthy();
-    });
-});
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.ts
deleted file mode 100644
index e0c85ed..0000000
--- a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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 } from '@angular/core';
-import {
-    FnService,
-    LogService,
-    ZoomService, Zoomer, ZoomOpts
-} from 'gui2-fw-lib';
-
-/**
- * ONOS GUI -- Topology Zoom Layer View.
- * View that contains the 'Force graph' message
- *
- * This component is an SVG snippet that expects to be in an SVG element with a view box of 1000x1000
- *
- * It should be added to a template with a tag like <svg:g onos-zoomlayer />
- */
-@Component({
-  selector: '[onos-zoomlayer]',
-  templateUrl: './zoomlayersvg.component.html',
-  styleUrls: ['./zoomlayersvg.component.css']
-})
-export class ZoomLayerSvgComponent implements OnInit {
-    zoomer: Zoomer;
-    zoomEventListeners: any[];
-
-    constructor(
-        protected fs: FnService,
-        protected log: LogService,
-        protected zs: ZoomService
-    ) {
-        this.log.debug('ZoomLayerSvgComponent constructed');
-    }
-
-    ngOnInit() {
-
-    }
-
-    createZoomer(options: ZoomOpts) {
-        // need to wrap the original zoom callback to extend its behavior
-        const origCallback = this.fs.isF(options.zoomCallback) ? options.zoomCallback : () => {};
-
-        options.zoomCallback = () => {
-            origCallback([0, 0], 1);
-
-            this.zoomEventListeners.forEach((ev) => ev(this.zoomer));
-        };
-
-        this.zoomer = this.zs.createZoomer(options);
-        return this.zoomer;
-    }
-
-    getZoomer() {
-        return this.zoomer;
-    }
-
-    findZoomEventListener(ev) {
-        for (let i = 0, len = this.zoomEventListeners.length; i < len; i++) {
-            if (this.zoomEventListeners[i] === ev) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    addZoomEventListener(callback) {
-        this.zoomEventListeners.push(callback);
-    }
-
-    removeZoomEventListener(callback) {
-        const evIndex = this.findZoomEventListener(callback);
-
-        if (evIndex !== -1) {
-            this.zoomEventListeners.splice(evIndex);
-        }
-    }
-
-    adjustmentScale(min: number, max: number): number {
-        let _scale = 1;
-        const size = (min + max) / 2;
-
-        if (size * this.scale() < max) {
-            _scale = min / (size * this.scale());
-        } else if (size * this.scale() > max) {
-            _scale = min / (size * this.scale());
-        }
-
-        return _scale;
-    }
-
-    scale(): number {
-        return this.zoomer.scale();
-    }
-
-    panAndZoom(translate: number[], scale: number, transition?: number) {
-        this.zoomer.panZoom(translate, scale, transition);
-    }
-
-}
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.html b/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.html
index 1ff43e6..af8f6a0 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.html
@@ -14,7 +14,7 @@
 ~ limitations under the License.
 -->
 <div id="topo2-p-detail" class="floatpanel topo2-p"
-     style="opacity: 1; right: 20px; width: 260px; top: 350px;" [@detailsPanelState]="!on">
+     style="opacity: 1; right: 20px; width: 260px; top: 350px;" [@detailsPanelState]="on">
     <div class="header">
         <div class="icon clickable">
             <svg>
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.ts b/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.ts
index c8bf95b..0fcf179 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/details/details.component.ts
@@ -63,6 +63,7 @@
     }
 
     ngOnInit() {
+        this.on = false;
     }
 
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.html b/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.html
index e71b58d..d781418 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.html
@@ -14,63 +14,7 @@
 ~ limitations under the License.
 -->
 <div id="topo2-p-summary" class="floatpanel topo2-p"
-     style="opacity: 1; right: 20px; width: 260px;" [@summaryPanelState]="!on">
-    <div class="header">
-        <div class="icon">
-            <svg>
-                <use width="24" height="24" class="glyph" xlink:href="#bird"
-                     transform="translate(1,1)"></use>
-            </svg>
-        </div>
-        <h2>ONOS Summary</h2>
-    </div>
-    <div class="body">
-        <table>
-            <tbody>
-            <tr>
-                <td class="label">Version :</td>
-                <td class="value">1.15.0.a18e6e1</td>
-            </tr>
-            <tr>
-                <td colspan="2">
-                    <hr>
-                </td>
-            </tr>
-            <tr>
-                <td class="label">Devices :</td>
-                <td class="value">2</td>
-            </tr>
-            <tr>
-                <td class="label">Links :</td>
-                <td class="value">0</td>
-            </tr>
-            <tr>
-                <td class="label">Hosts :</td>
-                <td class="value">0</td>
-            </tr>
-            <tr>
-                <td class="label">Topology SCCs :</td>
-                <td class="value">2</td>
-            </tr>
-            <tr>
-                <td colspan="2">
-                    <hr>
-                </td>
-            </tr>
-            <tr>
-                <td class="label">Intents :</td>
-                <td class="value">0</td>
-            </tr>
-            <tr>
-                <td class="label">Tunnels :</td>
-                <td class="value">0</td>
-            </tr>
-            <tr>
-                <td class="label">Flows :</td>
-                <td class="value">8</td>
-            </tr>
-            </tbody>
-        </table>
-    </div>
-    <div class="footer"></div>
+     style="opacity: 1; right: 20px; width: 260px;" [@summaryPanelState]="on">
+    <!-- everything else is filled in dynamically by listProps() and the
+    response showSummary received from the server -->
 </div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts b/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts
index d314528..7de5e89 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/summary/summary.component.ts
@@ -13,15 +13,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import {Component, OnDestroy, OnInit, ViewEncapsulation} from '@angular/core';
 import { animate, state, style, transition, trigger } from '@angular/animations';
+import * as d3 from 'd3';
+import { TopoPanelBaseImpl } from '../topopanel.base';
 import {
     LogService,
     LoadingService,
     FnService,
-    PanelBaseImpl
+    WebSocketService,
+    GlyphService
 } from 'gui2-fw-lib';
 
+export interface SummaryResponse {
+    title: string;
+}
 /*
  ONOS GUI -- Topology Summary Module.
  Defines modeling of ONOS Summary Panel.
@@ -31,9 +37,10 @@
     templateUrl: './summary.component.html',
     styleUrls: [
         './summary.component.css',
-        '../../topology.common.css',
+        '../../topology.common.css', '../../topology.theme.css',
         '../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css'
     ],
+    encapsulation: ViewEncapsulation.None,
     animations: [
         trigger('summaryPanelState', [
             state('true', style({
@@ -49,19 +56,62 @@
         ])
     ]
 })
-export class SummaryComponent extends PanelBaseImpl implements OnInit {
+export class SummaryComponent extends TopoPanelBaseImpl implements OnInit, OnDestroy {
+    private handlers: string[] = [];
+    private resp: string = 'showSummary';
+    private summaryData: SummaryResponse;
 
     constructor(
         protected fs: FnService,
         protected log: LogService,
         protected ls: LoadingService,
+        protected wss: WebSocketService,
+        protected gs: GlyphService
     ) {
-        super(fs, ls, log);
+        super(fs, ls, log, 'summary');
+        this.summaryData = <SummaryResponse>{};
         this.log.debug('SummaryComponent constructed');
     }
 
 
     ngOnInit() {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.resp, (data) => this.handleSummaryData(data)]
+        ]));
+        this.handlers.push(this.resp);
+
+        this.init(d3.select('#topo2-p-summary'));
+        this.on = true;
+
+        this.wss.sendEvent('requestSummary', {});
     }
 
+    ngOnDestroy() {
+        this.wss.sendEvent('cancelSummary', {});
+        this.wss.unbindHandlers(this.handlers);
+    }
+
+    handleSummaryData(data: SummaryResponse) {
+        this.summaryData = data;
+        this.render();
+        this.log.debug('Summary', data);
+    }
+
+    private render() {
+        let endedWithSeparator;
+
+        this.emptyRegions();
+
+        const svg = this.appendToHeader('div')
+                .classed('icon', true)
+                .append('svg');
+        const title = this.appendToHeader('h2');
+        const table = this.appendToBody('table');
+        const tbody = table.append('tbody');
+
+        title.text(this.summaryData.title);
+        this.gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
+        endedWithSeparator = this.listProps(tbody, this.summaryData);
+        // TODO : review whether we need to use/store end-with-sep state
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/topopanel.base.ts b/web/gui2/src/main/webapp/app/view/topology/panel/topopanel.base.ts
new file mode 100644
index 0000000..48aa2ed
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/topopanel.base.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 {
+    FnService,
+    LoadingService,
+    LogService,
+    PanelBaseImpl
+} from 'gui2-fw-lib';
+
+/**
+ * Base model of panel view - implemented by Topology Panel components
+ */
+export abstract class TopoPanelBaseImpl extends PanelBaseImpl {
+
+    protected header: any;
+    protected body: any;
+    protected footer: any;
+
+    protected constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        protected log: LogService,
+        protected id: string
+    ) {
+        super(fs, ls, log);
+    }
+
+    protected init(el: any) {
+        this.header = el.append('div').classed('header', true);
+        this.body = el.append('div').classed('body', true);
+        this.footer = el.append('div').classed('footer', true);
+    }
+
+    /**
+     * Decode lists of props sent back through Web Socket
+     *
+     * Means that panels do not have to know property names in advance
+     * Driven by PropertyPanel on Server side
+     */
+    listProps(el, data) {
+        let sepLast: boolean = false;
+
+        // note: track whether we end with a separator or not...
+        data.propOrder.forEach((p) => {
+            if (p === '-') {
+                this.addSep(el);
+                sepLast = true;
+            } else {
+                this.addProp(el, data.propLabels[p], data.propValues[p]);
+                sepLast = false;
+            }
+        });
+        return sepLast;
+    }
+
+    addProp(el, label, value) {
+        const tr = el.append('tr');
+        let lab;
+
+        if (typeof label === 'string') {
+            lab = label.replace(/_/g, ' ');
+        } else {
+            lab = label;
+        }
+
+        function addCell(cls, txt) {
+            tr.append('td').attr('class', cls).text(txt);
+        }
+
+        addCell('label', lab + ' :');
+        addCell('value', value);
+    }
+
+    addSep(el) {
+        el.append('tr').append('td').attr('colspan', 2).append('hr');
+    }
+
+    appendToHeader(x) {
+        return this.header.append(x);
+    }
+
+    appendToBody(x) {
+        return this.body.append(x);
+    }
+
+    appendToFooter(x) {
+        return this.footer.append(x);
+    }
+
+    emptyRegions() {
+        this.header.selectAll('*').remove();
+        this.body.selectAll('*').remove();
+        this.footer.selectAll('*').remove();
+    }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
index cc03053..a26f3ec 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
@@ -19,7 +19,6 @@
 import { TopologyComponent } from './topology/topology.component';
 import { NoDeviceConnectedSvgComponent } from './layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component';
 import { LayoutComponent } from './layer/layout/layout.component';
-import { ZoomLayerSvgComponent } from './layer/zoomlayersvg/zoomlayersvg.component';
 import { InstanceComponent } from './panel/instance/instance.component';
 import { SummaryComponent } from './panel/summary/summary.component';
 import { ToolbarComponent } from './panel/toolbar/toolbar.component';
@@ -45,7 +44,6 @@
         TopologyComponent,
         NoDeviceConnectedSvgComponent,
         LayoutComponent,
-        ZoomLayerSvgComponent,
         InstanceComponent,
         SummaryComponent,
         ToolbarComponent,
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.css b/web/gui2/src/main/webapp/app/view/topology/topology.theme.css
similarity index 61%
rename from web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.css
rename to web/gui2/src/main/webapp/app/view/topology/topology.theme.css
index fca0dc7..caa6199 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/zoomlayersvg/zoomlayersvg.component.css
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.theme.css
@@ -15,5 +15,32 @@
  */
 
 /**
- * ONOS GUI -- Topology Zoom Layer -- CSS file
- */
\ No newline at end of file
+ * ONOS GUI -- Topology Common styles -- CSS file
+ */
+
+.topo2-p h2 {
+    display: inline-block;
+    padding: 6px;
+}
+
+.topo2-p svg {
+    background: #c0242b;
+    width: 28px;
+    height: 28px;
+}
+
+.topo2-p svg .glyph {
+    fill: #ffffff;
+}
+
+.topo2-p hr {
+    background-color: #cccccc;
+}
+
+#topo2-p-detail svg {
+    background: none;
+}
+
+#topo2-p-detail .header svg .glyph {
+    fill: #c0242b;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
index 7347cb5..0db2c82 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
@@ -21,11 +21,12 @@
 <onos-details #details></onos-details>
 
 <div id="ov-topo2">
-    <svg viewBox="0 0 1000 1000" id="topo2"
-        resize offset-height="56" offset-width="12"
-        notifier="notifyResize()">
+    <svg viewBox="0 0 1000 1000" id="topo2">
         <svg:g onos-nodeviceconnected />
-        <svg:g onos-zoomlayer />
+        <svg:g id="topo-zoomlayer">
+            <svg:g onos-backgroundsvg/>
+            <svg:g onos-forcesvg/>
+        </svg:g>
     </svg>
 </div>
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
index 5ed744b..bf46637 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 import { Component, OnInit, ViewChild } from '@angular/core';
+import * as d3 from 'd3';
 import {
     FnService,
     KeysService, KeysToken,
     LogService, PrefsService,
-    SvgUtilService, WebSocketService
+    SvgUtilService, WebSocketService, Zoomer, ZoomOpts, ZoomService
 } from 'gui2-fw-lib';
 import {InstanceComponent} from '../panel/instance/instance.component';
 import {SummaryComponent} from '../panel/summary/summary.component';
@@ -60,16 +61,19 @@
 
     flashMsg: string = '';
     prefsState = {};
-    svg: any;
     hostLabelIdx: number = 1;
 
+    zoomer: Zoomer;
+    zoomEventListeners: any[];
+
     constructor(
         protected log: LogService,
         protected fs: FnService,
         protected ks: KeysService,
         protected sus: SvgUtilService,
         protected ps: PrefsService,
-        protected wss: WebSocketService
+        protected wss: WebSocketService,
+        protected zs: ZoomService
     ) {
 
         this.log.debug('Topology component constructed');
@@ -77,6 +81,15 @@
 
     ngOnInit() {
         this.bindCommands();
+        this.zoomer = this.createZoomer(<ZoomOpts>{
+            svg: d3.select('svg#topo2'),
+            zoomLayer: d3.select('g#topo-zoomlayer'),
+            zoomEnabled: () => true,
+            zoomMin: 0.25,
+            zoomMax: 10.0,
+            zoomCallback: (() => { return; })
+        });
+        this.zoomEventListeners = [];
         this.log.debug('Topology component initialized');
     }
 
@@ -88,6 +101,8 @@
             I: [(token) => {this.toggleInstancePanel(token); }, 'Toggle ONOS Instance Panel'],
             O: [() => {this.toggleSummary(); }, 'Toggle the Summary Panel'],
             R: [() => {this.resetZoom(); }, 'Reset pan / zoom'],
+            'shift-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() * 2); }, 'Zoom x2'],
+            'alt-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() / 2); }, 'Zoom x0.5'],
             P: [(token) => {this.togglePorts(token); }, 'Toggle Port Highlighting'],
             E: [() => {this.equalizeMasters(); }, 'Equalize mastership roles'],
             X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
@@ -98,8 +113,8 @@
             'shift-L': [() => {this.cycleHostLabels(); }, 'Cycle host labels'],
 
             // -- instance color palette debug
-            9: function () {
-                this.sus.cat7().testCard(this.svg);
+            9: () => {
+                this.sus.cat7().testCard(d3.select('svg#topo2'));
             },
 
             esc: this.handleEscape,
@@ -249,6 +264,7 @@
     }
 
     protected resetZoom() {
+        this.zoomer.reset();
         this.log.debug('resetting zoom');
         // TODO: Reinstate with components
         // t2bgs.resetZoom();
@@ -332,4 +348,65 @@
         return this.fs.isA(entry) || [entry, ''];
     }
 
+
+
+    protected createZoomer(options: ZoomOpts) {
+        // need to wrap the original zoom callback to extend its behavior
+        const origCallback = this.fs.isF(options.zoomCallback) ? options.zoomCallback : () => {};
+
+        options.zoomCallback = () => {
+            origCallback([0, 0], 1);
+
+            this.zoomEventListeners.forEach((ev) => ev(this.zoomer));
+        };
+
+        return this.zs.createZoomer(options);
+    }
+
+    getZoomer() {
+        return this.zoomer;
+    }
+
+    findZoomEventListener(ev) {
+        for (let i = 0, len = this.zoomEventListeners.length; i < len; i++) {
+            if (this.zoomEventListeners[i] === ev) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    addZoomEventListener(callback) {
+        this.zoomEventListeners.push(callback);
+    }
+
+    removeZoomEventListener(callback) {
+        const evIndex = this.findZoomEventListener(callback);
+
+        if (evIndex !== -1) {
+            this.zoomEventListeners.splice(evIndex);
+        }
+    }
+
+    adjustmentScale(min: number, max: number): number {
+        let _scale = 1;
+        const size = (min + max) / 2;
+
+        if (size * this.scale() < max) {
+            _scale = min / (size * this.scale());
+        } else if (size * this.scale() > max) {
+            _scale = min / (size * this.scale());
+        }
+
+        return _scale;
+    }
+
+    scale(): number {
+        return this.zoomer.scale();
+    }
+
+    panAndZoom(translate: number[], scale: number, transition?: number) {
+        this.zoomer.panZoom(translate, scale, transition);
+    }
+
 }
diff --git a/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-300.woff b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-300.woff
new file mode 100644
index 0000000..38328c4
--- /dev/null
+++ b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-300.woff
Binary files differ
diff --git a/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-300.woff2 b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-300.woff2
new file mode 100644
index 0000000..4af4545
--- /dev/null
+++ b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-300.woff2
Binary files differ
diff --git a/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-600.woff b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-600.woff
new file mode 100644
index 0000000..5a604b3
--- /dev/null
+++ b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-600.woff
Binary files differ
diff --git a/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-600.woff2 b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-600.woff2
new file mode 100644
index 0000000..a0965b7
--- /dev/null
+++ b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-600.woff2
Binary files differ
diff --git a/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-700.woff b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-700.woff
new file mode 100644
index 0000000..2523e95
--- /dev/null
+++ b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-700.woff
Binary files differ
diff --git a/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-700.woff2 b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-700.woff2
new file mode 100644
index 0000000..2b04b15
--- /dev/null
+++ b/web/gui2/src/main/webapp/fonts/open-sans-v15-latin-700.woff2
Binary files differ
diff --git a/web/gui2/src/main/webapp/index.html b/web/gui2/src/main/webapp/index.html
index 780634c..f50331b 100644
--- a/web/gui2/src/main/webapp/index.html
+++ b/web/gui2/src/main/webapp/index.html
@@ -24,10 +24,6 @@
     <meta name="apple-mobile-web-app-status-bar-style" content="black">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
-    <!-- Needs investigation - should not have any external dependencies -->
-    <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,700'
-          rel='stylesheet' type='text/css'>
-    <link href="onos.theme.css" type='text/css'>
     <link href="app/fw/layer/loading.service.css" rel='stylesheet' type='text/css'>
     <base href="/">
     <title>ONOS</title>
diff --git a/web/gui2/src/main/webapp/onos.theme.css b/web/gui2/src/main/webapp/onos.global.css
similarity index 69%
rename from web/gui2/src/main/webapp/onos.theme.css
rename to web/gui2/src/main/webapp/onos.global.css
index 8a09e16..36f107e 100644
--- a/web/gui2/src/main/webapp/onos.theme.css
+++ b/web/gui2/src/main/webapp/onos.global.css
@@ -18,12 +18,48 @@
  ONOS GUI -- core (theme) -- CSS file
  */
 
+/**
+ * Offline cache - see
+ * https://google-webfonts-helper.herokuapp.com/fonts/open-sans?subsets=latin
+ */
+/* open-sans-300 - latin */
+@font-face {
+    font-family: 'Open Sans';
+    font-style: normal;
+    font-weight: 300;
+    src: local('Open Sans Light'), local('OpenSans-Light'),
+    url('./fonts/open-sans-v15-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/open-sans-v15-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* open-sans-600 - latin */
+@font-face {
+    font-family: 'Open Sans';
+    font-style: normal;
+    font-weight: 600;
+    src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
+    url('./fonts/open-sans-v15-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/open-sans-v15-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* open-sans-700 - latin */
+@font-face {
+    font-family: 'Open Sans';
+    font-style: normal;
+    font-weight: 700;
+    src: local('Open Sans Bold'), local('OpenSans-Bold'),
+    url('./fonts/open-sans-v15-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/open-sans-v15-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+
+
 body {
     background-color: white;
 }
 
 html {
-    font-family: 'Open Sans', sans-serif;
+    font-family: 'Open Sans', normal;
     -webkit-text-size-adjust: 100%;
     -ms-text-size-adjust: 100%;
     height: 100%;
@@ -41,7 +77,7 @@
 
 #view h2 {
     color: #3c3a3a;
-    margin: 32px 0 4px 16px;
+    /*margin: 32px 0 4px 16px;*/
     padding: 0;
     font-size: 18pt;
     font-weight: lighter;