Added native Bazel build to GUI2. Reduced a lot of the unused Angular CLI structures

Reviewers should look at the changes in WORKSPACE, BUILD, BUILD.bazel, README.md files
This is only possible now as rules_nodejs went to 1.0.0 on December 20
gui2 has now been made the entry point (rather than gui2-fw-lib)
No tests or linting are functional yet for Typescript
Each NgModule now has its own BUILD.bazel file with ng_module
gui2-fw-lib is all one module and has been refactored to simplify the directory structure
gui2-topo-lib is also all one module - its directory structure has had 3 layers removed
The big bash script in web/gui2/BUILD has been removed - all is done through ng_module rules
in web/gui2/src/main/webapp/BUILD.bazel and web/gui2/src/main/webapp/app/BUILD.bazel

Change-Id: Ifcfcc23a87be39fe6d6c8324046cc8ebadb90551
diff --git a/web/gui2-fw-lib/lib/consolelogger.service.spec.ts b/web/gui2-fw-lib/lib/consolelogger.service.spec.ts
new file mode 100644
index 0000000..bbb8974
--- /dev/null
+++ b/web/gui2-fw-lib/lib/consolelogger.service.spec.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 { TestBed, inject } from '@angular/core/testing';
+
+import { ConsoleLoggerService } from './consolelogger.service';
+
+/**
+ * ONOS GUI -- Console Logger Service - Unit Tests
+ */
+describe('ConsoleloggerService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [ConsoleLoggerService]
+    });
+  });
+
+  it('should be created', inject([ConsoleLoggerService], (service: ConsoleLoggerService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/web/gui2-fw-lib/lib/consolelogger.service.ts b/web/gui2-fw-lib/lib/consolelogger.service.ts
new file mode 100644
index 0000000..627359c
--- /dev/null
+++ b/web/gui2-fw-lib/lib/consolelogger.service.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { environment } from '../environments/environment';
+import { Logger } from './log.service';
+
+export let isDebugMode: boolean = !environment.production;
+
+const noop = (): any => undefined;
+
+/**
+ * ONOS GUI -- LogService
+ * Inspired by https://robferguson.org/blog/2017/09/09/a-simple-logging-service-for-angular-4/
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class ConsoleLoggerService implements Logger {
+
+  get debug() {
+    if (isDebugMode) {
+      // tslint:disable-next-line:no-console
+      return console.debug.bind(console);
+    } else {
+      return noop;
+    }
+  }
+
+  get info() {
+    if (isDebugMode) {
+      // tslint:disable-next-line:no-console
+      return console.info.bind(console);
+    } else {
+      return noop;
+    }
+  }
+
+  get warn() {
+    return console.warn.bind(console);
+  }
+
+  get error() {
+    return console.error.bind(console);
+  }
+
+  invokeConsoleMethod(type: string, args?: any): void {
+    const logFn: Function = (console)[type] || console.log || noop;
+    logFn.apply(console, [args]);
+  }
+}
diff --git a/web/gui2-fw-lib/lib/detectbrowser.directive.spec.ts b/web/gui2-fw-lib/lib/detectbrowser.directive.spec.ts
new file mode 100644
index 0000000..3faa7f1
--- /dev/null
+++ b/web/gui2-fw-lib/lib/detectbrowser.directive.spec.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from './log.service';
+import { ConsoleLoggerService } from './consolelogger.service';
+import { DetectBrowserDirective } from './detectbrowser.directive';
+import { ActivatedRoute, Params } from '@angular/router';
+import { FnService } from './util/fn.service';
+import { OnosService } from './onos.service';
+import { of } from 'rxjs';
+
+class MockFnService extends FnService {
+    constructor(ar: ActivatedRoute, log: LogService, w: Window) {
+        super(ar, log, w);
+    }
+}
+
+class MockOnosService {}
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Detect Browser Directive - Unit Tests
+ */
+describe('DetectBrowserDirective', () => {
+    let log: LogService;
+    let ar: ActivatedRoute;
+    let mockWindow: Window;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute(['debug', 'DetectBrowserDirective']);
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            }
+        };
+
+        TestBed.configureTestingModule({
+            providers: [ DetectBrowserDirective,
+                { provide: FnService, useValue: new MockFnService(ar, log, mockWindow) },
+                { provide: LogService, useValue: log },
+                { provide: OnosService, useClass: MockOnosService },
+                { provide: Document, useValue: document },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+    });
+
+    afterEach(() => {
+        log = null;
+    });
+
+    it('should create an instance', inject([DetectBrowserDirective], (directive: DetectBrowserDirective) => {
+        expect(directive).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/detectbrowser.directive.ts b/web/gui2-fw-lib/lib/detectbrowser.directive.ts
new file mode 100644
index 0000000..a2ec16d
--- /dev/null
+++ b/web/gui2-fw-lib/lib/detectbrowser.directive.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { Inject } from '@angular/core';
+import { Directive } from '@angular/core';
+import { FnService } from './util/fn.service';
+import { LogService } from './log.service';
+import { OnosService } from './onos.service';
+
+/**
+ * ONOS GUI -- Detect Browser Directive
+ */
+@Directive({
+  selector: '[onosDetectBrowser]'
+})
+export class DetectBrowserDirective {
+  constructor(
+    private fs: FnService,
+    private log: LogService,
+    private onos: OnosService,
+
+    // TODO: Change the any type to Window when https://github.com/angular/angular/issues/15640 is fixed.
+    @Inject('Window') private w: any
+  ) {
+        const body: HTMLBodyElement = document.getElementsByTagName('body')[0];
+//        let body = d3.select('body');
+        let browser = '';
+        if (fs.isChrome()) {
+            browser = 'chrome';
+        } else if (fs.isChromeHeadless()) {
+            browser = 'chromeheadless';
+        } else if (fs.isSafari()) {
+            browser = 'safari';
+        } else if (fs.isFirefox()) {
+            browser = 'firefox';
+        } else {
+            this.log.warn('Unknown browser. ',
+            'Vendor:', this.w.navigator.vendor,
+            'Agent:', this.w.navigator.userAgent);
+            return;
+        }
+        body.classList.add(browser);
+//        body.classed(browser, true);
+        this.onos.browser = browser;
+
+        if (fs.isMobile()) {
+            body.classList.add('mobile');
+            this.onos.mobile = true;
+        }
+
+//        this.log.debug('Detected browser is', fs.cap(browser));
+    }
+}
diff --git a/web/gui2-fw-lib/lib/gui2-fw-lib.module.ts b/web/gui2-fw-lib/lib/gui2-fw-lib.module.ts
new file mode 100644
index 0000000..4a59988
--- /dev/null
+++ b/web/gui2-fw-lib/lib/gui2-fw-lib.module.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 {DetectBrowserDirective} from './detectbrowser.directive';
+import {IconComponent} from './svg/icon/icon.component';
+import {VeilComponent} from './layer/veil/veil.component';
+import {FlashComponent} from './layer/flash/flash.component';
+import {ConfirmComponent} from './layer/confirm/confirm.component';
+import {MastComponent} from './mast/mast/mast.component';
+import {TableFilterPipe} from './widget/tablefilter.pipe';
+import {TableResizeDirective} from './widget/tableresize.directive';
+import {QuickhelpComponent} from './layer/quickhelp/quickhelp.component';
+import {LoadingComponent} from './layer/loading/loading.component';
+import {ZoomableDirective} from './svg/zoomable.directive';
+import {NameInputComponent} from './util/name-input/name-input.component';
+
+@NgModule({
+    imports: [
+        CommonModule
+    ],
+    declarations: [
+        DetectBrowserDirective,
+        TableResizeDirective,
+        IconComponent,
+        VeilComponent,
+        FlashComponent,
+        ConfirmComponent,
+        QuickhelpComponent,
+        MastComponent,
+        TableFilterPipe,
+        LoadingComponent,
+        ZoomableDirective,
+        NameInputComponent
+    ],
+    exports: [
+        DetectBrowserDirective,
+        TableResizeDirective,
+        IconComponent,
+        VeilComponent,
+        FlashComponent,
+        ConfirmComponent,
+        QuickhelpComponent,
+        MastComponent,
+        TableFilterPipe,
+        LoadingComponent,
+        ZoomableDirective,
+        NameInputComponent
+    ]
+})
+export class Gui2FwLibModule {
+}
diff --git a/web/gui2-fw-lib/lib/layer/confirm/confirm.component.css b/web/gui2-fw-lib/lib/layer/confirm/confirm.component.css
new file mode 100644
index 0000000..b097534
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/confirm/confirm.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.
+ */
+
+/*
+ ONOS GUI -- Confirm Component (layout) -- CSS file
+ */
+#app-dialog {
+    top: 140px;
+    padding: 12px;
+}
+
+#app-dialog h3 {
+    display: inline-block;
+    font-weight: bold;
+    font-size: 18pt;
+}
+
+#app-dialog p {
+    font-size: 12pt;
+}
+
+#app-dialog p.strong {
+    font-weight: bold;
+    padding: 8px;
+    text-align: center;
+}
diff --git a/web/gui2-fw-lib/lib/layer/confirm/confirm.component.html b/web/gui2-fw-lib/lib/layer/confirm/confirm.component.html
new file mode 100644
index 0000000..098d83a
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/confirm/confirm.component.html
@@ -0,0 +1,22 @@
+<!--
+~ 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="app-dialog" class="floatpanel dialog" [@confirmDlgState]="message!==''">
+    <h3> {{ title }} </h3>
+    <p>{{ message }}</p>
+    <p *ngIf="warning" class="warning strong">{{ warning }}</p>
+    <div tabindex="10" class="dialog-button" (click)="choice(true)">OK</div>
+    <div tabindex="11" class="dialog-button" (click)="choice(false)">Cancel</div>
+</div>
diff --git a/web/gui2-fw-lib/lib/layer/confirm/confirm.component.spec.ts b/web/gui2-fw-lib/lib/layer/confirm/confirm.component.spec.ts
new file mode 100644
index 0000000..9692e3a
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/confirm/confirm.component.spec.ts
@@ -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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { LionService } from '../../util/lion.service';
+
+import { ConsoleLoggerService } from '../../consolelogger.service';
+import { LogService } from '../../log.service';
+import { ConfirmComponent } from './confirm.component';
+
+/**
+ * ONOS GUI -- Layer -- Confirm Component - Unit Tests
+ */
+describe('ConfirmComponent', () => {
+    let log: LogService;
+    let component: ConfirmComponent;
+    let fixture: ComponentFixture<ConfirmComponent>;
+    const bundleObj = {
+        'core.view.App': {
+            test: 'test1',
+            dlg_confirm_action: 'Confirm'
+        }
+    };
+    const mockLion = (key) => {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+    beforeEach(async(() => {
+        log = new ConsoleLoggerService();
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ ConfirmComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                {
+                    provide: LionService, useFactory: (() => {
+                        return {
+                            bundle: ((bundleId) => mockLion),
+                            ubercache: new Array(),
+                            loadCbs: new Map<string, () => void>([])
+                        };
+                    })
+                },
+            ]
+        });
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(ConfirmComponent);
+        component = fixture.debugElement.componentInstance;
+        component.title = 'Confirm';
+        component.message = 'A message';
+        component.warning = 'A warning';
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a h3 inside a div#app-dialog', () => {
+        const appDe: DebugElement = fixture.debugElement;
+        const divDe = appDe.query(By.css('div#app-dialog h3'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual(' Confirm ');
+    });
+
+    it('should have a div.dialog-button inside a div#app-dialog', () => {
+        const appDe: DebugElement = fixture.debugElement;
+        const divDe = appDe.query(By.css('div#app-dialog div.dialog-button'));
+        const div: HTMLElement = divDe.nativeElement;
+        // It selects the first one
+        expect(div.textContent).toEqual('OK');
+    });
+});
diff --git a/web/gui2-fw-lib/lib/layer/confirm/confirm.component.ts b/web/gui2-fw-lib/lib/layer/confirm/confirm.component.ts
new file mode 100644
index 0000000..7533a7e
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/confirm/confirm.component.ts
@@ -0,0 +1,88 @@
+/*
+ * 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, Output, EventEmitter } from '@angular/core';
+import { trigger, state, style, animate, transition } from '@angular/animations';
+import { LogService } from '../../log.service';
+import { LionService } from '../../util/lion.service';
+
+/**
+ * ONOS GUI -- Layer -- Confirm Component
+ *
+ * Replaces Flash Service in old GUI.
+ * Provides a mechanism to present a confirm dialog to the screen
+ *
+ * To use add an element to the template like
+ *   <onos-confirm message="Performing something dangerous. Would you like to proceed"></onos-flash>
+ *
+ * An event is raised with either OK or Cancel
+ */
+@Component({
+    selector: 'onos-confirm',
+    templateUrl: './confirm.component.html',
+    styleUrls: [
+        './confirm.component.css',
+        './confirm.theme.css',
+        '../dialog.css',
+        '../dialog.theme.css',
+        '../../widget/panel.css',
+        '../../widget/panel-theme.css'
+    ],
+    animations: [
+        trigger('confirmDlgState', [
+            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 ConfirmComponent {
+
+    lionFn; // Function
+
+    @Input() message: string;
+    @Input() warning: string;
+    @Input() title: string;
+    @Output() chosen: EventEmitter<boolean> = new EventEmitter();
+
+    constructor(
+        private log: LogService,
+        private lion: LionService,
+    ) {
+        this.log.debug('ConfirmComponent constructed');
+        this.doLion();
+    }
+
+    /**
+     * When OK or Cancel is pressed, send an event to parent with choice
+     */
+    choice(chosen: boolean): void {
+        this.chosen.emit(chosen);
+    }
+
+    /**
+     * Read the LION bundle for App to get confirm dialouge header
+     */
+    doLion() {
+        this.lionFn = this.lion.bundle('core.view.App');
+    }
+}
diff --git a/web/gui2-fw-lib/lib/layer/confirm/confirm.theme.css b/web/gui2-fw-lib/lib/layer/confirm/confirm.theme.css
new file mode 100644
index 0000000..db97648
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/confirm/confirm.theme.css
@@ -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.
+ */
+
+/*
+ ONOS GUI -- Confirm Component (theme) -- CSS file
+ */
+/* temporarily removed .light */
+#app-dialog p.strong {
+    color: white;
+    background-color: #ce5b58;
+}
+
+#app-dialog.floatpanel.dialog {
+    background-color: #ffffff;
+}
+
+#app-dialog p.strong {
+    color: white;
+    background-color: #ce5b58;
+}
diff --git a/web/gui2-fw-lib/lib/layer/dialog.css b/web/gui2-fw-lib/lib/layer/dialog.css
new file mode 100644
index 0000000..6792a3d
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/dialog.css
@@ -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.
+ */
+
+/*
+ ONOS GUI -- Dialog Service (layout) -- CSS file
+ */
+
+.dialog h2 {
+    margin: 0;
+    word-wrap: break-word;
+    display: inline-block;
+    width: 210px;
+    vertical-align: middle;
+}
+
+.dialog .dialog-button {
+    display: inline-block;
+    cursor: pointer;
+    height: 20px;
+    padding: 6px 8px 2px 8px;
+    margin: 4px;
+    float: right;
+}
diff --git a/web/gui2-fw-lib/lib/layer/dialog.theme.css b/web/gui2-fw-lib/lib/layer/dialog.theme.css
new file mode 100644
index 0000000..24cd5fa
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/dialog.theme.css
@@ -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.
+ */
+
+/*
+ ONOS GUI -- Dialog Service (theme) -- CSS file
+ */
+
+/* temporarily removed .light */
+.dialog .dialog-button {
+    background-color: #518ecc;
+    color: white;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark .dialog .dialog-button {
+    background-color: #345e85;
+    color: #cccccd;
+}
diff --git a/web/gui2-fw-lib/lib/layer/flash/flash.component.css b/web/gui2-fw-lib/lib/layer/flash/flash.component.css
new file mode 100644
index 0000000..66b8f3b
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/flash/flash.component.css
@@ -0,0 +1,68 @@
+/*
+ * 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 -- Flash Component (layout) -- CSS file
+ */
+
+#flash {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 1400;
+}
+
+#flash.warning div#flashBox {
+    border: 2px solid #222222;
+    border-radius: 10px;
+    background: #FFFFFF;
+    padding: 10px;
+}
+
+#flash div#flashBox {
+    background: #CCCCCC;
+    border-radius: 10px;
+    padding: 1px;
+}
+
+#flash div#flashBox div.dialog-button {
+    transform :translateY(-32px);
+}
+
+#flash.warning p#flashText {
+    stroke: #FF0000;
+    color: #FF0000;
+    text-anchor: middle;
+    alignment-baseline: middle;
+    text-align: center;
+    font-size: 16pt;
+    border-radius: 10px;
+    background: #FFFFFF;
+    padding: 10px;
+}
+
+#flash p#flashText {
+    stroke: none;
+    color: #222222;
+    text-anchor: middle;
+    alignment-baseline: middle;
+    text-align: center;
+    font-size: 16pt;
+    border-radius: 10px;
+    background: #CCCCCC;
+    padding: 5px;
+}
diff --git a/web/gui2-fw-lib/lib/layer/flash/flash.component.html b/web/gui2-fw-lib/lib/layer/flash/flash.component.html
new file mode 100644
index 0000000..46b6de9
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/flash/flash.component.html
@@ -0,0 +1,21 @@
+<!--
+~ 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="flash" class="dialog" [ngClass]="warning?'warning':''" [@flashState]="visible">
+    <div id="flashBox" *ngIf="visible">
+        <p id="flashText">{{ message }}</p>
+        <div class="dialog-button" *ngIf="dwell>1200" (click)="closeNow()">Dismiss</div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2-fw-lib/lib/layer/flash/flash.component.spec.ts b/web/gui2-fw-lib/lib/layer/flash/flash.component.spec.ts
new file mode 100644
index 0000000..661e223
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/flash/flash.component.spec.ts
@@ -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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+import { ConsoleLoggerService } from '../../consolelogger.service';
+import { LogService } from '../../log.service';
+import { FlashComponent } from './flash.component';
+
+/**
+ * ONOS GUI -- Layer -- Flash Component - Unit Tests
+ */
+describe('FlashComponent', () => {
+    let log: LogService;
+    let component: FlashComponent;
+    let fixture: ComponentFixture<FlashComponent>;
+
+    beforeEach(async(() => {
+        log = new ConsoleLoggerService();
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ FlashComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+            ]
+        });
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(FlashComponent);
+        component = fixture.debugElement.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+//    it('should have a div#flash', () => {
+//        component.enabled = true;
+//        const appDe: DebugElement = fixture.debugElement;
+//        const divDe = appDe.query(By.css('div#flash'));
+//        expect(divDe).toBeTruthy();
+//    });
+});
diff --git a/web/gui2-fw-lib/lib/layer/flash/flash.component.ts b/web/gui2-fw-lib/lib/layer/flash/flash.component.ts
new file mode 100644
index 0000000..7dd721d
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/flash/flash.component.ts
@@ -0,0 +1,91 @@
+/*
+ * 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, Output, OnChanges, SimpleChange, EventEmitter } from '@angular/core';
+import { LogService } from '../../log.service';
+import { trigger, state, style, animate, transition } from '@angular/animations';
+
+/**
+ * ONOS GUI -- Layer -- Flash Component
+ *
+ * Replaces Flash Service in old GUI.
+ * Provides a mechanism to flash short informational messages to the screen
+ * to alert the user of something, e.g. "Hosts visible" or "Hosts hidden".
+ *
+ * It can be used in a warning mode, where text will appear in red
+ * The dwell time (milliseconds) can be controlled or the default is 1200ms
+ *
+ * To use add an element to the template like
+ *   <onos-flash message="Hosts visible" dwell="2000" warning="true"></onos-flash>
+ * This whole element can be disabled until needed with an ngIf, but if this is done
+ * the animated fade-in and fade-out will not happen
+ * There is also a (closed) event that tells you when the message is closed, or
+ * fades-out
+ */
+@Component({
+    selector: 'onos-flash',
+    templateUrl: './flash.component.html',
+    styleUrls: [
+        './flash.component.css',
+        '../dialog.css',
+        '../dialog.theme.css',
+    ],
+    animations: [
+        trigger('flashState', [
+            state('false', style({
+//                transform: 'translateY(-400%)',
+                opacity: '0.0',
+            })),
+            state('true', style({
+//                transform: 'translateY(0%)',
+                opacity: '1.0',
+            })),
+            transition('0 => 1', animate('200ms ease-in')),
+            transition('1 => 0', animate('200ms ease-out'))
+        ])
+    ]
+})
+export class FlashComponent implements OnChanges {
+    @Input() message: string;
+    @Input() dwell: number = 1200; // milliseconds
+    @Input() warning: boolean = false;
+    @Output() closed: EventEmitter<boolean> = new EventEmitter();
+
+    public visible: boolean = false;
+
+    /**
+     * Flash a message up for 1200ms then disappear again.
+     * See animation parameter for the ease in and ease out params
+     */
+    ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
+        if (changes['message'] && this.message && this.message !== '') {
+            this.visible = true;
+
+            setTimeout(() => {
+                this.visible = false;
+                this.closed.emit(false);
+            }, this.dwell);
+        }
+    }
+
+    /**
+     * The message will flash up for 'dwell' milliseconds
+     * If dwell is > 2000ms, then there will be a button that allows it to be dismissed now
+     */
+    closeNow() {
+        this.visible = false;
+        this.closed.emit(false);
+    }
+}
diff --git a/web/gui2-fw-lib/lib/layer/loading/loading.component.css b/web/gui2-fw-lib/lib/layer/loading/loading.component.css
new file mode 100644
index 0000000..9095bd2
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/loading/loading.component.css
@@ -0,0 +1,28 @@
+/*
+ *  Copyright 2019-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 -- Loading Service -- CSS file
+ */
+
+#loading-anim {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    -webkit-transform: translate(-50%, -50%);
+    transform: translate(-50%, -50%);
+    z-index: -5000;
+}
diff --git a/web/gui2-fw-lib/lib/layer/loading/loading.component.html b/web/gui2-fw-lib/lib/layer/loading/loading.component.html
new file mode 100644
index 0000000..8220197
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/loading/loading.component.html
@@ -0,0 +1,16 @@
+<!--
+~ Copyright 2019-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.
+-->
+<img id="loading-anim" [src]="img" alt="loading - please wait" [@loadingState]="running"/>
diff --git a/web/gui2-fw-lib/lib/layer/loading/loading.component.spec.ts b/web/gui2-fw-lib/lib/layer/loading/loading.component.spec.ts
new file mode 100644
index 0000000..185c20e
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/loading/loading.component.spec.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2019-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 { LoadingComponent } from './loading.component';
+import {LogService} from '../../log.service';
+import {ConsoleLoggerService} from '../../consolelogger.service';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+describe('LoadingComponent', () => {
+    let log: LogService;
+    let component: LoadingComponent;
+    let fixture: ComponentFixture<LoadingComponent>;
+
+    beforeEach(async(() => {
+        log = new ConsoleLoggerService();
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ LoadingComponent ],
+            providers: [
+                { provide: LogService, useValue: log }
+            ]
+        })
+        .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(LoadingComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-fw-lib/lib/layer/loading/loading.component.ts b/web/gui2-fw-lib/lib/layer/loading/loading.component.ts
new file mode 100644
index 0000000..59ce04b
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/loading/loading.component.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2019-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, SimpleChanges} from '@angular/core';
+import {LogService} from '../../log.service';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+
+const LOADING_IMG_DIR = 'data/img/loading/';
+const LOADING_PFX = '/load-';
+const NUM_IMGS = 16;
+
+/**
+ * ONOS GUI - A component that shows the loading icon
+ *
+ * Should be shown if someone has to wait for more than
+ * a certain time for data to be retrieved
+ * Note the animation - there is a pause of 500ms before the images appear
+ * and then it eases in over 200ms
+ */
+@Component({
+    selector: 'onos-loading',
+    templateUrl: './loading.component.html',
+    styleUrls: ['./loading.component.css'],
+    animations: [
+        trigger('loadingState', [
+            state('false', style({
+                opacity: '0.0',
+                'z-index': -5000
+            })),
+            state('true', style({
+                opacity: '1.0',
+                'z-index': 5000
+            })),
+            transition('0 => 1', animate('200ms 500ms ease-in')),
+            transition('1 => 0', animate('200ms ease-out'))
+        ])
+    ]
+})
+export class LoadingComponent implements OnChanges {
+    @Input() theme: string = 'light';
+    @Input() running: boolean;
+
+    speed: number = 8; // Frames per second
+    idx = 1;
+    images: HTMLImageElement[] = [];
+    img: string;
+    task: any;
+
+    constructor(
+        private log: LogService,
+    ) {
+        let idx: number;
+
+        for (idx = 1; idx <= NUM_IMGS ; idx++) {
+            this.addImg('light', idx);
+            this.addImg('dark', idx);
+        }
+
+        this.log.debug('LoadingComponent constructed - images preloaded from', this.fname(1, this.theme));
+    }
+
+    /**
+     * Detects changes in in Input variable
+     * Here we want to detect if running has been enabled or disabled
+     * @param changes
+     */
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes['running']) {
+            const newRunning: boolean = changes['running'].currentValue;
+
+            if (newRunning) {
+                this.task = setInterval(() => this.nextFrame(), 1000 / this.speed);
+            } else {
+                if (this.task) {
+                    clearInterval(this.task);
+                    this.task = null;
+                }
+            }
+        }
+    }
+
+    private addImg(theme: string, idx: number): void {
+        const img = new Image();
+        img.src = this.fname(idx, theme);
+        this.images.push(img);
+    }
+
+    private fname(i: number, theme: string): string {
+        const z = i > 9 ? '' : '0';
+        return LOADING_IMG_DIR + theme + LOADING_PFX + z + i + '.png';
+    }
+
+    private nextFrame(): void {
+        this.idx = this.idx === 16 ? 1 : this.idx + 1;
+        this.img  = this.fname(this.idx, this.theme);
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/layer/panel.service.ts b/web/gui2-fw-lib/lib/layer/panel.service.ts
new file mode 100644
index 0000000..863d18f
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/panel.service.ts
@@ -0,0 +1,222 @@
+/*
+ *  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 {Injectable} from '@angular/core';
+import {FnService} from '../util/fn.service';
+import {LogService} from '../log.service';
+import {ThemeService} from '../util/theme.service';
+import {WebSocketService} from '../remote/websocket.service';
+import * as d3 from 'd3';
+
+let fs;
+
+const defaultSettings = {
+    edge: 'right',
+    width: 200,
+    margin: 20,
+    hideMargin: 20,
+    xtnTime: 750,
+    fade: true,
+};
+
+let panels,
+    panelLayer;
+
+function init() {
+    panelLayer = d3.select('div#floatpanels');
+    panelLayer.text('');
+    panels = {};
+}
+
+// helpers for panel
+function noop() {
+}
+
+function margin(p: any) {
+    return p.settings.margin;
+}
+
+function hideMargin(p: any) {
+    return p.settings.hideMargin;
+}
+
+function noPx(p: any, what: any) {
+    return Number(p.el.style(what).replace(/px$/, ''));
+}
+
+function widthVal(p: any) {
+    return noPx(p, 'width');
+}
+
+function heightVal(p: any) {
+    return noPx(p, 'height');
+}
+
+function pxShow(p: any) {
+    return margin(p) + 'px';
+}
+
+function pxHide(p: any) {
+    return (-hideMargin(p) - widthVal(p) - (noPx(p, 'padding') * 2)) + 'px';
+}
+
+function makePanel(id: any, settings: any) {
+    const p = {
+            id: id,
+            settings: settings,
+            on: false,
+            el: null,
+        },
+        api = {
+            show: showPanel,
+            hide: hidePanel,
+            toggle: togglePanel,
+            empty: emptyPanel,
+            append: appendPanel,
+            width: panelWidth,
+            height: panelHeight,
+            bbox: panelBBox,
+            isVisible: panelIsVisible,
+            classed: classed,
+            el: panelEl,
+        };
+
+    p.el = panelLayer.append('div')
+        .attr('id', id)
+        .attr('class', 'floatpanel')
+        .style('opacity', 0);
+
+    // has to be called after el is set
+    p.el.style(p.settings.edge, pxHide(p));
+    panelWidth(p.settings.width);
+    if (p.settings.height) {
+        panelHeight(p.settings.height);
+    }
+
+    panels[id] = p;
+
+    function showPanel(cb: any) {
+        const endCb = fs.isF(cb) || noop;
+        p.on = true;
+        p.el.transition().duration(p.settings.xtnTime)
+            .style(p.settings.edge, pxShow(p))
+            .style('opacity', 1);
+    }
+
+    function hidePanel(cb: any) {
+        const endCb = fs.isF(cb) || noop,
+            endOpacity = p.settings.fade ? 0 : 1;
+        p.on = false;
+        p.el.transition().duration(p.settings.xtnTime)
+            .style(p.settings.edge, pxHide(p))
+            .style('opacity', endOpacity);
+    }
+
+    function togglePanel(cb: any) {
+        if (p.on) {
+            hidePanel(cb);
+        } else {
+            showPanel(cb);
+        }
+        return p.on;
+    }
+
+    function emptyPanel() {
+        return p.el.text('');
+    }
+
+    function appendPanel(what: any) {
+        return p.el.append(what);
+    }
+
+    function panelWidth(w: any) {
+        if (w === undefined) {
+            return widthVal(p);
+        }
+        p.el.style('width', w + 'px');
+    }
+
+    function panelHeight(h: any) {
+        if (h === undefined) {
+            return heightVal(p);
+        }
+        p.el.style('height', h + 'px');
+    }
+
+    function panelBBox() {
+        return p.el.node().getBoundingClientRect();
+    }
+
+    function panelIsVisible() {
+        return p.on;
+    }
+
+    function classed(cls: any, bool: any) {
+        return p.el.classed(cls, bool);
+    }
+
+    function panelEl() {
+        return p.el;
+    }
+
+    return api;
+}
+
+function removePanel(id: any) {
+    panelLayer.select('#' + id).remove();
+    delete panels[id];
+}
+
+@Injectable({
+    providedIn: 'root',
+})
+export class PanelService {
+    constructor(private funcs: FnService,
+                private log: LogService,
+                private ts: ThemeService,
+                private wss: WebSocketService) {
+        fs = this.funcs;
+        init();
+    }
+
+    createPanel(id: any, opts: any) {
+        const settings = (<any>Object).assign({}, defaultSettings, opts);
+        if (!id) {
+            this.log.warn('createPanel: no ID given');
+            return null;
+        }
+        if (panels[id]) {
+            this.log.warn('Panel with ID "' + id + '" already exists');
+            return null;
+        }
+        if (fs.debugOn('widget')) {
+            this.log.debug('creating panel:', id, settings);
+        }
+        return makePanel(id, settings);
+    }
+
+    destroyPanel(id: any) {
+        if (panels[id]) {
+            if (fs.debugOn('widget')) {
+                this.log.debug('destroying panel:', id);
+            }
+            removePanel(id);
+        } else {
+            if (fs.debugOn('widget')) {
+                this.log.debug('no panel to destroy:', id);
+            }
+        }
+    }
+}
diff --git a/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.css b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.css
new file mode 100644
index 0000000..fb0995a
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.css
@@ -0,0 +1,56 @@
+/*
+ * 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 -- Quickhelp Panel -- CSS file
+ */
+
+/*
+ ONOS GUI -- Quick Help Service (layout) -- CSS file
+ */
+
+#quickhelp {
+    top: 100px;
+    z-index: 5000;
+    position: absolute;
+    width: 100%;
+}
+
+#quickhelp div.help {
+    background: black;
+    opacity: 0.8;
+}
+
+#quickhelp div.help table {
+    vertical-align: top;
+    width: 100%;
+}
+
+#quickhelp div.help p {
+    text-align: center;
+    color: white;
+    font-weight: bold;
+}
+
+#quickhelp td.key {
+    color: #add;
+    padding-left: 8px;
+    padding-right: 4px;
+}
+
+#quickhelp td.desc {
+    color: white;
+}
diff --git a/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.html b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.html
new file mode 100644
index 0000000..7d96ebf
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.html
@@ -0,0 +1,72 @@
+<!--
+~ Copyright 2019-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="quickhelp" [@quickHelpState]="ks.quickHelpShown">
+    <div class="help" *ngIf="ks.quickHelpShown">
+        <p class="title">{{lionFn("qh_title")}}</p>
+        <!-- TODO: drive this through the keys service keyHandler -->
+        <table class="qhrow">
+            <tr>
+                <td class="key">&#92;</td>
+                <td class="desc">{{lionFn("qh_hint_show_hide_qh")}}</td>
+            </tr>
+            <tr>
+                <td class="key">/</td>
+                <td class="desc">{{lionFn("qh_hint_show_hide_qh")}}</td>
+            </tr>
+            <tr>
+                <td class="key">Esc</td>
+                <td class="desc">{{lionFn("qh_hint_esc")}}</td>
+            </tr>
+            <tr>
+                <td class="key">T</td>
+                <td class="desc">{{lionFn("qh_hint_t")}}</td>
+            </tr>
+        </table>
+        <hr class="qhrowsep">
+        <table class="qhrow">
+            <tr *ngFor="let vk of viewKeys; i as index">
+                <ng-container *ngFor="let vkrow of vk">
+                    <td class="key">{{vkrow.keystroke}}</td>
+                    <td class="desc">{{vkrow.text}}</td>
+                </ng-container>
+            </tr>
+        </table>
+        <hr class="qhrowsep">
+        <div class="qhrow">
+            <table class="qh-r4-c0">
+                <tr>
+                    <td class="key">{{lionFnTopo("click")}}</td>
+                    <td class="desc">{{lionFnTopo("qh_gest_click")}}</td>
+                </tr>
+                <tr>
+                    <td class="key">{{lionFnTopo("shift_click")}}</td>
+                    <td class="desc">{{lionFnTopo("qh_gest_shift_click")}}</td>
+                </tr>
+                <tr>
+                    <td class="key">{{lionFnTopo("drag")}}</td>
+                    <td class="desc">{{lionFnTopo("qh_gest_drag")}}</td>
+                </tr>
+                <tr>
+                    <td class="key">{{lionFnTopo("cmd_scroll")}}</td>
+                    <td class="desc">{{lionFnTopo("qh_gest_cmd_scroll")}}</td>
+                </tr>
+                <tr>
+                    <td class="key" y="48">{{lionFnTopo("drag")}}</td>
+                    <td class="desc" y="48" x="74.84375">{{lionFnTopo("qh_gest_cmd_drag")}}</tr>
+            </table>
+        </div>
+    </div>
+</div>
diff --git a/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.spec.ts b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.spec.ts
new file mode 100644
index 0000000..5dfb73c
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.spec.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2019-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 { QuickhelpComponent } from './quickhelp.component';
+import {LogService} from '../../log.service';
+import {ConsoleLoggerService} from '../../consolelogger.service';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {FnService} from '../../util/fn.service';
+import {LionService} from '../../util/lion.service';
+import {KeysService} from '../../util/keys.service';
+
+class MockFnService {}
+
+class MockKeysService {
+    keyHandler: {
+        viewKeys: any[],
+        globalKeys: any[]
+    };
+
+    mockViewKeys: Object[];
+    constructor() {
+        this.mockViewKeys = [];
+        this.keyHandler = {
+            viewKeys: this.mockViewKeys,
+            globalKeys: this.mockViewKeys
+        };
+    }
+}
+
+/**
+ * ONOS GUI -- Layer -- Quickhelp Component - Unit Tests
+ */
+describe('QuickhelpComponent', () => {
+    let log: LogService;
+    let component: QuickhelpComponent;
+    let fixture: ComponentFixture<QuickhelpComponent>;
+    const bundleObj = {
+        'core.fw.QuickHelp': {
+            test: 'test1',
+            tt_help: 'Help!'
+        }
+    };
+    const mockLion = (key) =>  {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+    beforeEach(async(() => {
+        log = new ConsoleLoggerService();
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ QuickhelpComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                { provide: FnService, useClass: MockFnService },
+                { provide: LionService, useFactory: (() => {
+                        return {
+                            bundle: ((bundleId) => mockLion),
+                            ubercache: new Array(),
+                            loadCbs: new Map<string, () => void>([])
+                        };
+                    })
+                },
+                { provide: KeysService, useClass: MockKeysService }
+            ]
+        })
+        .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(QuickhelpComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.ts b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.ts
new file mode 100644
index 0000000..ca2408c
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/quickhelp/quickhelp.component.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2019-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 {animate, state, style, transition, trigger} from '@angular/animations';
+import {LogService} from '../../log.service';
+import {FnService} from '../../util/fn.service';
+import {KeysService} from '../../util/keys.service';
+import {LionService} from '../../util/lion.service';
+
+export interface KeyEntry {
+    keystroke: string;
+    text: string;
+}
+
+@Component({
+    selector: 'onos-quickhelp',
+    templateUrl: './quickhelp.component.html',
+    styleUrls: ['./quickhelp.component.css'],
+    animations: [
+        trigger('quickHelpState', [
+            state('true', style({
+                opacity: '1.0',
+            })),
+            state('false', style({
+                opacity: '0.0',
+            })),
+            transition('0 => 1', animate('500ms ease-in')),
+            transition('1 => 0', animate('500ms ease-out'))
+        ])
+    ]
+})
+export class QuickhelpComponent implements OnInit {
+    lionFn; // Function
+    lionFnTopo; // Function
+
+    dialogKeys: Object;
+    globalKeys: Object[];
+    maskedKeys: Object;
+    viewGestures: Object;
+    viewKeys: KeyEntry[][];
+
+    private static extractKeyEntry(viewKeyObj: Object, log: LogService): KeyEntry {
+        const subParts = (<any>Object).values(viewKeyObj[1]);
+        return <KeyEntry>{
+            keystroke: <string>viewKeyObj[0],
+            text: <string>subParts[1]
+        };
+    }
+
+    constructor(
+        private log: LogService,
+        private fs: FnService,
+        public ks: KeysService,
+        private lion: LionService
+    ) {
+        if (this.lion.ubercache.length === 0) {
+            this.lionFn = this.dummyLion;
+            this.lionFnTopo = this.dummyLion;
+            this.lion.loadCbs.set('quickhelp', () => this.doLion());
+        } else {
+            this.doLion();
+        }
+        this.globalKeys = [];
+        this.viewKeys = [[], [], [], [], [], [], [], [], []];
+
+        this.log.debug('QuickhelpComponent constructed');
+    }
+
+    ngOnInit(): void {
+        (<any>Object).entries(this.ks.keyHandler.viewKeys)
+            .filter((vk) => vk[0] !== '_helpFormat' && vk[0] !== '9' && vk[0] !== 'esc')
+            .forEach((vk, idx) => {
+                const ke = QuickhelpComponent.extractKeyEntry(vk, this.log);
+                this.viewKeys[Math.floor(idx / 3)][idx % 3] = ke;
+            });
+        this.log.debug('QuickhelpComponent initialized');
+        this.log.debug('view keys retrieved', this.ks.keyHandler.globalKeys);
+    }
+
+
+    /**
+     * Read the LION bundle for Toolbar and set up the lionFn
+     */
+    doLion() {
+        this.lionFn = this.lion.bundle('core.fw.QuickHelp');
+        this.lionFnTopo = this.lion.bundle('core.view.Topo');
+    }
+
+    /**
+    * A dummy implementation of the lionFn until the response is received and the LION
+    * bundle is received from the WebSocket
+    */
+    dummyLion(key: string): string {
+        return '%' + key + '%';
+    }
+}
diff --git a/web/gui2-fw-lib/lib/layer/quickhelp/test/uberlion_english_sample.json b/web/gui2-fw-lib/lib/layer/quickhelp/test/uberlion_english_sample.json
new file mode 100644
index 0000000..7ff843e
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/quickhelp/test/uberlion_english_sample.json
@@ -0,0 +1,288 @@
+{
+  "event": "uberlion",
+  "payload": {
+    "lion": {
+      "core.fw.Mast": {
+        "confirm_refresh_title": "Confirm GUI Refresh",
+        "logout": "Logout",
+        "tt_help": "Show help page for current view",
+        "ui_ok_to_update": "Press OK to update the GUI.",
+        "uicomp_added": "New GUI components were added.",
+        "uicomp_removed": "Some GUI components were removed.",
+        "unknown_user": "(no one)"
+      },
+      "core.fw.Nav": {
+        "cat_network": "Network",
+        "cat_other": "Other",
+        "cat_platform": "Platform",
+        "nav_item_app": "Applications",
+        "nav_item_cluster": "Cluster Nodes",
+        "nav_item_device": "Devices",
+        "nav_item_host": "Hosts",
+        "nav_item_intent": "Intents",
+        "nav_item_link": "Links",
+        "nav_item_partition": "Partitions",
+        "nav_item_processor": "Packet Processors",
+        "nav_item_settings": "Settings",
+        "nav_item_topo": "Topology",
+        "nav_item_topo2": "Topology 2",
+        "nav_item_tunnel": "Tunnels"
+      },
+      "core.fw.QuickHelp": {
+        "qh_hint_close_detail": "Close the details panel",
+        "qh_hint_esc": "Dismiss dialog or cancel selections",
+        "qh_hint_show_hide_qh": "Show / hide Quick Help",
+        "qh_hint_t": "Toggle theme",
+        "qh_title": "Quick Help"
+      },
+      "core.view.App": {
+        "activate": "Activate",
+        "app_id": "App ID",
+        "category": "Category",
+        "click_row": "click row",
+        "deactivate": "Deactivate",
+        "dlg_confirm_action": "Confirm Action",
+        "dlg_warn_deactivate": "Deactivating or uninstalling this component can have serious negative consequences!",
+        "dlg_warn_own_risk": "** DO SO AT YOUR OWN RISK **",
+        "dp_features": "Features",
+        "dp_permissions": "Permissions",
+        "dp_required_apps": "Required Apps",
+        "icon": "Icon",
+        "nav_item_app": "Applications",
+        "origin": "Origin",
+        "qh_hint_click_row": "Select / deselect application",
+        "qh_hint_close_detail": "Close the details panel",
+        "qh_hint_esc": "Deselect application",
+        "qh_hint_scroll_down": "See more applications",
+        "role": "Role",
+        "scroll_down": "scroll down",
+        "state": "State",
+        "title": "Title",
+        "title_apps": "Applications",
+        "total": "Total",
+        "tt_ctl_activate": "Activate selected application",
+        "tt_ctl_auto_refresh": "Toggle auto refresh",
+        "tt_ctl_deactivate": "Deactivate selected application",
+        "tt_ctl_download": "Download selected application (.oar file)",
+        "tt_ctl_uninstall": "Uninstall selected application",
+        "tt_ctl_upload": "Upload an application (.oar file)",
+        "uninstall": "Uninstall",
+        "version": "Version"
+      },
+      "core.view.Cluster": {
+        "active": "Active",
+        "chassis_id": "Chassis ID",
+        "click": "click",
+        "devices": "Devices",
+        "hw_version": "H/W Version",
+        "ip_address": "IP Address",
+        "last_updated": "Last Updated",
+        "nav_item_cluster": "Cluster Nodes",
+        "node_id": "Node ID",
+        "protocol": "Protocol",
+        "qh_hint_click": "Select a row to show cluster node details",
+        "qh_hint_close_detail": "Close the details panel",
+        "qh_hint_scroll_down": "See available cluster nodes",
+        "scroll_down": "scroll down",
+        "serial_number": "Serial #",
+        "started": "Started",
+        "sw_version": "S/W Version",
+        "tcp_port": "TCP Port",
+        "title_cluster_nodes": "Cluster Nodes",
+        "total": "Total",
+        "type": "Type",
+        "uri": "URI",
+        "vendor": "Vendor"
+      },
+      "core.view.Topo": {
+        "active": "active",
+        "added": "added",
+        "btn_show_view_device": "Show Device View",
+        "btn_show_view_flow": "Show Flow View for this Device",
+        "btn_show_view_group": "Show Group View for this Device",
+        "btn_show_view_meter": "Show Meter View for this Device",
+        "btn_show_view_port": "Show Port View for this Device",
+        "click": "click",
+        "close": "Close",
+        "cmd_drag": "cmd-drag",
+        "cmd_scroll": "cmd-scroll",
+        "device": "Device",
+        "devices": "Devices",
+        "direct": "direct",
+        "disable": "Disable",
+        "drag": "drag",
+        "edge": "edge",
+        "enable": "Enable",
+        "expected": "expected",
+        "fl_background_map": "background map",
+        "fl_bad_links": "Bad Links",
+        "fl_device_labels_hide": "Hide device labels",
+        "fl_device_labels_show_friendly": "Show friendly device labels",
+        "fl_device_labels_show_id": "Show device ID labels",
+        "fl_eq_masters": "Equalizing master roles",
+        "fl_grid_display_1000": "Show XY grid",
+        "fl_grid_display_both": "Show both grids",
+        "fl_grid_display_geo": "Show Geo grid",
+        "fl_grid_display_hide": "Hide grid",
+        "fl_host_labels_hide": "Hide host labels",
+        "fl_host_labels_show_friendly": "Show friendly host labels",
+        "fl_host_labels_show_ip": "Show host IP addresses",
+        "fl_host_labels_show_mac": "Show host MAC addresses",
+        "fl_layer_all": "All Layers Shown",
+        "fl_layer_opt": "Optical Layer Shown",
+        "fl_layer_pkt": "Packet Layer Shown",
+        "fl_monitoring_canceled": "Monitoring Canceled",
+        "fl_normal_view": "Normal View",
+        "fl_oblique_view": "Oblique View",
+        "fl_offline_devices": "Offline Devices",
+        "fl_pan_zoom_reset": "Pan and zoom reset",
+        "fl_panel_details": "Details Panel",
+        "fl_panel_instances": "Instances Panel",
+        "fl_panel_summary": "Summary Panel",
+        "fl_pinned_floating_nodes": "Pinned floating nodes",
+        "fl_port_highlighting": "Port Highlighting",
+        "fl_reset_node_locations": "Reset Node Locations",
+        "fl_selecting_intent": "Selecting Intent",
+        "fl_sprite_layer": "sprite layer",
+        "fl_unpinned_floating_nodes": "Unpinned floating nodes",
+        "flows": "Flows",
+        "grid_x": "Grid X",
+        "grid_y": "Grid Y",
+        "hidden": "Hidden",
+        "hide": "Hide",
+        "host": "Host",
+        "hosts": "Hosts",
+        "hw_version": "H/W Version",
+        "inactive": "inactive",
+        "indirect": "indirect",
+        "intent": "Intent",
+        "intents": "Intents",
+        "ip": "IP",
+        "latitude": "Latitude",
+        "links": "Links",
+        "longitude": "Longitude",
+        "lp_label_a2b": "A to B",
+        "lp_label_a_friendly": "A name",
+        "lp_label_a_id": "A id",
+        "lp_label_a_port": "A port",
+        "lp_label_a_type": "A type",
+        "lp_label_b2a": "B to A",
+        "lp_label_b_friendly": "B name",
+        "lp_label_b_id": "B id",
+        "lp_label_b_port": "B port",
+        "lp_label_b_type": "B type",
+        "lp_label_friendly": "Friendly",
+        "lp_value_no_link": "[no link]",
+        "mac": "MAC",
+        "nav_item_topo": "Topology",
+        "no_devices_are_connected": "No Devices Are Connected",
+        "not_expected": "not expected",
+        "ok": "OK",
+        "optical": "optical",
+        "ov_tt_none": "No Overlay",
+        "ov_tt_protected_intents": "Protected Intents Overlay",
+        "ov_tt_traffic": "Traffic Overlay",
+        "ports": "Ports",
+        "protocol": "Protocol",
+        "purged": "purged",
+        "qh_gest_click": "Select the item and show details",
+        "qh_gest_cmd_drag": "Pan",
+        "qh_gest_cmd_scroll": "Zoom in / out",
+        "qh_gest_drag": "Reposition (and pin) device / host",
+        "qh_gest_shift_click": "Toggle selection state",
+        "qh_hint_close_detail": "Close the details panel",
+        "resubmitted": "resubmitted",
+        "select": "Select",
+        "serial_number": "Serial #",
+        "shift_click": "shift-click",
+        "show": "Show",
+        "sw_version": "S/W Version",
+        "tbtt_bad_links": "Show bad links",
+        "tbtt_cyc_dev_labs": "Cycle device labels",
+        "tbtt_cyc_grid_display": "Cycle grid display",
+        "tbtt_cyc_host_labs": "Cycle host labels",
+        "tbtt_cyc_layers": "Cycle node layers",
+        "tbtt_eq_master": "Equalize mastership roles",
+        "tbtt_reset_loc": "Reset node locations",
+        "tbtt_reset_zoom": "Reset pan / zoom",
+        "tbtt_sel_map": "Select background geo map",
+        "tbtt_tog_host": "Toggle host visibility",
+        "tbtt_tog_instances": "Toggle ONOS instances panel",
+        "tbtt_tog_map": "Toggle background geo map",
+        "tbtt_tog_oblique": "Toggle oblique view (experimental)",
+        "tbtt_tog_offline": "Toggle offline visibility",
+        "tbtt_tog_porthi": "Toggle port highlighting",
+        "tbtt_tog_sprite": "Toggle sprite layer",
+        "tbtt_tog_summary": "Toggle ONOS summary panel",
+        "tbtt_tog_toolbar": "Toggle Toolbar",
+        "tbtt_tog_use_detail": "Disable / enable details panel",
+        "tbtt_unpin_node": "Unpin node (hover mouse over)",
+        "title_edge_link": "Edge Link",
+        "title_infra_link": "Infrastructure Link",
+        "title_panel_summary": "ONOS Summary",
+        "title_select_map": "Select Map",
+        "title_selected_items": "Selected Items",
+        "topology_sccs": "Topology SCCs",
+        "tr_btn_cancel_monitoring": "Cancel traffic monitoring",
+        "tr_btn_create_h2h_flow": "Create Host-to-Host Flow",
+        "tr_btn_create_msrc_flow": "Create Multi-Source Flow",
+        "tr_btn_monitor_all": "Monitor all traffic",
+        "tr_btn_monitor_sel_intent": "Monitor traffic of selected intent",
+        "tr_btn_show_all_rel_intents": "Show all related intents",
+        "tr_btn_show_dev_link_flows": "Show device link flows",
+        "tr_btn_show_device_flows": "Show Device Flows",
+        "tr_btn_show_next_rel_intent": "Show next related intent",
+        "tr_btn_show_prev_rel_intent": "Show previous related intent",
+        "tr_btn_show_related_traffic": "Show Related Traffic",
+        "tr_fl_dev_flows": "Device Flows",
+        "tr_fl_fstats_bytes": "Flow Stats (bytes)",
+        "tr_fl_h2h_flow_added": "Host-to-Host flow added",
+        "tr_fl_multisrc_flow": "Multi-Source Flow",
+        "tr_fl_next_rel_int": "Next related intent",
+        "tr_fl_prev_rel_int": "Previous related intent",
+        "tr_fl_pstats_bits": "Port Stats (bits / second)",
+        "tr_fl_pstats_pkts": "Port Stats (packets / second)",
+        "tr_fl_rel_paths": "Related Paths",
+        "tr_fl_traf_on_path": "Traffic on Selected Path",
+        "tunnel": "tunnel",
+        "tunnels": "Tunnels",
+        "uri": "URI",
+        "vendor": "Vendor",
+        "version": "Version",
+        "virtual": "virtual",
+        "visible": "Visible",
+        "vlan": "VLAN",
+        "vlan_none": "None",
+        "withdrawn": "withdrawn"
+      },
+      "core.view.Flow": {
+        "appId": "App ID",
+        "appName": "App Name",
+        "bytes": "Bytes",
+        "duration": "Duration ",
+        "flowId": "Flow ID",
+        "groupId": "Group ID",
+        "hardTimeout": "Hard Timeout",
+        "idleTimeout": "Idle Timeout",
+        "nav_item_flow": "Flows",
+        "packets": "Packets",
+        "permanent": "Permanent",
+        "priority": "Flow Priority ",
+        "selector": "Selector",
+        "state": "State",
+        "tableName": "Table Name ",
+        "title_flows": "Flows for Device ",
+        "total": "Total",
+        "treatment": "Treatment",
+        "tt_ctl_show_device": "Show device table",
+        "tt_ctl_show_group": "Show group view for this device",
+        "tt_ctl_show_meter": "Show meter view for selected device",
+        "tt_ctl_show_pipeconf": "Show pipeconf view for selected device",
+        "tt_ctl_show_port": "Show port view for this device",
+        "tt_ctl_switcth_brief": "Switch to brief view",
+        "tt_ctl_switcth_detailed": "Switch to detailed view"
+      }
+    },
+    "locale": "en_IE"
+  }
+}
diff --git a/web/gui2-fw-lib/lib/layer/veil/veil.component.css b/web/gui2-fw-lib/lib/layer/veil/veil.component.css
new file mode 100644
index 0000000..b688ae7
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/veil/veil.component.css
@@ -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.
+ */
+
+/*
+ ONOS GUI -- Veil Service (layout) -- CSS file
+ */
+
+#veil {
+    z-index: 5000;
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    padding: 60px;
+}
+
+#veil p {
+    display: block;
+    text-align: left;
+    font-size: 14pt;
+    font-style: italic;
+}
diff --git a/web/gui2-fw-lib/lib/layer/veil/veil.component.html b/web/gui2-fw-lib/lib/layer/veil/veil.component.html
new file mode 100644
index 0000000..69410c5
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/veil/veil.component.html
@@ -0,0 +1,25 @@
+<!--
+~ 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="veil" *ngIf="enabled">
+    <p *ngFor="let msg of messages">{{ msg }}</p>
+    <svg [attr.width]="fs.windowSize().width" [attr.height]="fs.windowSize().height">
+        <use [attr.width]="birdDim" [attr.height]="birdDim" class="glyph"
+             style="opacity: 0.2;"
+             xlink:href = "#bird" [attr.transform]="trans"/>
+
+    </svg>
+</div>
diff --git a/web/gui2-fw-lib/lib/layer/veil/veil.component.spec.ts b/web/gui2-fw-lib/lib/layer/veil/veil.component.spec.ts
new file mode 100644
index 0000000..72123dd
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/veil/veil.component.spec.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 -- Layer -- Veil Service - Unit Tests
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+
+import { VeilComponent } from './veil.component';
+import { ConsoleLoggerService } from '../../consolelogger.service';
+import { FnService } from '../../util/fn.service';
+import { LogService } from '../../log.service';
+import { GlyphService } from '../../svg/glyph.service';
+import { of } from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockGlyphService {}
+
+describe('VeilComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({});
+        windowMock = <any>{
+            location: <any> {
+                hostname: 'foo'
+            }
+        };
+        fs = new FnService(ar, logSpy, windowMock);
+
+        TestBed.configureTestingModule({
+            declarations: [ VeilComponent ],
+            providers: [
+                { provide: FnService, useValue: fs },
+                { provide: LogService, useValue: logSpy },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        });
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    it('should create', () => {
+        const fixture = TestBed.createComponent(VeilComponent);
+        const component = fixture.componentInstance;
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-fw-lib/lib/layer/veil/veil.component.theme.css b/web/gui2-fw-lib/lib/layer/veil/veil.component.theme.css
new file mode 100644
index 0000000..2939b9f
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/veil/veil.component.theme.css
@@ -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.
+ */
+
+/*
+ ONOS GUI -- Veil Service (theme) -- CSS file
+ */
+
+.light, #veil {
+    background-color: rgba(0,0,0,0.75);
+}
+.dark, #veil {
+    background-color: rgba(64,64,64,0.75);
+}
+
+#veil p {
+    color: #ddd;
+}
+
+#veil svg .glyph {
+    fill: #222;
+}
diff --git a/web/gui2-fw-lib/lib/layer/veil/veil.component.ts b/web/gui2-fw-lib/lib/layer/veil/veil.component.ts
new file mode 100644
index 0000000..b93d950
--- /dev/null
+++ b/web/gui2-fw-lib/lib/layer/veil/veil.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 } from '@angular/core';
+import { FnService } from '../../util/fn.service';
+import { GlyphService } from '../../svg/glyph.service';
+import { LogService } from '../../log.service';
+import { SvgUtilService } from '../../svg/svgutil.service';
+import { WebSocketService } from '../../remote/websocket.service';
+
+const BIRD = 'bird';
+
+/**
+ * ONOS GUI -- Layer -- Veil Component
+ *
+ * Provides a mechanism to display an overlaying div with information.
+ * Used mainly for web socket connection interruption.
+ *
+ * It can be added to an component's template as follows:
+ *     <onos-veil #veil></onos-veil>
+ *     <p (click)="veil.show(['t1','t2','t3'])">Test Veil</p>
+ */
+@Component({
+  selector: 'onos-veil',
+  templateUrl: './veil.component.html',
+  styleUrls: ['./veil.component.css', './veil.component.theme.css']
+})
+export class VeilComponent implements OnInit {
+    ww: number;
+    wh: number;
+    birdSvg: string;
+    birdDim: number;
+    enabled: boolean = false;
+    trans: string;
+    messages: string[] = [];
+    veilStyle: string;
+
+    constructor(
+        public fs: FnService,
+        private gs: GlyphService,
+        private log: LogService,
+        private sus: SvgUtilService,
+        private wss: WebSocketService
+    ) {
+        const wSize = this.fs.windowSize();
+        this.ww = wSize.width;
+        this.wh = wSize.height;
+        const shrink = this.wh * 0.3;
+        this.birdDim = this.wh - shrink;
+        const birdCenter = (this.ww - this.birdDim) / 2;
+        this.trans = this.sus.translate([birdCenter, shrink / 2]);
+
+        this.log.debug('VeilComponent with ' + BIRD + ' constructed');
+    }
+
+    ngOnInit() {
+    }
+
+    // msg should be an array of strings
+    show(msgs: string[]): void {
+        this.messages = msgs;
+        this.enabled = true;
+//        this.ks.enableKeys(false);
+    }
+
+    hide(): void {
+        this.veilStyle = 'display: none';
+//        this.ks.enableKeys(true);
+    }
+
+
+}
diff --git a/web/gui2-fw-lib/lib/log.service.spec.ts b/web/gui2-fw-lib/lib/log.service.spec.ts
new file mode 100644
index 0000000..e2998f8
--- /dev/null
+++ b/web/gui2-fw-lib/lib/log.service.spec.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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from './log.service';
+
+/**
+ * ONOS GUI -- Log Service - Unit Tests
+ */
+describe('LogService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [LogService]
+    });
+  });
+
+  it('should be created', inject([LogService], (service: LogService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/web/gui2-fw-lib/lib/log.service.ts b/web/gui2-fw-lib/lib/log.service.ts
new file mode 100644
index 0000000..2835371
--- /dev/null
+++ b/web/gui2-fw-lib/lib/log.service.ts
@@ -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.
+ */
+import { Injectable } from '@angular/core';
+
+export abstract class Logger {
+  debug: any;
+  info: any;
+  warn: any;
+  error: any;
+}
+
+/**
+ * ONOS GUI -- LogService
+ * Inspired by https://robferguson.org/blog/2017/09/09/a-simple-logging-service-for-angular-4/
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class LogService extends Logger {
+  debug: any;
+  info: any;
+  warn: any;
+  error: any;
+
+  invokeConsoleMethod(type: string, args?: any): void {}
+}
diff --git a/web/gui2-fw-lib/lib/mast/mast.service.spec.ts b/web/gui2-fw-lib/lib/mast/mast.service.spec.ts
new file mode 100644
index 0000000..ca99af8
--- /dev/null
+++ b/web/gui2-fw-lib/lib/mast/mast.service.spec.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { MastService } from './mast.service';
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {
+    isMobile() {}
+}
+
+/**
+ * ONOS GUI -- Masthead Service - Unit Tests
+ */
+describe('MastService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [MastService,
+                { provide: FnService, useClass: MockFnService },
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([MastService], (service: MastService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/mast/mast.service.ts b/web/gui2-fw-lib/lib/mast/mast.service.ts
new file mode 100644
index 0000000..b279aec
--- /dev/null
+++ b/web/gui2-fw-lib/lib/mast/mast.service.ts
@@ -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.
+ */
+import { Injectable } from '@angular/core';
+import { FnService } from '../util/fn.service';
+import { LogService } from '../log.service';
+
+const PADMOBILE = 16;
+
+/**
+ * ONOS GUI -- Masthead Service
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class MastService {
+
+  public mastHeight = 48;
+
+  constructor(
+    private fs: FnService,
+    private log: LogService
+  ) {
+    if (this.fs.isMobile()) {
+        this.mastHeight += PADMOBILE;
+    }
+
+    this.log.debug('MastService constructed');
+  }
+
+}
diff --git a/web/gui2-fw-lib/lib/mast/mast/mast.component.css b/web/gui2-fw-lib/lib/mast/mast/mast.component.css
new file mode 100644
index 0000000..ec7612a
--- /dev/null
+++ b/web/gui2-fw-lib/lib/mast/mast/mast.component.css
@@ -0,0 +1,132 @@
+/*
+ * 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 -- Masthead (layout) -- CSS file
+ */
+#mast-top-block {
+    display: block;
+    z-index: -1;
+    height: 48px;
+    width: 100%;
+}
+
+#mast {
+    position: absolute;
+    width: 100%;
+    top: 0px;
+    height: 48px;
+    padding: 0;
+    z-index: 10000;
+}
+
+#mast a:hover {
+    text-decoration: none;
+}
+
+html[data-platform='iPad'] #mast {
+    padding-top: 16px;
+}
+
+#mast .nav-menu-button {
+    display: inline-block;
+    vertical-align: middle;
+    text-align: center;
+    line-height: 48px;
+    padding: 0 12px;
+    cursor: pointer; cursor: hand;
+    /* Needed to removed 3px space at the bottom of img tags */
+    font-size: 0;
+}
+
+#mast .nav-menu-button img {
+    width: 25px;
+    vertical-align: middle;
+}
+
+#mast .logo {
+    height: 47px;
+    width: 511px;
+    vertical-align: bottom;
+}
+
+#mast-right {
+    display: inline-block;
+    float: right;
+    position: relative;
+    top: 0;
+    padding-right: 15px;
+    line-height: 48px;
+}
+
+/*
+    MAST HEAD DROPDOWN MENU
+*/
+
+#mast-right div.ctrl-btns {
+    float: right;
+}
+
+#mast-right div.icon {
+    box-sizing: border-box;
+    position: relative;
+    height: 48px;
+    width: 48px;
+    padding: 9px;
+}
+
+#mast .dropdown-parent {
+    position: relative;
+    float: right;
+}
+
+#mast .dropdown-parent i.dropdown-icon {
+    display: inline-block;
+    height: 7px;
+    width: 9px;
+    margin-left: 10px;
+    /*background: url('data/img/dropdown-icon.png') no-repeat;*/
+}
+
+#mast .dropdown {
+    position: absolute;
+    top: 40px;
+    right: -8px;
+    display: none;
+    min-width: 100px;
+    line-height: 16px;
+    font-size: 12pt;
+    z-index: 1000;
+}
+
+#mast .dropdown a {
+    text-decoration: none;
+    font-size: 12px;
+    display: block;
+    padding: 8px 16px 6px 12px;
+}
+
+#mast .dropdown-parent:hover .dropdown {
+    display: block;
+}
+
+#mast .dropdown-parent:hover i.dropdown-icon {
+    background-position-x: -14px
+}
+
+html[data-platform='iPad'] #mast .dropdown {
+    top: 57px;
+}
diff --git a/web/gui2-fw-lib/lib/mast/mast/mast.component.html b/web/gui2-fw-lib/lib/mast/mast/mast.component.html
new file mode 100644
index 0000000..437f96d
--- /dev/null
+++ b/web/gui2-fw-lib/lib/mast/mast/mast.component.html
@@ -0,0 +1,44 @@
+<!--
+~ 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="mast-top-block"></div>
+<!-- The mast-top-block is an inline display element that pushes any
+  subsequent elements down the page. It has a height of 48px
+     The mast block overlays the mast-top-block. It is is positioned
+     absolutely so that the nav component can slide in behind it when
+     not shown -->
+<div id="mast" align="left">
+    <span class="nav-menu-button clickable" (click)="ns.toggleNav()">
+        <img src="data/img/nav-menu-mojo.png"/>
+    </span>
+    <img class="logo" src="data/img/masthead-logo-mojo.png">
+    <onos-confirm title="{{ lionFn('ui_ok_to_update') }}" message="{{ confirmMessage }}" warning="{{ strongWarning }}" (chosen)="dOk($event)"></onos-confirm>
+    <div id="mast-right">
+        <nav>
+            <div class="dropdown-parent">
+                <a class="clickable user-menu__name">{{ username }} <i class="dropdown-icon"></i></a>
+                <div class="dropdown">
+                    <a href="rs/logout"> {{ lionFn('logout') }} </a>
+                </div>
+            </div>
+            <div class="ctrl-btns">
+                <div class="active clickable icon" (click)="directTo()">
+                    <onos-icon iconId="query" iconSize="32" toolTip="{{ lionFn('tt_help') }}"></onos-icon>
+                </div>
+            </div>
+        </nav>
+
+    </div>
+</div>
diff --git a/web/gui2-fw-lib/lib/mast/mast/mast.component.spec.ts b/web/gui2-fw-lib/lib/mast/mast/mast.component.spec.ts
new file mode 100644
index 0000000..fca2dd9
--- /dev/null
+++ b/web/gui2-fw-lib/lib/mast/mast/mast.component.spec.ts
@@ -0,0 +1,135 @@
+/*
+ * 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 { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+import { LogService } from '../../log.service';
+import { ConsoleLoggerService } from '../../consolelogger.service';
+import { MastComponent } from './mast.component';
+import { IconComponent } from '../../svg/icon/icon.component';
+import { ConfirmComponent } from '../../layer/confirm/confirm.component';
+import { LionService } from '../../util/lion.service';
+import { IconService } from '../../svg/icon.service';
+import { NavService } from '../../nav/nav.service';
+import { WebSocketService } from '../../remote/websocket.service';
+
+class MockNavService {}
+
+class MockIconService {
+    loadIconDef() {}
+}
+
+class MockWebSocketService {
+    createWebSocket() {}
+    isConnected() { return false; }
+    unbindHandlers() {}
+    bindHandlers() {}
+}
+
+/**
+ * ONOS GUI -- Masthead Controller - Unit Tests
+ */
+describe('MastComponent', () => {
+    let log: LogService;
+    let windowMock: Window;
+    let component: MastComponent;
+    let fixture: ComponentFixture<MastComponent>;
+    const bundleObj = {
+        'core.view.App': {
+            test: 'test1',
+            tt_help: 'Help!'
+        }
+    };
+    const mockLion = (key) =>  {
+        return bundleObj[key] || '%' + key + '%';
+    };
+
+    beforeEach(async(() => {
+        log = new ConsoleLoggerService();
+        windowMock = <any>{
+            location: <any> {
+                hostname: 'foo',
+                pathname: 'apps',
+                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'
+            }
+        };
+
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule, RouterTestingModule ],
+            declarations: [ MastComponent, IconComponent, ConfirmComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                { provide: NavService, useClass: MockNavService },
+                { provide: LionService, useFactory: (() => {
+                        return {
+                            bundle: ((bundleId) => mockLion),
+                            ubercache: new Array(),
+                            loadCbs: new Map<string, () => void>([])
+                        };
+                    })
+                },
+                { provide: IconService, useClass: MockIconService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: 'Window', useValue: windowMock }
+            ]
+        })
+        .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(MastComponent);
+        component = fixture.debugElement.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should have a div#mast-top-block', () => {
+        const appDe: DebugElement = fixture.debugElement;
+        const divDe = appDe.query(By.css('div#mast-top-block'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a span.nav-menu-button inside a div#mast', () => {
+        const appDe: DebugElement = fixture.debugElement;
+        const divDe = appDe.query(By.css('div#mast span.nav-menu-button'));
+        expect(divDe).toBeTruthy();
+    });
+
+    it('should have a div.dropdown-parent inside a div#mast-right inside a div#mast', () => {
+        const appDe: DebugElement = fixture.debugElement;
+        const divDe = appDe.query(By.css('div#mast div#mast-right div.dropdown-parent'));
+        const div: HTMLElement = divDe.nativeElement;
+        expect(div.textContent).toEqual('  %logout% ');
+    });
+
+    it('should have an onos-icon inside a div#mast-right inside a div#mast', () => {
+        const appDe: DebugElement = fixture.debugElement;
+        const divDe = appDe.query(By.css('div#mast div#mast-right div.ctrl-btns div.active onos-icon'));
+        expect(divDe).toBeTruthy();
+    });
+
+});
diff --git a/web/gui2-fw-lib/lib/mast/mast/mast.component.ts b/web/gui2-fw-lib/lib/mast/mast/mast.component.ts
new file mode 100644
index 0000000..41e2f3e
--- /dev/null
+++ b/web/gui2-fw-lib/lib/mast/mast/mast.component.ts
@@ -0,0 +1,128 @@
+/*
+ * 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, Inject, NgZone } from '@angular/core';
+import { Router } from '@angular/router';
+import { LionService } from '../../util/lion.service';
+import { LogService } from '../../log.service';
+import { NavService } from '../../nav/nav.service';
+import { WebSocketService } from '../../remote/websocket.service';
+
+/**
+ * ONOS GUI -- Masthead Component
+ */
+@Component({
+  selector: 'onos-mast',
+  templateUrl: './mast.component.html',
+  styleUrls: ['./mast.component.css', './mast.theme.css']
+})
+export class MastComponent implements OnInit, OnDestroy {
+    @Input() username: string;
+
+    lionFn; // Function
+    viewMap = new Map<string, string>([]); // A map of app names
+    confirmMessage: string = '';
+    strongWarning: string = '';
+
+    constructor(
+        private lion: LionService,
+        private log: LogService,
+        public ns: NavService,
+        private wss: WebSocketService,
+        private router: Router,
+        private zone: NgZone,
+        @Inject('Window') private window: any,
+    ) {
+        this.viewMap.set('apps', 'https://wiki.onosproject.org/display/ONOS/GUI+Application+View');
+        this.viewMap.set('device', 'https://wiki.onosproject.org/display/ONOS/GUI+Device+View');
+        this.viewMap.set('', 'https://wiki.onosproject.org/display/ONOS/The+ONOS+Web+GUI');
+    }
+
+    ngOnInit() {
+        if (this.lion.ubercache.length === 0) {
+            this.lionFn = this.dummyLion;
+            this.lion.loadCbs.set('mast', () => this.doLion());
+            this.log.debug('LION not available when MastComponent initialized');
+        } else {
+            this.doLion();
+        }
+
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            ['guiRemoved', (data) => this.triggerRefresh(data, false) ],
+            ['guiAdded', (data) => this.triggerRefresh(data, true) ]
+        ]));
+        this.log.debug('MastComponent initialized');
+    }
+
+    /**
+     * Nav component should never be closed, but in case it does, it's
+     * safer to tidy up after itself
+     */
+    ngOnDestroy() {
+        this.lion.loadCbs.delete('mast');
+    }
+
+    /**
+    * Read the LION bundle for App and set up the lionFn
+    */
+    doLion() {
+        this.lionFn = this.lion.bundle('core.fw.Mast');
+        if (this.username === undefined) {
+            this.username = this.lionFn('unknown_user');
+        }
+    }
+
+    /**
+    * A dummy implementation of the lionFn until the response is received and the LION
+    * bundle is received from the WebSocket
+    */
+    dummyLion(key: string): string {
+        return '%' + key + '%';
+    }
+
+    directTo() {
+        const curId = this.window.location.pathname.replace('/', '');
+        let helpUrl: string = this.viewMap.get(curId);
+        if (helpUrl === undefined) {
+            helpUrl = this.viewMap.get('');
+            this.log.warn('No help file linked for view:', curId);
+        }
+        this.window.open(helpUrl);
+    }
+
+    triggerRefresh(data: any, added: boolean): void {
+        this.confirmMessage = this.lionFn(added ? 'uicomp_added' : 'uicomp_removed');
+        this.log.debug('Refresh has been triggered - item', added ? 'added' : 'removed', ' - ', data);
+    }
+
+    /**
+     * Callback when the Confirm dialog is shown and a choice is made
+     */
+    dOk(choice: boolean) {
+        if (choice) {
+            this.ns.getUiViews();
+            this.router.navigate(['/']);
+            this.zone.runOutsideAngular(() => {
+                location.reload();
+            });
+            this.log.debug('Refresh confirmed'); // Will not be printed if page reloads
+
+        } else {
+            this.log.debug('Refresh cancelled');
+        }
+        this.confirmMessage = '';
+        this.strongWarning = '';
+    }
+}
diff --git a/web/gui2-fw-lib/lib/mast/mast/mast.theme.css b/web/gui2-fw-lib/lib/mast/mast/mast.theme.css
new file mode 100644
index 0000000..968aefa
--- /dev/null
+++ b/web/gui2-fw-lib/lib/mast/mast/mast.theme.css
@@ -0,0 +1,51 @@
+/*
+ * 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 -- Masthead (theme) -- CSS file
+ */
+
+#mast {
+    background-color: #231f20;
+}
+
+#mast .nav-menu-button:hover {
+    background-color: #888;
+}
+
+#mast-right a {
+    color: #009fdb;
+}
+
+#mast nav  {
+    color: #009fdb;
+}
+
+/* Theme styles for drop down menu */
+
+#mast .dropdown {
+    background-color: #231f20;
+    border: 1px solid #dddddd;
+}
+
+#mast .dropdown a {
+    color: #009fdb;
+    border-bottom: solid #444 1px;
+}
+
+#mast .dropdown a:hover {
+    color: #fff;
+}
diff --git a/web/gui2-fw-lib/lib/nav/nav.service.spec.ts b/web/gui2-fw-lib/lib/nav/nav.service.spec.ts
new file mode 100644
index 0000000..88ab5fa
--- /dev/null
+++ b/web/gui2-fw-lib/lib/nav/nav.service.spec.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import { HttpClient, HttpErrorResponse } from '@angular/common/http';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { NavService } from './nav.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+class MockHttpClient {}
+
+
+/**
+ * ONOS GUI -- Util -- Navigation Service - Unit Tests
+ */
+describe('NavService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [NavService,
+                { provide: HttpClient, useClass: MockHttpClient },
+                { provide: FnService, useClass: MockFnService },
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([NavService], (service: NavService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/nav/nav.service.ts b/web/gui2-fw-lib/lib/nav/nav.service.ts
new file mode 100644
index 0000000..30d957b
--- /dev/null
+++ b/web/gui2-fw-lib/lib/nav/nav.service.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { FnService } from '../util/fn.service';
+import { LogService } from '../log.service';
+import {UrlFnService} from "../remote/urlfn.service";
+
+export interface UiView {
+    id: string;
+    icon: string;
+    cat: string;
+    label: string;
+}
+
+/**
+ * ONOS GUI -- Navigation Service
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class NavService {
+    public showNav = false;
+
+    uiPlatformViews = new Array<UiView>();
+    uiNetworkViews = new Array<UiView>();
+    uiOtherViews = new Array<UiView>();
+    uiHiddenViews = new Array<UiView>();
+
+    constructor(
+        private _fn_: FnService,
+        private log: LogService,
+        private ufs: UrlFnService,
+        private httpClient: HttpClient,
+    ) {
+        this.log.debug('NavService constructed');
+    }
+
+    hideNav() {
+        this.showNav = false;
+        this.log.debug('Hiding Nav menu');
+    }
+
+    toggleNav() {
+        this.showNav = !this.showNav;
+        if (this.showNav) {
+            this.log.debug('Showing Nav menu');
+            this.getUiViews();
+        } else {
+            this.log.debug('Hiding Nav menu');
+        }
+    }
+
+    getUiViews() {
+        this.uiPlatformViews = new Array<UiView>();
+        this.uiNetworkViews = new Array<UiView>();
+        this.uiOtherViews = new Array<UiView>();
+        this.uiHiddenViews = new Array<UiView>();
+        this.httpClient.get(this.ufs.rsUrl('nav/uiextensions')).subscribe((v: UiView[]) => {
+            v.forEach((uiView: UiView) => {
+                if (uiView.cat === 'PLATFORM') {
+                    this.uiPlatformViews.push(uiView);
+                } else if (uiView.cat === 'NETWORK') {
+                    if ( uiView.id !== 'topo') {
+                        this.uiNetworkViews.push(uiView);
+                    } else {
+                        this.uiNetworkViews.push(<UiView>{
+                            id: 'topo2',
+                            icon: 'nav_topo',
+                            cat: 'NETWORK',
+                            label: uiView.label
+                        });
+                    }
+                } else if (uiView.cat === 'HIDDEN') {
+                    this.uiHiddenViews.push(uiView);
+                } else {
+                    this.uiOtherViews.push(uiView);
+                }
+            });
+        });
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/onos.service.ts b/web/gui2-fw-lib/lib/onos.service.ts
new file mode 100644
index 0000000..17a30ff
--- /dev/null
+++ b/web/gui2-fw-lib/lib/onos.service.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { LogService } from './log.service';
+
+/**
+ * A structure of View elements for the OnosService
+ */
+export interface View {
+    id: string;
+    path: string;
+}
+
+/**
+ * ONOS GUI -- OnosService - a placeholder for the global onos variable
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class OnosService {
+    // Global variable
+    public browser: string;
+    public mobile: boolean;
+    public viewMap: View[];
+
+    constructor (
+        private log: LogService
+    ) {
+//        this.log.debug('OnosService constructed');
+    }
+}
diff --git a/web/gui2-fw-lib/lib/remote/urlfn.service.spec.ts b/web/gui2-fw-lib/lib/remote/urlfn.service.spec.ts
new file mode 100644
index 0000000..2c31610
--- /dev/null
+++ b/web/gui2-fw-lib/lib/remote/urlfn.service.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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { UrlFnService } from './urlfn.service';
+import { FnService } from '../util/fn.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Remote -- General Functions - Unit Tests
+ */
+describe('UrlFnService', () => {
+    let log: LogService;
+    let ufs: UrlFnService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        windowMock = <any>{
+            location: <any> {
+                hostname: '',
+                host: '',
+                port: '',
+                protocol: '',
+                search: { debug: 'true'},
+                href: ''
+            }
+        };
+
+        fs = new FnService(ar, log, windowMock);
+
+        TestBed.configureTestingModule({
+            providers: [UrlFnService,
+                { provide: LogService, useValue: log },
+                { provide: 'Window', useFactory: (() => windowMock ) },
+            ]
+        });
+
+        ufs = TestBed.get(UrlFnService);
+    });
+
+    function setLoc(prot: string, h: string, p: string, ctx: string = '') {
+        windowMock.location.host = h;
+        windowMock.location.hostname = h;
+        windowMock.location.port = p;
+        windowMock.location.protocol = prot;
+        windowMock.location.href = prot + '://' + h + ':' + p +
+            ctx + '/onos/ui/';
+    }
+
+    it('should define UrlFnService', () => {
+        expect(ufs).toBeDefined();
+    });
+
+    it('should define api functions', () => {
+        expect(fs.areFunctions(ufs, [
+            'rsUrl', 'wsUrl', 'urlBase', 'httpPrefix',
+            'wsPrefix', 'matchSecure'
+        ])).toBeTruthy();
+    });
+
+    it('should return the correct (http) RS url', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.rsUrl('path')).toEqual('http://foo:123/onos/ui/rs/path');
+    });
+
+    it('should return the correct (https) RS url', () => {
+        setLoc('https', 'foo', '123');
+        expect(ufs.rsUrl('path')).toEqual('https://foo:123/onos/ui/rs/path');
+    });
+
+    it('should return the correct (ws) WS url', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.wsUrl('path')).toEqual('ws://foo:123/onos/ui/websock/path');
+    });
+
+    it('should return the correct (wss) WS url', () => {
+        setLoc('https', 'foo', '123');
+        expect(ufs.wsUrl('path')).toEqual('wss://foo:123/onos/ui/websock/path');
+    });
+
+    it('should allow us to define an alternate WS port', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.wsUrl('xyyzy', '456')).toEqual('ws://foo:456/onos/ui/websock/xyyzy');
+    });
+
+    it('should allow us to define an alternate host', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.wsUrl('core', '456', 'bar')).toEqual('ws://bar:456/onos/ui/websock/core');
+    });
+
+    it('should allow us to inject an app context', () => {
+        setLoc('http', 'foo', '123', '/my/app');
+        expect(ufs.wsUrl('path')).toEqual('ws://foo:123/my/app/onos/ui/websock/path');
+    });
+
+});
diff --git a/web/gui2-fw-lib/lib/remote/urlfn.service.ts b/web/gui2-fw-lib/lib/remote/urlfn.service.ts
new file mode 100644
index 0000000..a7576c0
--- /dev/null
+++ b/web/gui2-fw-lib/lib/remote/urlfn.service.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { Injectable, Inject } from '@angular/core';
+import { LogService } from '../log.service';
+
+const UICONTEXT = '/onos/ui/';
+const RSSUFFIX = UICONTEXT + 'rs/';
+const WSSUFFIX = UICONTEXT + 'websock/';
+
+/**
+ * ONOS GUI -- Remote -- General Purpose URL Functions
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class UrlFnService {
+    constructor(
+        private log: LogService,
+        @Inject('Window') private w: any
+    ) {
+        this.log.debug('UrlFnService constructed');
+    }
+
+    matchSecure(protocol: string): string {
+        const p: string = this.w.location.protocol;
+        const secure: boolean = (p === 'https' || p === 'wss');
+        return secure ? protocol + 's' : protocol;
+    }
+
+    /* A little bit of funky here. It is possible that ONOS sits
+     * behind a proxy and has an app prefix, e.g.
+     *      http://host:port/my/app/onos/ui...
+     * This bit of regex grabs everything after the host:port and
+     * before the UICONTEXT (/onos/ui/) and uses that as an app
+     * prefix by inserting it back into the WS URL.
+     * If no prefix, then no insert.
+     */
+    urlBase(protocol: string, port: string = '', host: string = ''): string {
+        const match = this.w.location.href.match('.*//[^/]+/(.+)' + UICONTEXT);
+        const appPrefix = match ? '/' + match[1] : '';
+
+        return this.matchSecure(protocol) +
+            '://' +
+            (host === '' ? this.w.location.hostname : host) +
+            ':' +
+            (port === '' ? this.w.location.port : port) +
+            appPrefix;
+    }
+
+    httpPrefix(suffix: string): string {
+        return this.urlBase('http') + suffix;
+    }
+
+    wsPrefix(suffix: string, wsport: string, host: string): string {
+        return this.urlBase('ws', wsport, host) + suffix;
+    }
+
+    rsUrl(path: string): string {
+        return this.httpPrefix(RSSUFFIX) + path;
+    }
+
+    wsUrl(path: string, wsport?: string, host?: string): string {
+        return this.wsPrefix(WSSUFFIX, wsport, host) + path;
+    }
+}
diff --git a/web/gui2-fw-lib/lib/remote/websocket.service.spec.ts b/web/gui2-fw-lib/lib/remote/websocket.service.spec.ts
new file mode 100644
index 0000000..d1bd5d8
--- /dev/null
+++ b/web/gui2-fw-lib/lib/remote/websocket.service.spec.ts
@@ -0,0 +1,278 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { WebSocketService, WsOptions, Callback, EventType } from './websocket.service';
+import { FnService } from '../util/fn.service';
+import { GlyphService } from '../svg/glyph.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { UrlFnService } from './urlfn.service';
+import { WSock } from './wsock.service';
+import { of } from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockGlyphService {}
+
+/**
+ * ONOS GUI -- Remote -- Web Socket Service - Unit Tests
+ */
+describe('WebSocketService', () => {
+    let wss: WebSocketService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const noop = () => ({});
+    const send = jasmine.createSpy('send')
+        .and.callFake((ev) => ev);
+    const mockWebSocket = {
+        send: send,
+        onmessage: (msgEvent) => ({}),
+        onopen: () => ({}),
+        onclose: () => ({}),
+    };
+
+    beforeEach(() => {
+        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({
+            providers: [WebSocketService,
+                { provide: FnService, useValue: fs },
+                { provide: LogService, useValue: logSpy },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: UrlFnService, useValue: new UrlFnService(logSpy, windowMock) },
+                { provide: 'Window', useFactory: (() => windowMock ) },
+                { provide: WSock, useFactory: (() => {
+                        return {
+                            newWebSocket: (() => mockWebSocket)
+                        };
+                    })
+                }
+            ]
+        });
+
+        wss = TestBed.get(WebSocketService);
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    it('should define WebSocketService', () => {
+        expect(wss).toBeDefined();
+    });
+
+    it('should define api functions', () => {
+        expect(fs.areFunctions(wss, ['bootstrap', 'error',
+            'handleOpen', 'handleMessage', 'handleClose',
+            'findGuiSuccessor', 'informListeners', 'send',
+            'noHandlersWarn', 'resetState',
+            'createWebSocket', 'bindHandlers', 'unbindHandlers',
+            'addOpenListener', 'removeOpenListener', 'sendEvent',
+            'setVeilDelegate', 'setLoadingDelegate', 'isConnected',
+             'closeWebSocket', 'isHandling'
+        ])).toBeTruthy();
+    });
+
+    it('should use the appropriate URL, createWebsocket', () => {
+        const url = wss.createWebSocket();
+        expect(url).toEqual('ws://foo:80/onos/ui/websock/core');
+    });
+
+    it('should use the appropriate URL with modified port, createWebsocket',
+        () => {
+            const url = wss.createWebSocket(<WsOptions>{ wsport: 1243 });
+            expect(url).toEqual('ws://foo:1243/onos/ui/websock/core');
+    });
+
+    it('should verify websocket event handlers, createWebsocket', () => {
+        wss.createWebSocket({ wsport: 1234 });
+        expect(fs.isF(mockWebSocket.onopen)).toBeTruthy();
+        expect(fs.isF(mockWebSocket.onmessage)).toBeTruthy();
+        expect(fs.isF(mockWebSocket.onclose)).toBeTruthy();
+    });
+
+    it('should invoke listener callbacks when websocket is up, handleOpen',
+        () => {
+            let num = 0;
+            function incrementNum(host: string, url: string) {
+                expect(host).toEqual('foo');
+                num++;
+            }
+            wss.addOpenListener(incrementNum);
+            wss.createWebSocket({ wsport: 1234 });
+
+            mockWebSocket.onopen();
+            expect(num).toBe(1);
+    });
+
+    xit('should send pending events, handleOpen', () => {
+        const fakeEvent = {
+            event: 'mockEv',
+            payload: { mock: 'thing' }
+        };
+        wss.sendEvent(fakeEvent.event, fakeEvent.payload);
+        // on opening the socket, a single authentication event should have
+        // been sent already...
+        expect(mockWebSocket.send.calls.count()).toEqual(1);
+
+        wss.createWebSocket({ wsport: 1234 });
+        mockWebSocket.onopen();
+        expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(fakeEvent));
+    });
+
+    it('should handle an incoming bad JSON message, handleMessage', () => {
+        const badMsg = {
+            data: 'bad message'
+        };
+        wss.createWebSocket({ wsport: 1234 });
+        expect(mockWebSocket.onmessage(badMsg)).toBeNull();
+        expect(logServiceSpy.error).toHaveBeenCalled();
+    });
+
+    it('should verify message was handled, handleMessage', () => {
+        let num = 0;
+        function fakeHandler(data1: Object) { num++; }
+        const data = JSON.stringify(<EventType>{
+            event: 'mockEvResp',
+            payload: {}
+        });
+        const event = {
+            data: data
+        };
+
+        wss.createWebSocket({ wsport: 1234 });
+        wss.bindHandlers(new Map<string, (data) => void>([
+            ['mockEvResp', (data2) => fakeHandler(data2)]
+        ]));
+        expect(mockWebSocket.onmessage(event)).toBe(undefined);
+        expect(num).toBe(1);
+    });
+
+    it('should warn if there is an unhandled event, handleMessage', () => {
+        const data = { foo: 'bar', bar: 'baz'};
+        const dataString = JSON.stringify(data);
+        const badEv = {
+                data: dataString
+            };
+        wss.createWebSocket({ wsport: 1234 });
+        mockWebSocket.onmessage(badEv);
+        expect(logServiceSpy.warn).toHaveBeenCalledWith('Unhandled event:', data);
+    });
+
+    it('should not warn if valid input, bindHandlers', () => {
+        expect(wss.bindHandlers(new Map<string, (data) => void>([
+            ['test', noop ],
+            ['bar', noop ]
+        ]))).toBe(undefined);
+
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+    });
+
+    it('should warn if no arguments, bindHandlers', () => {
+        expect(wss.bindHandlers(
+            new Map<string, (data) => void>([])
+        )).toBeNull();
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'WSS.bindHandlers(): no event handlers'
+        );
+    });
+
+    it('should warn if duplicate handlers were given, bindHandlers',
+        () => {
+            wss.bindHandlers(
+                new Map<string, (data) => void>([
+                    ['noop', noop ]
+                ])
+            );
+            expect(wss.bindHandlers(
+                new Map<string, (data) => void>([
+                    ['noop', noop ]
+                ])
+            )).toBe(undefined);
+            expect(logServiceSpy.warn).toHaveBeenCalledWith('duplicate bindings ignored:',
+                                                    ['noop']);
+    });
+
+    it('should warn if no arguments, unbindHandlers', () => {
+        expect(wss.unbindHandlers([])).toBeNull();
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'WSS.unbindHandlers(): no event handlers'
+        );
+    });
+    // Note: cannot test unbindHandlers' forEach due to it using closure variable
+
+    it('should not warn if valid argument, addOpenListener', () => {
+        let o = wss.addOpenListener(noop);
+        expect(o.id).toEqual(1);
+        expect(o.cb).toEqual(noop);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+        o = wss.addOpenListener(noop);
+        expect(o.id).toEqual(2);
+        expect(o.cb).toEqual(noop);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+    });
+
+    it('should log error if callback not a function, addOpenListener',
+        () => {
+            const o = wss.addOpenListener(null);
+            expect(o.id).toEqual(1);
+            expect(o.cb).toEqual(null);
+            expect(o.error).toEqual('No callback defined');
+            expect(logServiceSpy.error).toHaveBeenCalledWith(
+                'WSS.addOpenListener(): callback not a function'
+            );
+    });
+
+    it('should not warn if valid listener object, removeOpenListener', () => {
+        expect(wss.removeOpenListener(<Callback>{
+            id: 1,
+            error: 'error',
+            cb: noop
+        })).toBe(undefined);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+    });
+
+    it('should warn if listener is invalid, removeOpenListener', () => {
+        expect(wss.removeOpenListener(<Callback>{})).toBeNull();
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'WSS.removeOpenListener(): invalid listener', {}
+        );
+    });
+
+    // Note: handleClose is not currently tested due to all work it does relies
+    //       on closure variables that cannot be mocked
+
+});
diff --git a/web/gui2-fw-lib/lib/remote/websocket.service.ts b/web/gui2-fw-lib/lib/remote/websocket.service.ts
new file mode 100644
index 0000000..93020f0
--- /dev/null
+++ b/web/gui2-fw-lib/lib/remote/websocket.service.ts
@@ -0,0 +1,472 @@
+/*
+ * 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 { Injectable, Inject } from '@angular/core';
+import { FnService } from '../util/fn.service';
+import { GlyphService } from '../svg/glyph.service';
+import { LogService } from '../log.service';
+import { UrlFnService } from './urlfn.service';
+import { VeilComponent } from '../layer/veil/veil.component';
+import { WSock } from './wsock.service';
+
+/**
+ * Event Type structure for the WebSocketService
+ */
+export interface EventType {
+    event: string;
+    payload: Object;
+}
+
+export interface Callback {
+    id: number;
+    error: string;
+    cb(host: string, url: string): void;
+}
+
+interface ClusterNode {
+    id: string;
+    ip: string;
+    m_uiAttached: boolean;
+}
+
+interface Glyph {
+    id: string;
+    viewbox: string;
+    path: string;
+}
+
+interface Bootstrap {
+    user: string;
+    clusterNodes: ClusterNode[];
+    glyphs: Glyph[];
+}
+
+interface ErrorData {
+    message: string;
+}
+
+export interface WsOptions {
+    wsport: number;
+}
+
+/**
+ * ONOS GUI -- Remote -- Web Socket Service
+ *
+ * To see debug messages add ?debug=txrx to the URL
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class WebSocketService {
+    // internal state
+    private webSockOpts: WsOptions; // web socket options
+    private ws: WebSocket = null; // web socket reference
+    private wsUp: boolean = false; // web socket is good to go
+
+    // A map of event handler bindings - names and functions (that accept data and return void)
+    private handlers = new Map<string, (data: any) => void>([]);
+    private pendingEvents: EventType[] = []; // events TX'd while socket not up
+    private host: string; // web socket host
+    private url; // web socket URL
+    private clusterNodes: ClusterNode[] = []; // ONOS instances data for failover
+    private clusterIndex = -1; // the instance to which we are connected
+    private glyphs: Glyph[] = [];
+    private connectRetries: number = 0; // limit our attempts at reconnecting
+
+    // A map of registered Callbacks for websocket open()
+    private openListeners = new Map<number, Callback>([]);
+    private nextListenerId: number = 1; // internal ID for open listeners
+    private loggedInUser = null; // name of logged-in user
+    private lcd: any; // The loading component delegate
+    private vcd: any; // The veil component delegate
+
+    /**
+     * built-in handler for the 'boostrap' event
+     */
+    private bootstrap(data: Bootstrap) {
+        this.loggedInUser = data.user;
+        this.log.info('Websocket connection bootstraped', data);
+
+        this.clusterNodes = data.clusterNodes;
+        this.clusterNodes.forEach((d, i) => {
+            if (d.m_uiAttached) {
+                this.clusterIndex = i;
+                this.log.info('Connected to cluster node ' + d.ip);
+                // TODO: add connect info to masthead somewhere
+            }
+        });
+        this.glyphs = data.glyphs;
+        const glyphsMap = new Map<string, string>([]);
+        this.glyphs.forEach((d) => {
+            glyphsMap.set('_' + d.id, d.viewbox);
+            glyphsMap.set(d.id, d.path);
+            this.gs.registerGlyphs(glyphsMap);
+        });
+    }
+
+    private error(data: ErrorData) {
+        const m: string = data.message || 'error from server';
+        this.log.error(m, data);
+
+        // Unrecoverable error - throw up the veil...
+        if (this.vcd) {
+            this.vcd.show([
+                'Oops!',
+                'Server reports error...',
+                m,
+            ]);
+        }
+    }
+
+    constructor(
+        private fs: FnService,
+        private gs: GlyphService,
+        private log: LogService,
+        private ufs: UrlFnService,
+        private wsock: WSock,
+        @Inject('Window') private window: any
+    ) {
+        this.log.debug(window.location.hostname);
+
+        // Bind the boot strap event by default
+        this.bindHandlers(new Map<string, (data) => void>([
+            ['bootstrap', (data) => this.bootstrap(data)],
+            ['error', (data) => this.error(data)]
+        ]));
+
+        this.log.debug('WebSocketService constructed');
+    }
+
+
+    // ==========================
+    // === Web socket callbacks
+
+    /**
+     * Called when WebSocket has just opened
+     *
+     * Lift the Veil if it is displayed
+     * If there are any events pending, send them
+     * Mark the WSS as up and inform any listeners for this open event
+     */
+    handleOpen(): void {
+        this.log.info('Web socket open - ', this.url);
+        // Hide the veil
+        if (this.vcd) {
+            this.vcd.hide();
+        }
+
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug('Sending ' + this.pendingEvents.length + ' pending event(s)...');
+        }
+        this.pendingEvents.forEach((ev) => {
+            this.send(ev);
+        });
+        this.pendingEvents = [];
+
+        this.connectRetries = 0;
+        this.wsUp = true;
+        this.informListeners(this.host, this.url);
+    }
+
+    /**
+     * Function called when WebSocket send a message
+     */
+    handleMessage(msgEvent: MessageEvent): void {
+        let ev: EventType;
+        let h;
+        try {
+            ev = JSON.parse(msgEvent.data);
+        } catch (e) {
+            this.log.error('Message.data is not valid JSON', msgEvent.data, e);
+            return null;
+        }
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug(' << *Rx* ', ev.event, ev.payload);
+        }
+        h = this.handlers.get(ev.event);
+        if (h) {
+            try {
+                h(ev.payload);
+            } catch (e) {
+                this.log.error('Problem handling event:', ev, e);
+                return null;
+            }
+        } else {
+            this.log.warn('Unhandled event:', ev);
+        }
+    }
+
+    /**
+     * Called by the WebSocket if it is closed from the server end
+     *
+     * If the loading component is shown, call stop() on it
+     * Try to find another node in the cluster to connect to
+     * If this is not possible then show the Veil Component
+     */
+    handleClose(): void {
+        this.log.warn('Web socket closed');
+        if (this.lcd) {
+            this.lcd.stop();
+        }
+        this.wsUp = false;
+        let gsucc;
+
+        if (gsucc = this.findGuiSuccessor()) {
+            this.url = this.createWebSocket(this.webSockOpts, gsucc);
+        } else {
+            // If no controllers left to contact, show the Veil...
+            if (this.vcd) {
+                this.vcd.show([
+                    'Oops!',  // TODO: Localize this
+                    'Web-socket connection to server closed...',
+                    'Try refreshing the page.',
+                ]);
+            }
+        }
+    }
+
+    // ==============================
+    // === Private Helper Functions
+
+    /**
+     * Find the next node in the ONOS cluster.
+     *
+     * This is used if the WebSocket connection closes because a
+     * node in the cluster ges down - fail over should be automatic
+     */
+    findGuiSuccessor(): string {
+        const ncn = this.clusterNodes.length;
+        let ip: string;
+        let node;
+
+        while (this.connectRetries < ncn && !ip) {
+            this.connectRetries++;
+            this.clusterIndex = (this.clusterIndex + 1) % ncn;
+            node = this.clusterNodes[this.clusterIndex];
+            ip = node && node.ip;
+        }
+
+        return ip;
+    }
+
+    /**
+     * When the WebSocket is opened, inform any listeners that registered
+     * for that event
+     */
+    informListeners(host: string, url: string): void {
+        for (const [key, cb] of this.openListeners.entries()) {
+            cb.cb(host, url);
+        }
+    }
+
+    send(ev: EventType): void {
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug(' *Tx* >> ', ev.event, ev.payload);
+        }
+        this.ws.send(JSON.stringify(ev));
+    }
+
+    /**
+     * Check if there are no WSS event handlers left
+     */
+    noHandlersWarn(handlers: Map<string, Object>, caller: string): boolean {
+        if (!handlers || handlers.size === 0) {
+            this.log.warn('WSS.' + caller + '(): no event handlers');
+            return true;
+        }
+        return false;
+    }
+
+    /* ===================
+     * === API Functions
+     */
+
+    /**
+     * Required for unit tests to set to known state
+     */
+    resetState(): void {
+        this.webSockOpts = undefined;
+        this.ws = null;
+        this.wsUp = false;
+        this.host = undefined;
+        this.url = undefined;
+        this.pendingEvents = [];
+        this.handlers.clear();
+        this.clusterNodes = [];
+        this.clusterIndex = -1;
+        this.glyphs = [];
+        this.connectRetries = 0;
+        this.openListeners.clear();
+        this.nextListenerId = 1;
+
+    }
+
+    /*
+     * Currently supported opts:
+     *  wsport: web socket port (other than default 8181)
+     *  host:   if defined, is the host address to use
+     */
+    createWebSocket(opts?: WsOptions, host?: string) {
+        this.webSockOpts = opts; // preserved for future calls
+        this.host = host === undefined ? this.window.location.host : host;
+        this.url = this.ufs.wsUrl('core', opts === undefined ? '' : opts['wsport'].toString(), host);
+
+        this.log.debug('Attempting to open websocket to: ' + this.url);
+        this.ws = this.wsock.newWebSocket(this.url);
+        if (this.ws) {
+            // fat arrow => syntax means that the 'this' context passed will
+            // be of WebSocketService, not the WebSocket
+            this.ws.onopen = (() => this.handleOpen());
+            this.ws.onmessage = ((msgEvent) => this.handleMessage(msgEvent));
+            this.ws.onclose = (() => this.handleClose());
+            const authToken = this.window['onosAuth'];
+            this.log.debug('Auth Token for opening WebSocket', authToken);
+            this.sendEvent('authentication', { token: authToken });
+        }
+        // Note: Wsock logs an error if the new WebSocket call fails
+        return this.url;
+    }
+
+    /**
+     * Tell the WebSocket to close - this should call the handleClose() method
+     */
+    closeWebSocket() {
+        this.ws.close();
+    }
+
+
+    /**
+     * Binds the message handlers to their message type (event type) as
+     *  specified in the given map. Note that keys are the event IDs; values
+     *  are either:
+     *     * the event handler function, or
+     *     * an API object which has an event handler for the key
+     */
+    bindHandlers(handlerMap: Map<string, (data) => void>): void {
+        const dups: string[] = [];
+
+        if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
+            return null;
+        }
+        for (const [eventId, api] of handlerMap) {
+            this.log.debug('Adding handler for ', eventId);
+            const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
+            if (!fn) {
+                this.log.warn(eventId + ' handler not a function');
+                return;
+            }
+
+            if (this.handlers.get(eventId)) {
+                dups.push(eventId);
+            } else {
+                this.handlers.set(eventId, fn);
+            }
+        }
+        if (dups.length) {
+            this.log.warn('duplicate bindings ignored:', dups);
+        }
+    }
+
+    /**
+     * Unbinds the specified message handlers.
+     *   Expected that the same map will be used, but we only care about keys
+     */
+    unbindHandlers(handlerIds: string[]): void {
+        if ( handlerIds.length === 0 ) {
+            this.log.warn('WSS.unbindHandlers(): no event handlers');
+            return null;
+        }
+        for (const eventId of handlerIds) {
+            this.handlers.delete(eventId);
+        }
+    }
+
+    isHandling(handlerId: string): boolean {
+        return this.handlers.get(handlerId) !== undefined;
+    }
+
+    /**
+     * Add a listener function for listening for WebSocket opening.
+     * The function must give a host and url and return void
+     */
+    addOpenListener(callback: (host: string, url: string) => void ): Callback {
+        const id: number = this.nextListenerId++;
+        const cb = this.fs.isF(callback);
+        const o: Callback = <Callback>{ id: id, cb: cb };
+
+        if (cb) {
+            this.openListeners.set(id, o);
+        } else {
+            this.log.error('WSS.addOpenListener(): callback not a function');
+            o.error = 'No callback defined';
+        }
+        return o;
+    }
+
+    /**
+     * Remove a listener of WebSocket opening
+     */
+    removeOpenListener(lsnr: Callback): void {
+        const id = this.fs.isO(lsnr) && lsnr.id;
+        let o;
+
+        if (!id) {
+            this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
+            return null;
+        }
+        o = this.openListeners[id];
+
+        if (o) {
+            this.openListeners.delete(id);
+        }
+    }
+
+    /**
+     * Formulates an event message and sends it via the web-socket.
+     * If the websocket is not up yet, we store it in a pending list.
+     */
+    sendEvent(evType: string, payload: Object ): void {
+        const ev = <EventType> {
+            event: evType,
+            payload: payload
+        };
+
+        if (this.wsUp) {
+            this.send(ev);
+        } else {
+            this.pendingEvents.push(ev);
+        }
+    }
+
+    /**
+     * Binds the veil service as a delegate.
+     */
+    setVeilDelegate(vd: VeilComponent): void {
+        this.vcd = vd;
+    }
+
+    /**
+     * Binds the loading service as a delegate
+     */
+    setLoadingDelegate(ld: any): void {
+        // TODO - Investigate changing Loading Service to LoadingComponent
+        this.log.debug('Loading delegate set', ld);
+        this.lcd = ld;
+    }
+
+    isConnected(): boolean {
+        return this.wsUp;
+    }
+}
diff --git a/web/gui2-fw-lib/lib/remote/wsock.service.ts b/web/gui2-fw-lib/lib/remote/wsock.service.ts
new file mode 100644
index 0000000..40e0b2a
--- /dev/null
+++ b/web/gui2-fw-lib/lib/remote/wsock.service.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { LogService } from '../log.service';
+
+/**
+ * ONOS GUI -- Remote -- Web Socket Wrapper Service
+ *
+ * This service provided specifically so that it can be mocked in unit tests.
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class WSock {
+
+  constructor(
+    private log: LogService,
+  ) {
+    this.log.debug('WSockService constructed');
+  }
+
+
+  newWebSocket(url) {
+      let ws = null;
+      try {
+          ws = new WebSocket(url);
+      } catch (e) {
+          this.log.error('Unable to create web socket:', e);
+      }
+      return ws;
+  }
+
+}
diff --git a/web/gui2-fw-lib/lib/svg/glyph.service.spec.ts b/web/gui2-fw-lib/lib/svg/glyph.service.spec.ts
new file mode 100644
index 0000000..5b4669d
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/glyph.service.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { GlyphService } from './glyph.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+/**
+ * ONOS GUI -- SVG -- Glyph Service - Unit Tests
+ */
+describe('GlyphService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [GlyphService,
+                { provide: FnService, useClass: MockFnService },
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([GlyphService], (service: GlyphService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/svg/glyph.service.ts b/web/gui2-fw-lib/lib/svg/glyph.service.ts
new file mode 100644
index 0000000..20d5026
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/glyph.service.ts
@@ -0,0 +1,215 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+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.';
+const rg = 'registerGlyphs(): ';
+const rgs = 'registerGlyphSet(): ';
+
+/**
+ * ONOS GUI -- SVG -- Glyph Service
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class GlyphService {
+    // internal state
+    glyphs = d3.map();
+    api: Object;
+
+    constructor(
+        private fs: FnService,
+        //        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');
+    }
+
+    warn(msg: string): void {
+        this.log.warn(msgGS + msg);
+    }
+
+    addToMap(key, value, vbox, overwrite: boolean, dups) {
+        if (!overwrite && this.glyphs.get(key)) {
+            dups.push(key);
+        } else {
+            this.glyphs.set(key, { id: key, vb: vbox, d: value });
+        }
+    }
+
+    reportDups(dups: string[], which: string): boolean {
+        const ok: boolean = (dups.length === 0);
+        const msg = 'ID collision: ';
+
+        if (!ok) {
+            dups.forEach((id) => {
+                this.warn(which + msg + '"' + id + '"');
+            });
+        }
+        return ok;
+    }
+
+    reportMissVb(missing: string[], which: string): boolean {
+        const ok: boolean = (missing.length === 0);
+        const msg = 'Missing viewbox property: ';
+
+        if (!ok) {
+            missing.forEach((vbk) => {
+                this.warn(which + msg + '"' + vbk + '"');
+            });
+        }
+        return ok;
+    }
+
+    clear() {
+        // start with a fresh map
+        this.glyphs = d3.map();
+    }
+
+    init() {
+        this.log.info('Registering glyphs');
+        this.registerGlyphs(gds.logos);
+        this.registerGlyphSet(gds.glyphDataSet);
+        this.registerGlyphSet(gds.badgeDataSet);
+        this.registerGlyphs(gds.spriteData);
+        this.registerGlyphSet(gds.mojoDataSet);
+        this.registerGlyphs(gds.extraGlyphs);
+    }
+
+    registerGlyphs(data: Map<string, string>, overwrite: boolean = false): boolean {
+        const dups: string[] = [];
+        const missvb: string[] = [];
+        for (const [key, value] of data.entries()) {
+            const vbk = '_' + key;
+            const vb = data.get(vbk);
+
+            if (key[0] !== '_') {
+                if (!vb) {
+                    missvb.push(vbk);
+                    continue;
+                }
+                this.addToMap(key, value, vb, overwrite, dups);
+            }
+        }
+        return this.reportDups(dups, rg) && this.reportMissVb(missvb, rg);
+    }
+
+    registerGlyphSet(data: Map<string, string>, overwrite: boolean = false): boolean {
+        const dups: string[] = [];
+        const vb: string = data.get('_viewbox');
+
+        if (!vb) {
+            this.warn(rgs + 'no "_viewbox" property found');
+            return false;
+        }
+
+        for (const [key, value] of data.entries()) {
+            //        angular.forEach(data, function (value, key) {
+            if (key[0] !== '_') {
+                this.addToMap(key, value, vb, overwrite, dups);
+            }
+        }
+        return this.reportDups(dups, rgs);
+    }
+
+    ids() {
+        return this.glyphs.keys();
+    }
+
+    glyph(id) {
+        return this.glyphs.get(id);
+    }
+
+    glyphDefined(id) {
+        return this.glyphs.has(id);
+    }
+
+
+    /**
+     * Load definitions of a glyph
+     *
+     * Note: defs should be a D3 selection of a single <defs> element
+     */
+    loadDefs(defs, glyphIds: string[], noClear: boolean, asName?: string[]) {
+        const list = this.fs.isA(glyphIds) || this.ids();
+
+        if (!noClear) {
+            // remove all existing content
+            defs.html(null);
+        }
+
+        // load up the requested glyphs
+        list.forEach((id, idx) => {
+            const g = this.glyph(id);
+            let asNameStr: string = asName[idx];
+            if (!asNameStr) {
+                asNameStr = id;
+            }
+            if (g) {
+                if (noClear) {
+                    // quick exit if symbol is already present
+                    // TODO: check if this should be a continue or break instead
+                    if (defs.select('symbol#' + asNameStr).size() > 0) {
+                        return;
+                    }
+                }
+                defs.append('symbol')
+                    .attr('id', asNameStr)
+                    .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;
+        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) {
+            glyphUse.attr('transform', this.sus.translate(trans));
+        }
+
+        return glyphUse;
+    }
+}
diff --git a/web/gui2-fw-lib/lib/svg/glyphdata.service.spec.ts b/web/gui2-fw-lib/lib/svg/glyphdata.service.spec.ts
new file mode 100644
index 0000000..ab770d5
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/glyphdata.service.spec.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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '..//log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { GlyphDataService } from './glyphdata.service';
+
+/**
+ * ONOS GUI -- SVG -- Glyph Data Service - Unit Tests
+ */
+describe('GlyphDataService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [GlyphDataService,
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([GlyphDataService], (service: GlyphDataService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/svg/glyphdata.service.ts b/web/gui2-fw-lib/lib/svg/glyphdata.service.ts
new file mode 100644
index 0000000..8cb2551
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/glyphdata.service.ts
@@ -0,0 +1,1398 @@
+/*
+ *  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 { Injectable } from '@angular/core';
+import { LogService } from '../log.service';
+
+
+/**
+ * ONOS logo glyph
+ *
+ * TODO: Some major refactoring needs to go on here to make this more understandable
+ */
+export const logos = new Map<string, string>([
+    [ '_bird', '352 224 113 112'], // viewbox of bird - next is the bird path
+    [ 'bird', 'M427.7,300.4 c-6.9,0.6-13.1,5-19.2,7.1c-18.1,6.2-33.9,' +
+    '9.1-56.5,4.7c24.6,17.2,36.6,13,63.7,0.1c-0.5,0.6-0.7,1.3-1.3,' +
+    '1.9c1.4-0.4,2.4-1.7,3.4-2.2c-0.4,0.7-0.9,1.5-1.4,1.9c2.2-0.6,' +
+    '3.7-2.3,5.9-3.9c-2.4,2.1-4.2,5-6,8c-1.5,2.5-3.1,4.8-5.1,6.9c-1,' +
+    '1-1.9,1.9-2.9,2.9c-1.4,1.3-2.9,2.5-5.1,2.9c1.7,0.1,3.6-0.3,6.5' +
+    '-1.9c-1.6,2.4-7.1,6.2-9.9,7.2c10.5-2.6,19.2-15.9,25.7-18c18.3' +
+    '-5.9,13.8-3.4,27-14.2c1.6-1.3,3-1,5.1-0.8c1.1,0.1,2.1,0.3,3.2,' +
+    '0.5c0.8,0.2,1.4,0.4,2.2,0.8l1.8,0.9c-1.9-4.5-2.3-4.1-5.9-6c-2.3' +
+    '-1.3-3.3-3.8-6.2-4.9c-7.1-2.6-11.9,11.7-11.7-5c0.1-8,4.2-14.4,' +
+    '6.4-22c1.1-3.8,2.3-7.6,2.4-11.5c0.1-2.3,0-4.7-0.4-7c-2-11.2-8.4' +
+    '-21.5-19.7-24.8c-1-0.3-1.1-0.3-0.9,0c9.6,17.1,7.2,38.3,3.1,54.2' +
+    'C429.9,285.5,426.7,293.2,427.7,300.4z'],
+    [ '_cord', '0 0 110 110'],
+    [ 'cord', 'M92.5,62.3l-33,33,2.5,2.5c4.1,4.1,7.4,3.6,11.2-.1L95.9,75' +
+    'l-4.5-4.5,4.7-4.7-3.6-3.6Z' +
+    'm2.6,7L98.4,66l3.3,3.3-3.3,3.3-3.3-3.3Z' +
+    'M94.5,60l4.9-4.9,4.9,4.9-4.9,4.9Z' +
+    'M36.2,36.1L18.6,53.8c-7.8,7.8-5.8,17.4-2.4,22l-2.2-2.2' +
+    'c-10.6-10.6-11.2-20,0-31.2L28.2,28.1L31.3,25l8,8-3.1,3.1Z' +
+    'M55.5,55.4l3.6-3.6L66.9,44l-8-8l-2.5,2.5-5.2,5.2l-3.6,3.6' +
+    'L33.2,61.6C22,72.7,22.5,82.2,33.2,92.8L35.4,95' +
+    'c-3.4-4.5-5.4-14.1,2.4-22L55.5,55.4Z' +
+    'M50.7,21.7l-8-8L35,21.2l8,8,7.6-7.6Z' +
+    'M62.8,9.6L55.4,17l-8-8,7.4-7.4,8,8Z' +
+    'm0.7,18.3-7.6,7.6-8-8,7.6-7.6,8,8Z' +
+    'm26.1-6.6-8.1,8.1-8-8,8.1-8.1,8,8Z' +
+    'M79.3,31.5l-7.4,7.4-8-8,7.4-7.4,8,8Z' +
+    'M45.7,45.6L54.3,37l-8-8-8.6,8.6L23.4,51.8' +
+    'C12.2,63,12.8,72.4,23.4,83l2.2,2.2c-3.4-4.5-5.4-14.1,2.4-22Z' +
+    'M34.9,80.7l20.6,20.5c2,2,4.6,4.1,7.9,3.2-2.9,2.9-8.9,1.7-11.9-1.3' +
+    'L35.1,86.8,35,86.6H34.9l-0.8-.8' +
+    'a15,15,0,0,1,.1-1.9,14.7,14.7,0,0,1,.7-3.2Z' +
+    'm-0.6,7.4a21.3,21.3,0,0,0,5.9,11.7l5.7,5.7' +
+    'c3,3,9,4.1,11.9,1.3-3.3.9-5.9-1.2-7.9-3.2L34.3,88.1Z' +
+    'm3.5-12.4a16.6,16.6,0,0,0-2.3,3.6L57,100.8' +
+    'c3,3,9,4.1,11.9,1.3-3.3.9-5.9-1.2-7.9-3.2Z'],
+]);
+
+
+// --- Core glyphs ------------------------------------
+
+// NOTE: when adding glyphs, please also update GlyphConstants class.
+const tableFrame = 'M6.3,5.3h8.5v14.2h-8.5z' +
+        'M95.3,5.3h8.5v14.2h-8.5z' +
+        'M18.5,5.3h15.6v14.2h-15.6z' +
+        'M37.9,5.3h15.6v14.2h-15.6z' +
+        'M57,5.3h15.6v14.2h-15.6z' +
+        'M76.1,5.3h15.6v14.2h-15.6z' +
+        'M6.3,23.9h97.5v80.75h-97.5z';
+
+const rSquare = 'M10,20a10,10,0,0,1,10-10h70a10,10,0,0,1,10,10v70a10,10,' +
+        '0,0,1-10,10h-70a10,10,0,0,1-10-10z';
+
+const octagon = 'M10,35l25-25h40l25,25v40l-25,25h-40l-25-25z';
+
+const circle = 'M10,55A45,45,0,0,1,100,55A45,45,0,0,1,10,55';
+
+const arrowsLR = 'M58,26l12,0,0-8,18,13-18,13,0-8-12,0z' +
+        'M58,60l12,0,0-8,18,13-18,13,0-8-12,0z' +
+        'M52,40l-12,0,0-8-18,13,18,13,0-8,12,0z' +
+        'M52,74l-12,0,0-8-18,13,18,13,0-8,12,0z';
+
+const arrowsInHOutV = 'M20,50l12,0,0-8,18,13-18,13,0-8-12,0z' +
+        'M90,50l-12,0,0-8-18,13,18,13,0-8,12,0z' +
+        'M50,47l0-12-8,0,13-18,13,18-8,0,0,12z' +
+        'M50,63l0,12-8,0,13,18,13-18-8,0,0-12z';
+
+const laser = 'M47.2,68.4L31.1,84.5,25.6,79,41.7,62.9Z' +
+        'M76.3,30.6A3.4,3.4,0,0,0,72.9,34a3.3,3.3,0,0,0,.3,1.3L44.7,63.7' +
+        'l1.7,1.7L74.8,37a3.2,3.2,0,0,0,1.5.4A3.4,3.4,0,1,0,76.3,30.6Z' +
+        'm0.9-2.9V23.8a0.8,0.8,0,0,0-.8-0.8H76.1a0.8,0.8,0,0,0-.8.8v3.9' +
+        'a0.8,0.8,0,0,0,.8.8h0.3A0.8,0.8,0,0,0,77.2,27.7Z' +
+        'm3.5,3.2,3.6-3.6a0.9,0.9,0,0,0,0-1.2H84.3a0.9,0.9,0,0,0-1.2,0' +
+        'l-3.6,3.6a0.9,0.9,0,0,0,0,1.2h0.1A0.9,0.9,0,0,0,80.7,30.9Z' +
+        'm1.8,4h3.9a0.8,0.8,0,0,0,.8-0.8V33.7a0.8,0.8,0,0,0-.8-0.8H82.6' +
+        'a0.8,0.8,0,0,0-.8.8V34A0.8,0.8,0,0,0,82.6,34.8Z' +
+        'm-16.3.1h3.9a0.8,0.8,0,0,0,.8-0.8V33.8a0.8,0.8,0,0,0-.8-0.8' +
+        'H66.2a0.8,0.8,0,0,0-.8.8v0.3A0.8,0.8,0,0,0,66.2,34.9Z' +
+        'm6.8-5.2-3.8-3.8a0.9,0.9,0,0,0-1.3,0a0.9,0.9,0,0,0,0,1.3' +
+        'L71.8,31A0.9,0.9,0,0,0,73,31A0.9,0.9,0,0,0,73.1,29.7Z' +
+        'M84.8,40.9L80.9,37a0.9,0.9,0,0,0-1.3,0a0.9,0.9,0,0,0,0,1.3' +
+        'l3.9,3.9a0.9,0.9,0,0,0,1.3,0A0.9,0.9,0,0,0,84.8,40.9Z' +
+        'm-7.6,3.2V40.2a0.8,0.8,0,0,0-.8-0.8H76.2a0.8,0.8,0,0,0-.8.8' +
+        'v3.9a0.8,0.8,0,0,0,.8.8h0.3A0.8,0.8,0,0,0,77.3,44.1Z';
+
+const fiberStar = 'M89,60V57H70.6a15,15,0,0,1-3.2,7.6l13,12.9L82.8,75v7.5' +
+        'H75.2l2.2-2.2-12.8-13A14.9,14.9,0,0,1,57,70.6V89h3.1l-5.3,5.4' +
+        'L49.4,89H53V70.6a13.2,13.2,0,0,1-8-3.2l-13.1,13,2.3,2.3H26.5' +
+        'V75.1l2.3,2.3,13-12.8A15,15,0,0,1,38.7,57H21v3l-5.4-5.4L21,49.3' +
+        'V53H38.7a13.1,13.1,0,0,1,3.2-8l-13-13.1-2.2,2.1V26.4h7.5l-2.4,2.4' +
+        'L45,41.8a13.2,13.2,0,0,1,8-3.2V21H49.4l5.4-5.4L60.1,21H57V38.6' +
+        'a14.9,14.9,0,0,1,7.6,3.2l12.9-13-2.4-2.3h7.5v7.6l-2.3-2.3L67.4,45' +
+        'a13.1,13.1,0,0,1,3.2,8H89V49.3l5.4,5.3Z';
+
+export const glyphDataSet = new Map<string, string>([
+    ['_viewbox', '0 0 110 110'],
+
+    ['uiAttached', 'M91.9,16.7H18.1A5.3,5.3,0,0,0,12.8,22V68' +
+    'a5.3,5.3,0,0,0,5.3,5.3H91.9A5.3,5.3,0,0,0,97.2,68V22' +
+    'A5.3,5.3,0,0,0,91.9,16.7ZM91.6,65.2H18.4V22.3H91.6V65.2Z' +
+    'M71.5,87.5h3.8v5.9h-40.6v-5.9h3.8v-1.7h5.4v-9.7h22.3v9.7h5.3v1.7z'],
+
+    // Small dot
+    ['unknown', 'M35,40a5,5,0,0,1,5-5h30a5,5,0,0,1,5,5v30a5,5,0,0,1-5,5' +
+    'h-30a5,5,0,0,1-5-5z'],
+
+    // Question mark for unknown device types
+    ['query', 'M51.4,69.9c0-0.9,0-1.6,0-2.1c0-2.7,0.4-5.1,1.2-7.1' +
+    'c0.6-1.5,1.5-3,2.8-4.5c0.9-1.1,2.6-2.7,5.1-4.8c2.4-2.1,4-3.8,' +
+    '4.8-5.1 c0.7-1.3,1.1-2.6,1.1-4.1c0-2.7-1.1-5.1-3.2-7.1c-2.1-2' +
+    '-4.8-3.1-7.9-3.1c-3,0-5.5,0.9-7.5,2.8c-2,1.9-3.3,4.8-4,8.7l-7.2' +
+    '-0.8 c0.7-5.3,2.6-9.3,5.8-12.1c3.2-2.8,7.5-4.2,12.8-4.2c5.6,0,' +
+    '10.1,1.5,13.4,4.5c3.3,3,5,6.7,5,10.9c0,2.5-0.6,4.8-1.8,6.8 ' +
+    's-3.5,4.6-6.9,7.6c-2.3,2-3.8,3.5-4.5,4.4c-0.7,1-1.2,2-1.6,3.3' +
+    'c-0.3,1.2-0.5,3.2-0.6,6H51.4z M51,83.8v-7.9h8v7.9H51z'],
+
+
+    // --- ONOS cluster node ---
+    ['node', 'M15,100a5,5,0,0,1-5-5v-65a5,5,0,0,1,5-5h80a5,5,0,0,1,5,5' +
+    'v65a5,5,0,0,1-5,5zM14,22.5l11-11a10,3,0,0,1,10-2h40a10,3,0,0,1,' +
+    '10,2l11,11zM16,35a5,5,0,0,1,10,0a5,5,0,0,1-10,0z'],
+
+
+    // --- DEVICES ---
+    // See Device.DeviceType enum for the following...
+
+    // NOTE: 'other' should map to 'unknown' (.) above
+
+    // deprecated glyphs -- using Mojo Designs below -- m_*
+    ['switch', rSquare + arrowsLR],
+
+    ['router', circle + arrowsInHOutV],
+
+    ['roadm', octagon + arrowsLR],
+
+    ['otn', rSquare + laser],
+
+    ['roadm_otn', octagon + laser],
+
+    ['fiber_switch', rSquare + fiberStar],
+
+    ['microwave', 'M85,71.2c-8.9,10.5-29.6,8.7-45.3-3.5C23.9,55.4,19.8,' +
+    '37,28.6,26.5C29.9,38.6,71.5,69.9,85,71.2z M92.7,76.2M16.2,15 ' +
+    'M69.5,100.7v-4c0-1.4-1.2-2.2-2.6-2.2H19.3c-1.4,0-2.8,0.7-2.8,2.2' +
+    'v3.9c0,0.7,0.8,1,1.5,1h50.3C69,101.5,69.5,101.3,69.5,100.7z ' +
+    'M77.3,7.5l0,3.7c9,0.1,16.3,7.1,16.2,15.7l3.9,0C97.5,16.3,88.5,' +
+    '7.6,77.3,7.5z M77.6,14.7l0,2.5c5.3,0,9.7,4.2,9.6,9.3l2.6,0C89.9' +
+    ',20,84.4,14.7,77.6,14.7z M82.3,22.2c-1.3-1.2-2.9-1.9-4.7-1.9' +
+    'l0,1.2c1.4,0,2.8,0.6,3.8,1.5c1,1,1.6,2.3,1.6,3.7l1.3,0C84.3,25.1,' +
+    '83.6,23.4,82.3,22.2z M38.9,69.5l-5.1,23h16.5l-2.5-17.2C44.1,73.3,' +
+    '38.9,69.5,38.9,69.5zM58.1,54.1c13.7,10.1,26.5,16.8,29.2,13.7' +
+    'c2.7-3.1-5.6-13-19.3-24.4 M62.9,34.2 M62,37.9C47.7,27.3,33.7,20,' +
+    '31,23.1c-2.7,3.2,7,14.2,20.6,26 M73.9,25.7c-2.9,0.1-5.2,2.3-5.1,' +
+    '4.8c0,0.7,0.2,1.4,0.6,2l0,0L53.8,49.7l3.3,2.5L72.7,35l-0.4-0.3' +
+    'c0.6,0.2,1.3,0.3,1.9,0.3c2.9-0.1,5.2-2.3,5.1-4.9C79.3,27.6,76.8,' +
+    '25.6,73.9,25.7z'],
+
+    // NOTE: 'unrecognized' should map to 'query' (?) above
+
+
+    // --- HOSTS ---
+
+    // default glyph for a host
+    ['endstation', 'M10,15a5,5,0,0,1,5-5h65a5,5,0,0,1,5,5v80a5,5,0,0,1' +
+    '-5,5h-65a5,5,0,0,1-5-5zM87.5,14l11,11a3,10,0,0,1,2,10v40a3,10,' +
+    '0,0,1,-2,10l-11,11zM17,19a2,2,0,0,1,2-2h56a2,2,0,0,1,2,2v26a2,' +
+    '2,0,0,1-2,2h-56a2,2,0,0,1-2-2zM20,20h54v10h-54zM20,33h54v10h' +
+    '-54zM42,70a5,5,0,0,1,10,0a5,5,0,0,1-10,0z'],
+
+    ['bgpSpeaker', 'M10,40a45,35,0,0,1,90,0Q100,77,55,100Q10,77,10,40z' +
+    'M50,29l12,0,0-8,18,13-18,13,0-8-12,0zM60,57l-12,0,0-8-18,13,' +
+    '18,13,0-8,12,0z'],
+
+
+    // --- Miscellaneous glyphs ---------------------------------
+
+    ['chain', 'M60.4,77.6c-4.9,5.2-9.6,11.3-15.3,16.3c-8.6,7.5-20.4,6.8' +
+    '-28-0.8c-7.7-7.7-8.4-19.6-0.8-28.4c6.5-7.4,13.5-14.4,20.9-20.9' +
+    'c7.5-6.7,19.2-6.7,26.5-0.8c3.5,2.8,4.4,6.1,2.2,8.7c-2.7,3.1' +
+    '-5.5,2.5-8.5,0.3c-4.7-3.4-9.7-3.2-14,0.9C37.1,58.7,31,64.8,' +
+    '25.2,71c-4.2,4.4-4.2,10.6-0.6,14.3c3.7,3.7,9.7,3.7,14.3-0.4' +
+    'c2.9-2.5,5.3-5.5,8.3-8c1-0.9,3-1.1,4.4-0.9C54.8,76.3,57.9,77.1,' +
+    '60.4,77.6zM49.2,32.2c5-5.2,9.7-10.9,15.2-15.7c12.8-11,31.2' +
+    '-4.9,34.3,11.2C100,34.2,98.3,40.2,94,45c-6.7,7.4-13.7,14.6' +
+    '-21.2,21.2C65.1,73,53.2,72.7,46,66.5c-3.2-2.8-3.9-5.8-1.6-8.4' +
+    'c2.6-2.9,5.3-2.4,8.2-0.3c5.2,3.7,10,3.3,14.7-1.1c5.8-5.6,11.6' +
+    '-11.3,17.2-17.2c4.6-4.8,4.9-11.1,0.9-15c-3.9-3.9-10.1-3.4-15,' +
+    '1.2c-3.1,2.9-5.7,7.4-9.3,8.5C57.6,35.3,53,33,49.2,32.2z'],
+
+    ['crown', 'M99.5,21.6c0,3-2.3,5.4-5.1,5.4c-0.3,0-0.7,0-1-0.1c-1.8,' +
+    '4-4.8,10-7.2,17.3c-3.4,10.6-0.9,26.2,2.7,27.3C90.4,72,91.3,' +
+    '75,88,75H22.7c-3.3,0-2.4-3-0.9-3.5c3.6-1.1,6.1-16.7,2.7-27.3' +
+    'c-2.4-7.4-5.4-13.5-7.2-17.5c-0.5,0.2-1,0.3-1.6,0.3c-2.8,0' +
+    '-5.1-2.4-5.1-5.4c0-3,2.3-5.4,5.1-5.4c2.8,0,5.1,2.4,5.1,5.4c0,' +
+    '1-0.2,1.9-0.7,2.7c0.7,0.8,1.4,1.6,2.4,2.6c8.8,8.9,11.9,12.7,' +
+    '18.1,11.7c6.5-1,11-8.2,13.3-14.1c-2-0.8-3.3-2.7-3.3-5.1c0-3,' +
+    '2.3-5.4,5.1-5.4c2.8,0,5.1,2.4,5.1,5.4c0,2.5-1.6,4.5-3.7,5.2' +
+    'c2.3,5.9,6.8,13,13.2,14c6.2,1,9.3-2.7,18.1-11.7c0.7-0.7,1.4' +
+    '-1.5,2-2.1c-0.6-0.9-1-2-1-3.1c0-3,2.3-5.4,5.1-5.4C97.2,16.2,' +
+    '99.5,18.6,99.5,21.6zM92,87.9c0,2.2-1.7,4.1-3.8,4.1H22.4c' +
+    '-2.1,0-4.4-1.9-4.4-4.1v-3.3c0-2.2,2.3-4.5,4.4-4.5h65.8c2.1,' +
+    '0,3.8,2.3,3.8,4.5V87.9z'],
+
+    ['lock', 'M79.4,48.6h-2.7c0.2-5.7-0.2-20.4-7.9-28.8c-3.6-3.9-8.3' +
+    '-5.9-13.7-5.9c-5.4,0-10.2,2-13.8,5.9c-7.8,8.4-8.3,23.2-8.1,28.8' +
+    'h-2.7c-4.4,0-8,2.6-8,5.9v35.7c0,3.3,3.6,5.9,8,5.9h48.9c4.4,0,' +
+    '8-2.6,8-5.9V54.5C87.5,51.3,83.9,48.6,79.4,48.6z M48.1,26.1c1.9' +
+    '-2,4.1-2.9,7-2.9c2.9,0,5.1,0.9,6.9,2.9c5,5.4,5.6,17.1,5.4,22.6' +
+    'h-25C42.3,43.1,43.1,31.5,48.1,26.1z'],
+
+    ['topo', 'M97.2,76.3H86.6l-7.7-21.9H82c1,0,1.9-0.8,1.9-1.9V35.7c' +
+    '0-1-0.8-1.9-1.9-1.9H65.2c-1,0-1.9,0.8-1.9,1.9v2.6L33.4,26.1v-11' +
+    'c0-1-0.8-1.9-1.9-1.9H14.7c-1,0-1.9,0.8-1.9,1.9v16.8c0,1,0.8,' +
+    '1.9,1.9,1.9h16.8c1,0,1.9-0.8,1.9-1.9v-2.6l29.9,12.2v9L30.5,76.9' +
+    'c-0.3-0.3-0.8-0.5-1.3-0.5H12.4c-1,0-1.9,0.8-1.9,1.9V95c0,1,0.8,' +
+    '1.9,1.9,1.9h16.8c1,0,1.9-0.8,1.9-1.9v-6.9h47.4V95c0,1,0.8,1.9,' +
+    '1.9,1.9h16.8c1,0,1.9-0.8,1.9-1.9V78.2C99.1,77.2,98.2,76.3,97.2,' +
+    '76.3z M31.1,85.1v-4.9l32.8-26.4c0.3,0.3,0.8,0.5,1.3,0.5h10.5l' +
+    '7.7,21.9h-3c-1,0-1.9,0.8-1.9,1.9v6.9H31.1z'],
+
+    ['refresh',
+   'M99.7,53.8l-10,13.3L85,73.5,78,64,70.4,53.7h9' +
+   'A28.5,28.5,0,1,0,68.3,77.6l10.6,6.9A40.7,40.7,0,1,1,91.6,53.8h8.2Z'],
+
+    ['garbage', 'M55.1,31.1c9.4,0,18.7.1,28.1-.1,3.2-.1,4.2,1,3.7,4.1' +
+    'q-4.1,28.6-8,57.3c-0.3,2.3-1.3,3.4-3.5,3.4h-41' +
+    'c-2.2,0-2.9-1.2-3.2-3.2Q27,63.5,22.7,34.4c-0.4-2.8.6-3.4,3.1-3.4' +
+    'H55.1Z' +
+    'M44.3,81.9c0.4-1.1-2.5-27.4-3.8-40.5a3.2,3.2,0,0,0-3.7-3.2' +
+    'c-2.5.1-2.5,1.9-2.4,3.7,0.5,4.9,1,9.8,1.5,14.7,0.8,8.1,1.6,16.2,' +
+    '2.4,24.2,0.2,2.2,1.1,4.1,3.6,3.4A3.6,3.6,0,0,0,44.3,81.9Z' +
+    'm21.2,0a2.8,2.8,0,0,0,2.2,2.3' +
+    'c2.3,0.8,3.7-.7,4-3.1,1.3-12.9,2.6-25.7,3.8-38.6,0.2-2,.3-4.1-2.6-4.4' +
+    's-3.3,1.7-3.5,3.7C68.1,54.8,65.5,81.1,65.5,81.9Z' +
+    'M57.9,61.3q0-9.8,0-19.6c0-2.2-.8-3.6-3.2-3.5s-2.6,1.7-2.6,3.6' +
+    'q0,19.4,0,38.8c0,1.9,0,3.8,2.8,3.9s3-1.8,3-3.9Q57.9,70.9,57.9,61.3Z' +
+    'M19,24.7c0.3-2,.5-5.7,1.5-8a5.1,5.1,0,0,1,3.6-2.3' +
+    'c5.5-.5,17.3-0.8,17.3-0.8l4.3-3.3H62.9l5.6,3.5S84.5,14.6,87,15' +
+    's2.5,0.7,3.2,1.9,0.9,7.4.9,7.4Z'],
+
+    ['cog', 'M100.2,46.4L87.1,44.8l-2.1-5L93.1,29a2.3,2.3,0,0,0-.2-3' +
+    'l-8.7-8.8a2.4,2.4,0,0,0-3.1-.2l-11,7.9L66,23.1,63.1,9.5' +
+    'A2.1,2.1,0,0,0,60.8,8H49.3A2.1,2.1,0,0,0,47,9.5L44.2,22.7l-5,2.2' +
+    'L28.8,16.8a2.3,2.3,0,0,0-3.1.2l-9.2,9.2a2.4,2.4,0,0,0-.2,3.2' +
+    'l8.1,10.4-1.7,4.1L9.8,46.4A2.3,2.3,0,0,0,8,48.7V61.9' +
+    'a2.3,2.3,0,0,0,2,2.4L22.6,66l1.7,5.2-7.7,10a2.4,2.4,0,0,0,.2,3.2' +
+    'l9.1,9a2.4,2.4,0,0,0,3.3.1l9-8.2,4.8,2.2,2.6,12.7' +
+    'a2.3,2.3,0,0,0,2.4,1.9l13.9-.2a2.5,2.5,0,0,0,2.3-2.4' +
+    'l0.7-11.4,5.5-2.3,9.8,8.1a2.4,2.4,0,0,0,3.2-.1L93,83.9' +
+    'a2.4,2.4,0,0,0,.1-3.3L84.7,71,87,66l13.2-2.5a2.3,2.3,0,0,0,1.9-2.3' +
+    'l0.2-12.5A2.4,2.4,0,0,0,100.2,46.4ZM54.4,73' +
+    'A18.2,18.2,0,1,1,72.6,54.8,18.2,18.2,0,0,1,54.4,73Z'],
+
+    ['delta', 'M55,19.2L13.7,90.8h82.7L55,19.2z ' +
+    'M55,31.2l30.9,53.5H24.1L55,31.2z'],
+
+    ['nonzero', 'M76.7,25.1l7.8-13.5l-7.6-0.3l-5.7,9.9' +
+    'c-4.8-2.9-10.4-4.5-16.2-4.5c-19.1,0-34.7,17.2-34.7,38.4' +
+    'c0,12.5,5.4,23.6,13.7,30.6l-7.6,13.2l7.6,0.1l5.5-9.6' +
+    'c4.7,2.6,9.9,4,15.5,4c19.1,0,34.7-17.2,34.7-38.4' +
+    'C89.7,42.9,84.6,32.1,76.7,25.1z M27.9,55C27.9,38.4,40,25,55,25' +
+    'c4.4,0,8.5,1.2,12.2,3.2l-29,50.3C31.9,73,27.9,64.5,27.9,55z' +
+    'M55,85c-4.1,0-7.9-1-11.4-2.8l29-50.1c5.8,5.5,9.5,13.7,9.5,22.8' +
+    'C82.1,71.6,70,85,55,85z'],
+
+    ['download', 'M90.3,94.5H19.7V79.2H90.3V94.5Z' +
+    'm-49.1-79V44H26.2L55,72.3,83.8,44H68.9V15.5H41.1Z'],
+
+    ['upload', 'M90.3,79.4H19.7V94.6H90.3V79.4Z' +
+    'M41.1,71.8V43.5H26.2L55,15.4,83.8,43.5H68.9V71.8H41.1Z'],
+
+    // --- Navigation glyphs ------------------------------------
+
+    ['flowTable', tableFrame +
+    'M86,63.2c0,3.3-2.7,6-6,6c-2.8,0-5.1-1.9-5.8-' +
+    '4.5H63.3v5.1c0,0.9-0.7,1.5-1.5,1.5h-5.2v10.6c2.6,0.7,4.5,3,4.5,' +
+    '5.8c0,3.3-2.7,6-6,6c-3.3,0-6-2.7-6-6c0-2.8,1.9-5.1,4.4-5.8V71.3' +
+    'H48c-0.9,0-1.5-0.7-1.5-1.5v-5.1H36c-0.7,2.6-3,4.4-5.8,4.4c-3.3,' +
+    '0-6-2.7-6-6s2.7-6,6-6c2.8,0,5.2,1.9,5.8,4.5h10.5V56c0-0.9,0.7-' +
+    '1.5,1.5-1.5h5.5V43.8c-2.6-0.7-4.5-3-4.5-5.8c0-3.3,2.7-6,6-6s6,' +
+    '2.7,6,6c0,2.8-1.9,5.1-4.5,5.8v10.6h5.2c0.9,0,1.5,0.7,1.5,1.5v' +
+    '5.6h10.8c0.7-2.6,3-4.5,5.8-4.5C83.3,57.1,86,59.8,86,63.2z ' +
+    'M55.1,42.3c2.3,0,4.3-1.9,4.3-4.3c0-2.3-1.9-4.3-4.3-4.3' +
+    's-4.3,1.9-4.3,4.3C50.8,40.4,52.7,42.3,55.1,42.3z ' +
+    'M34.4,63.1c0-2.3-1.9-4.3-4.3-4.3s-4.3,1.9-4.3,4.3' +
+    's1.9,4.3,4.3,4.3S34.4,65.5,34.4,63.1z ' +
+    'M55.1,83.5c-2.3,0-4.3,1.9-4.3,4.3s1.9,4.3,4.3,4.3' +
+    's4.3-1.9,4.3-4.3S57.5,83.5,55.1,83.5z' +
+    'M84.2,63.2c0-2.3-1.9-4.3-4.3-4.3s-4.3,' +
+    '1.9-4.3,4.3s1.9,4.3,4.3,4.3S84.2,65.5,84.2,63.2z'],
+
+    ['portTable', tableFrame +
+    'M85.5,37.7c0-0.7-0.4-1.3-0.9-1.3H26.2c-0.5,0-' +
+    '0.9,0.6-0.9,1.3v34.6c0,0.7,0.4,1.3,0.9,1.3h11v9.6c0,1.1,0.5,2,' +
+    '1.2,2h9.1c0,0.2,0,0.3,0,0.5v3c0,1.1,0.5,2,1.2,2h13.5c0.6,0,1.2-' +
+    '0.9,1.2-2v-3c0-0.2,0-0.3,0-0.5h9.1c0.6,0,1.2-0.9,1.2-2v-9.6h11' +
+    'c0.5,0,0.9-0.6,0.9-1.3V37.7z M30.2,40h-1v8h1V40zM75.2,40h-2.1v8' +
+    'h2.1V40z M67.7,40h-2.1v8h2.1V40z M60.2,40h-2.1v8h2.1V40z M52.7,' +
+    '40h-2.1v8h2.1V40z M45.2,40h-2.1v8h2.1V40zM37.7,40h-2.1v8h2.1V40' +
+    'z M81.6,40h-1v8h1V40z'],
+
+    ['groupTable', tableFrame +
+    'M45.7,52.7c0.2-5.6,2.6-10.7,6.2-14.4c-2.6-1.5-5.7-2.5-8.9-2.5' +
+    'c-9.8,0-17.7,7.9-17.7,17.7c0,6.3,3.3,11.9,8.3,15' +
+    'C34.8,61.5,39.4,55.6,45.7,52.7z ' +
+    'M51.9,68.8c-3.1-3.1-5.2-7.2-6-11.7c-4.7,2.8-7.9,7.6-8.6,13.2' +
+    'c1.8,0.6,3.6,0.9,5.6,0.9C46.2,71.2,49.3,70.3,51.9,68.8z ' +
+    'M55.2,71.5c-3.5,2.4-7.7,3.7-12.2,3.7c-1.9,0-3.8-0.3-5.6-0.7' +
+    'C38.5,83.2,45.9,90,54.9,90c9,0,16.4-6.7,17.5-15.4' +
+    'c-1.6,0.4-3.4,0.6-5.1,0.6C62.8,75.2,58.6,73.8,55.2,71.5z ' +
+    'M54.9,50.6c1.9,0,3.8,0.3,5.6,0.7c-0.5-4.1-2.5-7.9-5.4-10.6' +
+    'c-2.9,2.7-4.8,6.4-5.3,10.5C51.5,50.8,53.2,50.6,54.9,50.6z ' +
+    'M49.7,55.4c0.5,4.3,2.4,8.1,5.4,10.9c2.9-2.8,4.9-6.6,5.4-10.8' +
+    'c-1.8-0.6-3.6-0.9-5.6-0.9C53.1,54.6,51.4,54.9,49.7,55.4z ' +
+    'M89,53.5c0-12-9.7-21.7-21.7-21.7c-4.5,0-8.7,1.4-12.2,3.7' +
+    'c-3.5-2.4-7.7-3.7-12.2-3.7c-12,0-21.7,9.7-21.7,21.7' +
+    'c0,8.5,4.9,15.9,12,19.4C33.6,84.6,43.2,94,54.9,94' +
+    'c11.7,0,21.2-9.3,21.7-20.9C84,69.7,89,62.2,89,53.5z ' +
+    'M64.3,57.3c-0.8,4.4-2.9,8.4-5.9,11.5c2.6,1.5,5.7,2.5,8.9,2.5' +
+    'c1.8,0,3.6-0.3,5.2-0.8C72,64.9,68.8,60.1,64.3,57.3z ' +
+    'M67.3,35.8c-3.3,0-6.3,0.9-8.9,2.5c3.7,3.8,6.1,8.9,6.2,14.6' +
+    'c6.1,3.1,10.6,8.9,11.7,15.8C81.5,65.6,85,60,85,53.5' +
+    'C85,43.8,77.1,35.8,67.3,35.8z'],
+
+    ['meterTable', tableFrame +
+    'M96.3,79.2c0-19.1-22.1-34.6-41.3-34.6S13.7,60.1,13.7,79.2' +
+    'H39.4c0-7.2,8.4-13.1,15.7-13.1S70.6,72,70.6,79.2H96.3z' +
+    'M48,65.6L36.8,53c0-.5-1.5.5-1.4,0.7l5.3,16.6z'],
+
+    ['pipeconfTable', tableFrame +
+    'M10,66h13v3h-13z' +
+    'M23,62.5L28,67.5L23,72.5z' +
+    'M30,55h12.5v25h-12.5z' +
+    'M45,66h13v3h-13z' +
+    'M58,62.5L63,67.5L58,72.5z' +
+    'M65,55h12.5v25h-12.5z' +
+    'M79,66h15v3h-15z' +
+    'M94,62.5L99,67.5L94,72.5z'],
+
+    // --- Topology toolbar specific glyphs ----------------------
+
+    ['summary', 'M95.8,9.2H14.2c-2.8,0-5,2.2-5,5v81.5c0,2.8,2.2,5,5,' +
+    '5h81.5c2.8,0,5-2.2,5-5V14.2C100.8,11.5,98.5,9.2,95.8,9.2z ' +
+    'M16.7,22.2c0-1.1,0.9-2,2-2h20.1c1.1,0,2,0.9,2,2v20.1c0,1.1-0.9,' +
+    '2-2,2H18.7c-1.1,0-2-0.9-2-2V22.2z M93,87c0,1.1-0.9,2-2,2H18.9' +
+    'c-1.1,0-2-0.9-2-2v-7c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2V87z ' +
+    'M93,65c0,1.1-0.9,2-2,2H18.9c-1.1,0-2-0.9-2-2v-7c0-1.1,0.9-2,' +
+    '2-2H91c1.1,0,2,0.9,2,2V65z'],
+
+    ['details', 'M95.8,9.2H14.2c-2.8,0-5,2.2-5,5v81.5c0,2.8,2.2,5,5,' +
+    '5h81.5c2.8,0,5-2.2,5-5V14.2C100.8,11.5,98.5,9.2,95.8,9.2z M16.9,' +
+    '22.2c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2v7c0,1.1-0.9,2-2,2H18.9c' +
+    '-1.1,0-2-0.9-2-2V22.2z M93,87.8c0,1.1-0.9,2-2,2H18.9c-1.1,' +
+    '0-2-0.9-2-2v-7c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2V87.8z M93,68.2' +
+    'c0,1.1-0.9,2-2,2H18.9c-1.1,0-2-0.9-2-2v-7c0-1.1,0.9-2,2-2H91' +
+    'c1.1,0,2,0.9,2,2V68.2z M93,48.8c0,1.1-0.9,2-2,2H19c-1.1,0-2-' +
+    '0.9-2-2v-7c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2V48.8z'],
+
+    ['ports', 'M98,9.2H79.6c-1.1,0-2.1,0.9-2.1,2.1v17.6l-5.4,5.4c-1.7' +
+    '-1.1-3.8-1.8-6-1.8c-6,0-10.9,4.9-10.9,10.9c0,2.2,0.7,4.3,1.8,6' +
+    'l-7.5,7.5c-1.8-1.2-3.9-1.9-6.2-1.9c-6,0-10.9,4.9-10.9,10.9c0,' +
+    '2.3,0.7,4.4,1.9,6.2l-6.2,6.2H11.3c-1.1,0-2.1,0.9-2.1,2.1v18.4' +
+    'c0,1.1,0.9,2.1,2.1,2.1h18.4c1.1,0,2.1-0.9,2.1-2.1v-16l7-6.9' +
+    'c1.4,0.7,3,1.1,4.7,1.1c6,0,10.9-4.9,10.9-10.9c0-1.7-0.4-3.3-' +
+    '1.1-4.7l8-8c1.5,0.7,3.1,1.1,4.8,1.1c6,0,10.9-4.9,10.9-10.9c0' +
+    '-1.7-0.4-3.4-1.1-4.8l6.9-6.9H98c1.1,0,2.1-0.9,2.1-2.1V11.3' +
+    'C100.1,10.2,99.2,9.2,98,9.2z M43.4,72c-3.3,0-6-2.7-6-6s2.7-6,' +
+    '6-6s6,2.7,6,6S46.7,72,43.4,72z M66.1,49.5c-3.3,0-6-2.7-6-6' +
+    'c0-3.3,2.7-6,6-6s6,2.7,6,6C72.2,46.8,69.5,49.5,66.1,49.5z'],
+
+    ['map', 'M95.8,9.2H14.2c-2.8,0-5,2.2-5,5v66c0.3-1.4,0.7-2.8,' +
+    '1.1-4.1l1.6,0.5c-0.9,2.4-1.6,4.8-2.2,7.3l-0.5-0.1v12c0,2.8,2.2,' +
+    '5,5,5h81.5c2.8,0,5-2.2,5-5V14.2C100.8,11.5,98.5,9.2,95.8,9.2z ' +
+    'M16.5,67.5c-0.4,0.5-0.7,1-1,1.5c-0.3,0.5-0.6,1-0.9,1.6l-1.9-0.9' +
+    'c0.3-0.6,0.6-1.2,0.9-1.8c0.3-0.6,0.6-1.2,1-1.7c0.7-1.1,1.5-2.2,' +
+    '2.5-3.2l1.8,1.8C18,65.6,17.2,66.5,16.5,67.5z M29.7,64.1' +
+    'c-0.4-0.4-0.8-0.8-1.2-1.1c-0.1-0.1-0.2-0.1-0.2-0.1c0,0-0.1,' +
+    '0-0.1-0.1l-0.1,0l0,0l-0.1,0c-0.3-0.1-0.5-0.2-0.8-0.2c-0.5-0.1' +
+    '-1.1-0.2-1.6-0.3c-0.6,0-1.1,0-1.6,0l-0.4-2.8c0.7-0.1,1.5-0.2,2.2' +
+    '-0.1c0.7,0,1.4,0.1,2.2,0.3c0.4,0.1,0.7,0.2,1,0.3l0.1,0l0,0l0.1,' +
+    '0l0.1,0c0.1,0,0.1,0,0.3,0.1c0.3,0.1,0.5,0.2,0.7,0.4c0.7,0.5,' +
+    '1.2,0.9,1.7,1.4L29.7,64.1z M39.4,74.7c-1.8-1.8-3.6-3.8-5.3-5.7' +
+    'l2.6-2.4c0.9,0.9,1.8,1.8,2.7,2.7c0.9,0.9,1.8,1.7,2.7,2.6L39.4,' +
+    '74.7z M50.8,84.2c-1.1-0.7-2.2-1.5-3.3-2.3c-0.5-0.4-1.1-0.8-1.6' +
+    '-1.2c-0.5-0.4-1-0.8-1.5-1.2l2.7-3.4c0.5,0.4,1,0.8,1.5,1.1c0.5,' +
+    '0.3,1,0.7,1.5,1c1,0.7,2.1,1.3,3.1,1.9L50.8,84.2z M61.3,' +
+    '88.7c-0.7-0.1-1.4-0.3-2.1-0.5c-0.7-0.2-1.4-0.5-2-0.7l1.8' +
+    '-4.8c0.6,0.2,1.1,0.4,1.6,0.5c0.5,0.2,1.1,0.3,1.6,0.4c1,0.2,2.1,' +
+    '0.2,3,0.1l0.7,5.1C64.3,89.1,62.7,88.9,61.3,88.7z M75.1,80.4c' +
+    '-0.2,0.7-0.5,1.4-0.9,2c-0.2,0.3-0.3,0.7-0.5,1l-0.3,0.5l-0.3,' +
+    '0.4l-3.9-2.8l0.3-0.4l0.2-0.3c0.1-0.2,0.3-0.4,0.4-0.7c0.3-0.5,' +
+    '0.5-0.9,0.7-1.5c0.4-1,0.8-2.1,1.1-3.3l4.2,0.9C75.9,77.7,75.6,' +
+    '79,75.1,80.4z M73,69.2l0.2-1.9l0.1-1.9c0.1-1.2,0.1-2.5,0.1-' +
+    '3.8l2.5-0.2c0.2,1.3,0.4,2.6,0.5,3.9l0.1,2l0.1,2L73,69.2z ' +
+    'M73,51l0.5-0.1c0.4,1.3,0.8,2.6,1.1,3.9L73.2,55C73.1,53.7,73.1,' +
+    '52.3,73,51z M91.9,20.4c-0.7,1.4-3.6,3.6-4.2,3.9c-1.5,0.8-5,' +
+    '2.8-10.1,7.7c3,2.9,5.8,5.4,7.3,6.4c2.6,1.8,3.4,4.3,3.6,6.1c0.1,' +
+    '1.1-0.1,2.5-0.4,3c-0.5,0.9-1.6,2-3,1.4c-2-0.8-11.5-9.6-13-11c' +
+    '-3.5,3.9-7.4,8.9-11.7,15.1c0,0-3.1,3.4-5.2,0.9C52.9,51.5,61,' +
+    '39.3,61,39.3s2.2-3.1,5.6-7c-2.9-3-5.9-6.3-6.6-7.3c0,0-3.7-5-1.3' +
+    '-6.6c3.2-2.1,6.3,0.8,6.3,0.8s3.1,3.3,7,7.2c4.7-4.7,10.1-9.2,' +
+    '14.7-10c0,0,3.3-1,5.2,1.7C92.5,18.8,92.4,19.6,91.9,20.4z'],
+
+    ['cycleLabels', 'M72.5,33.9c0,0.6-0.2,1-0.5,1H40c-0.3,0-0.5-0.4' +
+    '-0.5-1V20.7c0-0.6,0.2-1,0.5-1h32c0.3,0,0.5,0.4,0.5,1V33.9z ' +
+    'M41.2,61.8c0-0.6-0.2-1-0.5-1h-32c-0.3,0-0.5,0.4-0.5,1V75c0,0.6,' +
+    '0.2,1,0.5,1h32c0.3,0,0.5-0.4,0.5-1V61.8z M101.8,61.8c0-0.6-0.2' +
+    '-1-0.5-1h-32c-0.3,0-0.5,0.4-0.5,1V75c0,0.6,0.2,1,0.5,1h32c0.3,' +
+    '0,0.5-0.4,0.5-1V61.8z M17.2,52.9c0-0.1-0.3-7.1,4.6-13.6l-2.4-1.8' +
+    'c-5.4,7.3-5.2,15.2-5.1,15.5L17.2,52.9z M12.7,36.8l7.4,2.5l1.5,' +
+    '7.6L29.5,31L12.7,36.8z M94.2,42.3c-2.1-8.9-8.3-13.7-8.6-13.9l' +
+    '-1.8,2.4c0.1,0,5.6,4.3,7.5,12.2L94.2,42.3z M99,37.8l-6.6,4.1l' +
+    '-6.8-3.7l7.1,16.2L99,37.8z M69,90.2l-1.2-2.8c-0.1,0-6.6,2.8' +
+    '-14.3,0.6l-0.8,2.9c2.5,0.7,4.9,1,7,1C65,91.8,68.7,90.2,69,90.2z ' +
+    'M54.3,97.3L54,89.5l6.6-4.1l-17.6-1.7L54.3,97.3z'],
+
+    ['oblique', 'M80.9,30.2h4.3l15-16.9H24.8l-15,16.9h19v48.5h-4l-15,' +
+    '16.9h75.3l15-16.9H80.9V30.2z M78.6,78.7H56.1V30.2h22.5V78.7z' +
+    'M79.7,17.4c2.4,0,4.3,1.9,4.3,4.3c0,2.4-1.9,4.3-4.3,4.3s-4.3' +
+    '-1.9-4.3-4.3C75.4,19.3,77.4,17.4,79.7,17.4z M55,17.4c2.4,0,' +
+    '4.3,1.9,4.3,4.3c0,2.4-1.9,4.3-4.3,4.3s-4.3-1.9-4.3-4.3C50.7,' +
+    '19.3,52.6,17.4,55,17.4z M26.1,21.7c0-2.4,1.9-4.3,4.3-4.3c2.4,' +
+    '0,4.3,1.9,4.3,4.3c0,2.4-1.9,4.3-4.3,4.3C28,26,26.1,24.1,26.1,' +
+    '21.7z M31.1,30.2h22.6v48.5H31.1V30.2z M30.3,91.4c-2.4,0-4.3' +
+    '-1.9-4.3-4.3c0-2.4,1.9-4.3,4.3-4.3c2.4,0,4.3,1.9,4.3,4.3C34.6,' +
+    '89.5,32.7,91.4,30.3,91.4z M54.9,91.4c-2.4,0-4.3-1.9-4.3-4.3c0' +
+    '-2.4,1.9-4.3,4.3-4.3c2.4,0,4.3,1.9,4.3,4.3C59.2,89.5,57.3,' +
+    '91.4,54.9,91.4z M84,87.1c0,2.4-1.9,4.3-4.3,4.3c-2.4,0-4.3-1.9' +
+    '-4.3-4.3c0-2.4,1.9-4.3,4.3-4.3C82.1,82.8,84,84.7,84,87.1z'],
+
+    ['filters', 'M24.8,13.3L9.8,40.5h75.3l15.0-27.2H24.8z M72.8,32.1l-' +
+    '9.7-8.9l-19.3,8.9l-6.0-7.4L24.1,30.9l-1.2-2.7l15.7-7.1l6.0,7.4' +
+    'l19.0-8.8l9.7,8.8l11.5-5.6l1.3,2.7L72.8,32.1zM24.3,68.3L9.3,' +
+    '95.5h75.3l15.0-27.2H24.3z M84.3,85.9L70.7,79.8l-6.0,7.4l-19.3' +
+    '-8.9l-9.7,8.9l-13.3-6.5l1.3-2.7l11.5,5.6l9.7-8.8l19.0,8.8l6.0' +
+    '-7.4l15.7,7.1L84.3,85.9z M15.3,57h-6v-4h6V57zM88.1,57H76.0v-4h' +
+    '12.1V57z M69.9,57H57.8v-4h12.1V57z M51.7,57H39.6v-4H51.7V57z ' +
+    'M33.5,57H21.4v-4h12.1V57zM100.2,57h-6v-4h6V57z'],
+
+    ['resetZoom', 'M86,79.8L61.7,54.3c1.8-2.9,2.8-6.3,2.9-10c0.3-11.2' +
+    '-8.6-20.5-19.8-20.8C33.7,23.2,24.3,32,24.1,43.2c-0.3,11.2,8.6,' +
+    '20.5,19.8,20.8c4,0.1,8.9-0.8,11.9-3.6l23.7,25c1.5,1.6,4,2.3,' +
+    '5.3,1l1.6-1.6C87.7,83.7,87.5,81.4,86,79.8z M31.4,43.4c0.2-7.1,' +
+    '6.1-12.8,13.2-12.6C51.8,31,57.5,37,57.3,44.1c-0.2,7.1-6.1,12.8' +
+    '-13.2,12.6C36.9,56.5,31.3,50.6,31.4,43.4zM22.6,104H6V86.4c0' +
+    '-1.7,1.4-3.1,3.1-3.1s3.1,1.4,3.1,3.1v11.4h10.4c1.7,0,3.1,1.4,' +
+    '3.1,3.1S24.3,104,22.6,104z M25.7,9.1c0,1.7-1.4,3.1-3.1,3.1' +
+    'H12.2v11.4c0,1.7-1.4,3.1-3.1,3.1S6,25.3,6,23.6V6h16.6C24.3,6,' +
+    '25.7,7.4,25.7,9.1z M84.3,100.9c0-1.7,1.4-3.1,3.1-3.1h10.4V86.4' +
+    'c0-1.7,1.4-3.1,3.1-3.1s3.1,1.4,3.1,3.1V104H87.4C85.7,104,84.3,' +
+    '102.6,84.3,100.9z M87.4,6H104v17.6c0,1.7-1.4,3.1-3.1,3.1s-3.1' +
+    '-1.4-3.1-3.1V12.2H87.4c-1.7,0-3.1-1.4-3.1-3.1S85.7,6,87.4,6z'],
+
+    ['relatedIntents', 'M99.9,43.7v22.6c0,1.9-1.5,3.4-3.4,3.4H73.9c' +
+    '-1.9,0-3.4-1.5-3.4-3.4V43.7c0-1.9,1.5-3.4,3.4-3.4h22.6C98.4,' +
+    '40.3,99.9,41.8,99.9,43.7z M48.4,46.3l6.2,6.7h-4.6L38.5,38v9.7' +
+    'l4.7,5.3H10.1V57h33.2l-4.8,5.3v9.5L49.8,57h5.1v0l-6.5,7v11.5' +
+    'L64.1,55L48.4,34.4V46.3z'],
+
+    ['nextIntent', 'M88.1,55.7L34.6,13.1c0,0-1.6-0.5-2.1-0.2c-1.9,1.2' +
+    '-6.5,13.8-3.1,17.2c7,6.9,30.6,24.5,32.4,25.9c-1.8,1.4-25.4,19' +
+    '-32.4,25.9c-3.4,3.4,1.2,16,3.1,17.2c0.6,0.4,2.1-0.2,2.1-0.2' +
+    's53.1-42.4,53.5-42.7C88.5,56,88.1,55.7,88.1,55.7z'],
+
+    ['prevIntent', 'M22.5,55.6L76,12.9c0,0,1.6-0.5,2.2-0.2c1.9,1.2,' +
+    '6.5,13.8,3.1,17.2c-7,6.9-30.6,24.5-32.4,25.9c1.8,1.4,25.4,19,' +
+    '32.4,25.9c3.4,3.4-1.2,16-3.1,17.2c-0.6,0.4-2.2-0.2-2.2-0.2' +
+    'S22.9,56.3,22.5,56C22.2,55.8,22.5,55.6,22.5,55.6z'],
+
+    ['intentTraffic', 'M14.7,71.5h-6v-33h6V71.5z M88.5,38.5H76.9v33' +
+    'h11.7V38.5z M70.1,38.5H58.4v33h11.7V38.5z M51.6,38.5H39.9v33' +
+    'h11.7V38.5z M33.1,38.5H21.5v33h11.7V38.5z M101.3,38.5h-6v33h6' +
+    'V38.5z'],
+
+    ['allTraffic', 'M15.7,64.5h-7v-19h7V64.5z M78.6,45.5H62.9v19h15.7' +
+    'V45.5z M47.1,45.5H31.4v19h15.7V45.5z M101.3,45.5h-7v19h7V45.5z' +
+    'M14.7,14.1h-6v19h6V14.1z M88.5,14.1H76.9v19h11.7V14.1z M70.1,' +
+    '14.1H58.4v19h11.7V14.1z M51.6,14.1H39.9v19h11.7V14.1z M33.1,14.1' +
+    'H21.5v19h11.7V14.1z M101.3,14.1h-6v19h6V14.1z M14.7,76.9h-6v19' +
+    'h6V76.9z M88.5,76.9H76.9v19h11.7V76.9z M70.1,76.9H58.4v19h11.7' +
+    'V76.9z M51.6,76.9H39.9v19h11.7V76.9z M33.1,76.9H21.5v19h11.7' +
+    'V76.9z M101.3,76.9h-6v19h6V76.9z'],
+
+    ['flows', 'M93.8,46.1c-4.3,0-8,3-9,7H67.9v-8.8c0-1.3-1.1-2.4-2.4' +
+    '-2.4h-8.1V25.3c4-1,7-4.7,7-9.1c0-5.2-4.2-9.4-9.4-9.4c-5.2,0' +
+    '-9.4,4.2-9.4,9.4c0,4.3,3,8,7,9v16.5H44c-1.3,0-2.4,1.1-2.4,2.4' +
+    'v8.8H25.3c-1-4.1-4.7-7.1-9.1-7.1c-5.2,0-9.4,4.2-9.4,9.4s4.2,' +
+    '9.4,9.4,9.4c4.3,0,8-2.9,9-6.9h16.4v7.9c0,1.3,1.1,2.4,2.4,2.4' +
+    'h8.6v16.6c-4,1.1-6.9,4.7-6.9,9c0,5.2,4.2,9.4,9.4,9.4c5.2,0,' +
+    '9.4-4.2,9.4-9.4c0-4.4-3-8-7.1-9.1V68.2h8.1c1.3,0,2.4-1.1,2.4' +
+    '-2.4v-7.9h16.8c1.1,4,4.7,7,9,7c5.2,0,9.4-4.2,9.4-9.4S99,46.1,' +
+    '93.8,46.1z M48.7,16.3c0-3.5,2.9-6.4,6.4-6.4c3.5,0,6.4,2.9,6.4,' +
+    '6.4s-2.9,6.4-6.4,6.4C51.5,22.6,48.7,19.8,48.7,16.3zM16.2,61.7c' +
+    '-3.5,0-6.4-2.9-6.4-6.4c0-3.5,2.9-6.4,6.4-6.4s6.4,2.9,6.4,6.4' +
+    'C22.6,58.9,19.7,61.7,16.2,61.7z M61.4,93.7c0,3.5-2.9,6.4-6.4,' +
+    '6.4c-3.5,0-6.4-2.9-6.4-6.4c0-3.5,2.9-6.4,6.4-6.4C58.6,87.4,' +
+    '61.4,90.2,61.4,93.7z M93.8,61.8c-3.5,0-6.4-2.9-6.4-6.4c0-3.5,' +
+    '2.9-6.4,6.4-6.4s6.4,2.9,6.4,6.4C100.1,58.9,97.3,61.8,93.8,61.8z'],
+
+    ['eqMaster', 'M100.1,46.9l-10.8-25h0.2c0.5,0,0.8-0.5,0.8-1.1v-3.2' +
+    'c0-0.6-0.4-1.1-0.8-1.1H59.2v-5.1c0-0.5-0.8-1-1.7-1h-5.1c-0.9,0' +
+    '-1.7,0.4-1.7,1v5.1l-30.2,0c-0.5,0-0.8,0.5-0.8,1.1v3.2c0,0.6,' +
+    '0.4,1.1,0.8,1.1h0.1l-10.8,25C9,47.3,8.4,48,8.4,48.8v1.6l0,0h0' +
+    'v6.4c0,1.3,1.4,2.3,3.2,2.3h21.7c1.8,0,3.2-1,3.2-2.3v-8c0-0.9' +
+    '-0.7-1.6-1.7-2L22.9,21.9h27.9v59.6l-29,15.9c0,1.2,1.8,2.2,4.1,' +
+    '2.2h58.3c2.3,0,4.1-1,4.1-2.2l-29-15.9V21.9h27.8L75.2,46.8c-1,' +
+    '0.4-1.7,1.1-1.7,2v8c0,1.3,1.4,2.3,3.2,2.3h21.7c1.8,0,3.2-1,3.2' +
+    '-2.3v-8C101.6,48,101,47.3,100.1,46.9z M22,23.7l10.8,22.8H12.1' +
+    'L22,23.7z M97.9,46.5H77.2L88,23.7L97.9,46.5z'],
+
+    ['xClose', 'M20,8l35,35,35-35,12,12-35,35,35,35-12,12-35-35-35,35' +
+    '-12-12,35-35-35-35,12-12Z'],
+
+    ['clock', 'M92.9,61.3a39,39,0,1,1-39-39,39,39,0,0,1,39,39h0Z' +
+    'M44,19.3c-4.4-7.4-14.8-9.3-23.2-4.2S9.1,30.2,13.5,37.6m80.8,0' +
+    'c4.4-7.4,1.2-17.5-7.3-22.5s-18.8-3.2-23.3,4.2m-8.4,1.8V16.5h4.4' +
+    'V11.9H48.2v4.6h4.6v4.6M51.6,56.4H51.5' +
+    'a5.4,5.4,0,0,0,2.4,10.3,4.7,4.7,0,0,0,4.9-3.1H74.5' +
+    'a2.2,2.2,0,0,0,2.4-2.2,2.4,2.4,0,0,0-2.4-2.3H58.8' +
+    'a5.3,5.3,0,0,0-2.5-2.6H56.2V32.9' +
+    'a2.3,2.3,0,0,0-.6-1.7,2.2,2.2,0,0,0-1.6-.7,2.4,2.4,0,0,0-2.4,2.4' +
+    'h0V56.4M82.2,91.1l-7.1,5.3-0.2.2-1.2,2.1a0.6,0.6,0,0,0,.2.8' +
+    'h0.2c2.6,0.4,10.7.9,10.3-1.2m-60.8,0c-0.4,2.1,7.7,1.6,10.3,1.2' +
+    'h0.2a0.6,0.6,0,0,0,.2-0.8l-1.2-2.1-0.2-.2-7.1-5.3'],
+
+// overlapping alarm clocks glyph (view box 110x110)
+    ['clocks', 'M65.7,26.6A34.7,34.7,0,0,0,31,61.5c0,19,16,35.2,35.5,34.8' +
+    'a35.1,35.1,0,0,0,34-35.1A34.7,34.7,0,0,0,65.7,26.6Z' +
+    'm8.6-2.7,27.4,16.4a31.3,31.3,0,0,0,1.2-3.1c1.3-5,0-9.4-3.2-13.2' +
+    'a16.9,16.9,0,0,0-16-6.2A12.8,12.8,0,0,0,74.3,23.9Z' +
+    'M57,23.9L56.4,23a12.9,12.9,0,0,0-10.7-5.5,16.9,16.9,0,0,0-15.1,8,' +
+    '14.1,14.1,0,0,0-2.3,11.2,10.4,10.4,0,0,0,1.4,3.5Z' +
+    'M26.9,31.6A33.7,33.7,0,0,0,9.7,59.3C9.2,72.8,14.9,83.1,26,90.7l1-1.9' +
+    'a0.6,0.6,0,0,0-.2-1A34.2,34.2,0,0,1,15.5,50.4' +
+    'a33.8,33.8,0,0,1,10.8-16,1.2,1.2,0,0,0,.5-0.6' +
+    'C26.9,33.1,26.9,32.4,26.9,31.6Z' +
+    'm1,8.1C14.6,55.9,19.2,81,37.6,91.1l1-2.3-2.8-2.4' +
+    'C26.4,77.6,22.9,66.7,25.1,54' +
+    'a31.6,31.6,0,0,1,4.2-10.8,0.8,0.8,0,0,0,.1-0.6Z' +
+    'M12,38.4a11.2,11.2,0,0,1-1.4-5.8A13.7,13.7,0,0,1,14.3,24' +
+    'a17.3,17.3,0,0,1,10.5-5.7h0.4C19,18,13.7,20,9.9,25.2' +
+    'a14.5,14.5,0,0,0-3,11,11.2,11.2,0,0,0,1.6,4.3Z' +
+    'm5.5-2.7L21,33' +
+    'a1,1,0,0,0,.3-0.7,14,14,0,0,1,3.9-8.6,17.3,17.3,0,0,1,10.2-5.4' +
+    'l0.6-.2C24.4,17.3,16.4,27,17.4,35.7Z' +
+    'M70.9,17.2H60.7v4.1h4v4.2H67V21.3h3.9V17.2Z' +
+    'M90.9,87.9l-0.5.3L86,91.5a7.9,7.9,0,0,0-2.6,3.1' +
+    'c-0.3.6-.2,0.8,0.5,0.9a27.9,27.9,0,0,0,6.8.2l1.6-.4' +
+    'a0.8,0.8,0,0,0,.6-1.2l-0.4-1.4Z' +
+    'm-50.2,0-1.8,6c-0.3,1.1-.1,1.4.9,1.7h0.7a26.3,26.3,0,0,0,7.2-.1' +
+    'c0.8-.1.9-0.4,0.5-1.1a8.2,8.2,0,0,0-2.7-3.1Z' +
+    'm-10.8-.4-0.2.6L28,93.5a0.9,0.9,0,0,0,.7,1.3,7.7,7.7,0,0,0,2.2.4' +
+    'l5.9-.2c0.5,0,.7-0.3.5-0.8a7.6,7.6,0,0,0-2.2-2.9Z' +
+    'm-11.3,0-0.2.7-1.6,5.4c-0.2.8-.1,1.2,0.7,1.4a8,8,0,0,0,2.2.4l6-.2' +
+    'c0.4,0,.7-0.3.5-0.6a7.1,7.1,0,0,0-1.9-2.7l-2.8-2.1Z' +
+    'M65.7,26.6m-2,30.3a4.4,4.4,0,0,0-2.2,2,4.8,4.8,0,0,0,4,7.2,' +
+    '4.1,4.1,0,0,0,4.3-2.3,0.8,0.8,0,0,1,.8-0.5H84.1' +
+    'a1.9,1.9,0,0,0,2-1.7,2.1,2.1,0,0,0-1.7-2.2H70.8' +
+    'a1,1,0,0,1-1-.5,6.4,6.4,0,0,0-1.5-1.6,1.1,1.1,0,0,1-.5-1' +
+    'q0-10.1,0-20.3a1.9,1.9,0,0,0-2.2-2.1,2.1,2.1,0,0,0-1.8,2.2' +
+    'q0,6.1,0,12.2C63.7,51.2,63.7,54,63.7,56.9Z'],
+]);
+
+export const badgeDataSet = new Map<string, string>([
+    ['_viewbox', '0 0 10 10'],
+
+    ['checkMark', 'M8.6,3.4L4.4,7.7L1.4,4.7L2.5,3.6L4.4,5.5L7.5,2.3L8.6,3.4Z'],
+
+    ['xMark', 'M7.8,6.7L6.7,7.8,5,6.1,3.3,7.8,2.2,6.7,3.9,5,2.2,3.3,3.3,' +
+    '2.2,5,3.9,6.7,2.2,7.8,3.3,6.1,5Z'],
+
+    ['triangleUp', 'M0.5,6.2c0,0,3.8-3.8,4.2-4.2C5,1.7,5.3,2,5.3,2l4.3,' +
+    '4.3c0,0,0.4,0.4-0.1,0.4c-1.7,0-8.2,0-8.8,0C0,6.6,0.5,6.2,0.5,6.2z'],
+
+    ['triangleLeft', 'm 6.13,9.63 c 0,0 -3.8,-3.8 -4.2,-4.2 -0.3,-0.3 0,-0.6 0,' +
+        '-0.6 L 6.23,0.54 c 0,0 0.4,-0.4 0.4,0.1 0,1.7 0,8.2 0,8.8 -0.1,0.7 -0.5,0.2 -0.5,0.2 z'],
+
+    ['triangleRight', 'm 4.07,9.6 c 0,0 3.8,-3.8 4.2,-4.2 0.3,-0.3 0,-0.6 0,-0.6' +
+        'l -4.3,-4.3 c 0,0 -0.4,-0.4 -0.4,0.1 0,1.7 0,8.2 0,8.8 0.1,0.7 0.5,0.2 0.5,0.2 z'],
+
+    ['triangleDown', 'M9.5,4.2c0,0-3.8,3.8-4.2,4.2c-0.3,0.3-0.5,0-0.5,' +
+    '0L0.5,4.2c0,0-0.4-0.4,0.1-0.4c1.7,0,8.2,0,8.8,0C10,3.8,9.5,4.2,' +
+    '9.5,4.2z'],
+
+    ['plus', 'M4,2h2v2h2v2h-2v2h-2v-2h-2v-2h2z'],
+
+    ['minus', 'M2,4h6v2h-6z'],
+
+    ['play', 'M3,1.5l3.5,3.5l-3.5,3.5z'],
+
+    ['stop', 'M2.5,2.5h5v5h-5z'],
+]);
+
+export const spriteData = new Map<string, string>([
+    ['_cloud', '0 0 110 110'],
+    ['cloud', 'M37.6,79.5c-6.9,8.7-20.4,8.6-22.2-2.7' +
+    'M16.3,41.2c-0.8-13.9,19.4-19.2,23.5-7.8' +
+    'M38.9,30.9c5.1-9.4,15.1-8.5,16.9-1.3' +
+    'M54.4,32.9c4-12.9,14.8-9.6,18.6-3.8' +
+    'M95.8,58.5c10-4.1,11.7-17.8-0.9-19.8' +
+    'M18.1,76.4C5.6,80.3,3.8,66,13.8,61.5' +
+    'M16.2,62.4C2.1,58.4,3.5,36,16.8,36.6' +
+    'M93.6,74.7c10.2-2,10.7-14,5.8-18.3' +
+    'M71.1,79.3c11.2,7.6,24.6,6.4,22.1-11.7' +
+    'M36.4,76.8c3.4,13.3,35.4,11.6,36.1-1.4' +
+    'M70.4,31c11.8-10.4,26.2-5.2,24.7,10.1'],
+]);
+
+
+    // --- Mojo Re-Design ---------------------------------------
+const m_octagon = 'M33.5,91.9a1.8,1.8,0,0,1-1.3-.5L8.7,68a1.8,1.8,0,0,' +
+    '1-.5-1.3V33.5a1.8,1.8,0,0,1,.5-1.3L32,8.7a1.8,1.8,0,0,1,1.3-.5' +
+    'H66.5a1.8,1.8,0,0,1,1.3.5L91.3,32a1.8,1.8,0,0,1,.5,1.3V66.5' +
+    'a1.8,1.8,0,0,1-.5,1.3L68,91.3a1.8,1.8,0,0,1-1.3.5H33.5Z' +
+    'm-21.7-26L34.3,88.2H65.9L88.2,65.7V34.1L65.7,11.8H34.1' +
+    'L11.8,34.3V65.9Z';
+
+const m_switch_arrows = 'M60,42.1l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V33.8' +
+    'a1.8,1.8,0,0,1,3.7,0v3.3l13-7.1L61.9,23v3.3' +
+    'A1.8,1.8,0,0,1,60,28.2h-7v3.7h1.7a1.8,1.8,0,0,1,0,3.7H51.2' +
+    'a1.8,1.8,0,0,1-1.8-1.8V26.4a1.8,1.8,0,0,1,1.8-1.8h7V20' +
+    'a1.8,1.8,0,0,1,2.7-1.6l18.7,10a1.8,1.8,0,0,1,0,3.2L60.9,41.8Z' +
+    'M60,69.2l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V60.9a1.8,1.8,0,0,1,3.7,0' +
+    'v3.3l13-7.1-13-6.9v3.3A1.8,1.8,0,0,1,60,55.3h-7V59h1.7' +
+    'a1.8,1.8,0,0,1,0,3.7H51.2a1.8,1.8,0,0,1-1.8-1.8V53.5' +
+    'a1.8,1.8,0,0,1,1.8-1.8h7V47.1a1.8,1.8,0,0,1,2.7-1.6l18.7,10' +
+    'a1.8,1.8,0,0,1,0,3.2L60.9,69ZM40,54.8l-0.9-.2L20.4,44.2' +
+    'a1.8,1.8,0,0,1,0-3.2L39.1,31a1.8,1.8,0,0,1,2.7,1.6v4.5h7' +
+    'A1.8,1.8,0,0,1,50.6,39v7.4a1.8,1.8,0,0,1-1.8,1.8H45.2' +
+    'a1.8,1.8,0,0,1,0-3.7h1.7V40.9H40A1.8,1.8,0,0,1,38.1,39' +
+    'V35.7l-13,6.9,13,7.1V46.4a1.8,1.8,0,0,1,3.7,0v6.5' +
+    'a1.8,1.8,0,0,1-.9,1.6ZM40,81.9l-0.9-.2L20.4,71.4' +
+    'a1.8,1.8,0,0,1,0-3.2l18.7-10a1.8,1.8,0,0,1,2.7,1.6v4.5h7' +
+    'a1.8,1.8,0,0,1,1.8,1.8v7.4a1.8,1.8,0,0,1-1.8,1.8H45.2' +
+    'a1.8,1.8,0,0,1,0-3.7h1.7V68H40a1.8,1.8,0,0,1-1.8-1.8V62.9' +
+    'l-13,6.9,13,7.1V73.6a1.8,1.8,0,0,1,3.7,0V80a1.8,1.8,0,0,1-.9,1.6Z';
+
+const m_diamond = 'M50,87a16.1,16.1,0,0,1-11.5-4.7L17.7,61.5a16.3,16.3,0,0,' +
+    '1,0-22.9L38.5,17.7a16.3,16.3,0,0,1,22.9,0L82.3,38.5' +
+    'a16.3,16.3,0,0,1,0,22.9L61.5,82.3A16.1,16.1,0,0,1,50,87Z' +
+    'm0-70.3a12.4,12.4,0,0,0-8.9,3.7L20.3,41.1a12.6,12.6,0,0,0,0,' +
+    '17.7L41.1,79.7a12.6,12.6,0,0,0,17.7,0L79.7,58.9a12.6,12.6,0,0,' +
+    '0,0-17.7L58.9,20.3A12.4,12.4,0,0,0,50,16.7Z';
+
+const m_trafficArrows = 'M41,66.7l-0.9-.2-16.8-9a1.8,1.8,0,0,1,0-3.2' +
+    'L40.1,45a1.8,1.8,0,0,1,2.7,1.6v5.8a1.8,1.8,0,0,1-3.7,0V49.7' +
+    'L28,55.8l11.1,5.9V59.1A1.8,1.8,0,0,1,41,57.2H47v-3H45.7' +
+    'a1.8,1.8,0,0,1,0-3.7h3.2a1.8,1.8,0,0,1,1.8,1.8v6.7' +
+    'a1.8,1.8,0,0,1-1.8,1.8H42.8v3.9a1.8,1.8,0,0,1-.9,1.6ZM59,55.3' +
+    'l-1-.3a1.8,1.8,0,0,1-.9-1.6V49.5H51.1a1.8,1.8,0,0,1-1.8-1.8' +
+    'V41a1.8,1.8,0,0,1,1.8-1.8h3.2a1.8,1.8,0,0,1,0,3.7H53v3H59' +
+    'a1.8,1.8,0,0,1,1.8,1.8v2.7L72,44.4,60.9,38.3V41' +
+    'a1.8,1.8,0,0,1-3.7,0V35.2a1.8,1.8,0,0,1,2.7-1.6l16.8,9.3' +
+    'a1.8,1.8,0,0,1,0,3.2L59.9,55Z';
+
+const m_otn_base = 'M63.1,79.8a7.5,7.5,0,0,1-5.3-12.8,7.7,7.7,0,0,1,10.7,0' +
+    'A7.5,7.5,0,0,1,63.1,79.8Zm0-11.3A3.8,3.8,0,0,0,60.4,75h0' +
+    'a3.9,3.9,0,0,0,5.4,0A3.8,3.8,0,0,0,63.1,68.4Z' +
+    'M63.1,35.2A7.5,7.5,0,1,1,68.5,33h0A7.5,7.5,0,0,1,63.1,35.2Z' +
+    'm4-3.5h0Zm-4-7.8A3.8,3.8,0,1,0,65.8,25,3.8,3.8,0,0,0,63.1,23.9Z' +
+    'M73.3,57.3a7.5,7.5,0,1,1,7.5-7.5A7.5,7.5,0,0,1,73.3,57.3Z' +
+    'm0-11.3a3.8,3.8,0,1,0,3.8,3.8A3.8,3.8,0,0,0,73.3,46Z' +
+    'M61.9,48.7H51.5V41.9l5-5a1.9,1.9,0,0,0-2.6-2.6l-4.2,4.2L38,48.7' +
+    'H34.1A7.3,7.3,0,0,0,32,45a7.6,7.6,0,0,0-10.7,0,7.5,7.5,0,0,0,0,' +
+    '10.6,7.6,7.6,0,0,0,10.7,0,7.4,7.4,0,0,0,1.9-3.2h5.2' +
+    'l11.4,9.9,3.4,3.4a1.8,1.8,0,0,0,2.6,0,1.8,1.8,0,0,0,0-2.6l-5-5' +
+    'V52.4H61.9A1.9,1.9,0,0,0,61.9,48.7ZM29.4,53a3.8,3.8,0,0,1-6.5-2.7' +
+    'A3.9,3.9,0,0,1,24,47.6a3.9,3.9,0,0,1,5.4,0A3.8,3.8,0,0,1,29.4,53Z';
+
+export const mojoDataSet = new Map<string, string>([
+    ['_viewbox', '0 0 100 100'],
+
+    ['m_cloud', 'M62,48.8H61.5a1.8,1.8,0,0,1-1.3-2.3,11,11,0,0,' +
+    '0-21.1-6.1,1.8,1.8,0,1,1-3.5-1,14.7,14.7,0,0,1,28.2,8.1' +
+    'A1.8,1.8,0,0,1,62,48.8ZM70.6,71.2H28.3' +
+    'A14.7,14.7,0,1,1,38.4,45.2,1.8,1.8,0,1,1,36,48' +
+    'a11,11,0,1,0-7.5,19.4H71.8A11,11,0,0,0,71,45.5' +
+    'a10.9,10.9,0,0,0-3.2.5,1.8,1.8,0,1,1-1.1-3.5,14.7,14.7,0,0,1,19,14' +
+    'A14.8,14.8,0,0,1,72.2,71.1H70.6Z'],
+
+    ['m_map', 'M50,17.6A32.4,32.4,0,1,0,82.4,50,32.4,32.4,0,0,0,50,17.6Z' +
+    'm0,61.1A28.7,28.7,0,1,1,78.7,50,28.7,28.7,0,0,1,50,78.7Z' +
+    'M75.9,48.8a1.8,1.8,0,0,1-1.8,1.8H65' +
+    'a38.3,38.3,0,0,1-8.4,22.9,1.8,1.8,0,0,1-1.4.7,1.8,1.8,0,0,' +
+    '1-1.2-.4,1.8,1.8,0,0,1-.3-2.6,34.6,34.6,0,0,0,7.5-20.5H39.2' +
+    'a34.5,34.5,0,0,0,7.6,20.7,1.8,1.8,0,0,1-1.4,3,1.8,1.8,0,0,' +
+    '1-1.4-.7,38.2,38.2,0,0,1-8.5-23H26.2a1.8,1.8,0,1,1,0-3.7' +
+    'h9.4a38.1,38.1,0,0,1,8.4-21.6,1.8,1.8,0,0,1,2.9,2.3,34.4,34.4,0,0,' +
+    '0-7.6,19.3h22a34.3,34.3,0,0,0-8-19.7,1.8,1.8,0,1,1,2.8-2.4,38,38,' +
+    '0,0,1,8.8,22.1h9.1A1.8,1.8,0,0,1,75.9,48.8Z'],
+
+    ['m_selectMap', 'M51.36,8.74A32.45,32.45,0,0,0,19,41.15' +
+    'a32.12,32.12,0,0,0,9.22,22.62L17,72.07a1.84,1.84,0,0,0,1,3.32' +
+    'L35.52,76h0l10,14.42a1.84,1.84,0,0,0,1.52.79,2.05,2.05,0,0,' +
+    '0,.49-0.06,1.84,1.84,0,0,0,1.35-1.66l2.68-39.19' +
+    'a1.73,1.73,0,0,0-.06-0.6,0.85,0.85,0,0,0-.09-0.24,1.7,1.7,0,0,' +
+    '0-.81-0.9l-0.18-.1a1.69,1.69,0,0,0-.94-0.16H49.33l-0.16,0' +
+    'a1,1,0,0,0-.31.12,1.52,1.52,0,0,0-.33.18L42.73,53' +
+    'a34.6,34.6,0,0,1-2.22-11.21H62.68a34.55,34.55,0,0,1-7.55,20.54' +
+    'A1.85,1.85,0,1,0,58,64.63a38.26,38.26,0,0,0,8.37-22.86h9.08' +
+    'a1.85,1.85,0,0,0,0-3.7H66.3a38.06,38.06,0,0,0-8.83-22.14,' +
+    '1.85,1.85,0,1,0-2.82,2.38,34.37,34.37,0,0,1,8,19.76h-22' +
+    'a34.38,34.38,0,0,1,7.58-19.3,1.85,1.85,0,1,0-2.87-2.33,' +
+    '38.11,38.11,0,0,0-8.41,21.63H27.53a1.85,1.85,0,0,0,0,3.7' +
+    'h9.28a38.43,38.43,0,0,0,2.85,13.48l-8.5,6.31a28.71,28.71,0,1,' +
+    '1,23.21,8.16,1.85,1.85,0,0,0,.19,3.68h0.19A32.41,32.41,0,0,' +
+    '0,51.36,8.74Zm-4,49.75L45.57,84l-6.85-9.87,2.64-4.92,0.56-1,2.8-5' +
+    'C45.61,61.58,46.49,60,47.31,58.49ZM44,56.63' +
+    'c-0.81,1.5-1.69,3.11-2.56,4.73l-3.3,6.11-0.19.35' +
+    'C37,69.4,36.19,70.93,35.4,72.34l-12-.44Z'],
+
+    ['thatsNoMoon', 'M50,83.4A33.2,33.2,0,1,1,83.2,50.2,33.2,33.2,0,0,' +
+    '1,50,83.4ZM79.3,50.1A29.3,29.3,0,1,0,50.1,79.4,29.3,29.3,0,0,0,' +
+    '79.3,50.1ZM65.4,46.9h9.1A1.9,1.9,0,0,1,76.4,48a1.9,1.9,0,0,' +
+    '1-.3,2.1,2.1,2.1,0,0,1-1.8.7H65.4c-0.1.6-.1,1.2-0.2,1.8' +
+    'A38.5,38.5,0,0,1,57,73.9a2.1,2.1,0,0,1-2.2.9,2,2,0,0,' +
+    '1-1.4-1.6,2.3,2.3,0,0,1,.6-1.7,33.6,33.6,0,0,0,4.9-8.5,34.6,34.6,' +
+    '0,0,0,2.5-11c0-.3,0-0.7,0-1.1H38.9c0.2,1.3.3,2.6,0.5,3.9' +
+    'a34.2,34.2,0,0,0,7.3,17,2,2,0,1,1-3,2.4A37.1,37.1,0,0,1,39.1,67' +
+    'a40.8,40.8,0,0,1-4-15.5V50.8H25.8a2,2,0,0,1-2.2-1.9,1.9,1.9,0,0,' +
+    '1,2.1-2H35l0.3-1.8-0.8-.2a7.5,7.5,0,0,1-5.5-8.5,12.4,12.4,0,0,' +
+    '1,10-10.3,15.5,15.5,0,0,1,3.4,0,0.6,0.6,0,0,0,.7-0.3l0.8-1.1' +
+    'a1.9,1.9,0,0,1,2.5-.4,1.8,1.8,0,0,1,.7,2.4,13.6,13.6,0,0,' +
+    '1-.8,1.4,7.7,7.7,0,0,1,2.1,6.3A11,11,0,0,1,46,40.2a12.6,12.6,0,0,' +
+    '1-6.3,4.5,0.5,0.5,0,0,0-.5.6c0,0.5-.1,1-0.1,1.6H61.5l-0.7-4' +
+    'A33.6,33.6,0,0,0,53.5,27c-0.8-1-.9-1.7-0.4-2.5a1.7,1.7,0,0,' +
+    '1,2.4-.7,3.6,3.6,0,0,1,1.1.9,37,37,0,0,1,8,17.1' +
+    'c0.3,1.5.4,3,.7,4.4v0.7Zm-28.8-4A10.1,10.1,0,0,0,46,34.1' +
+    'a5.3,5.3,0,0,0-6-5.8A10.2,10.2,0,0,0,31.3,36' +
+    'C30.4,39.9,32.8,42.9,36.6,42.9ZM34.9,35.1a4,4,0,0,' +
+    '1,2.7-3.2,2.7,2.7,0,0,1,3.8,2.6,4.4,4.4,0,0,1-2.5,4' +
+    'C36.8,39.5,34.7,38,34.9,35.1Zm2,0.7c0,0.7.3,1,.9,0.9' +
+    'a2.3,2.3,0,0,0,1.5-2.3,0.7,0.7,0,0,0-1.1-.6' +
+    'A2.2,2.2,0,0,0,36.9,35.8Z'],
+
+    ['m_ports', 'M83.2,20L80,16.8a4.8,4.8,0,0,0-6.8,0l-1.4,1.4' +
+    'a4.8,4.8,0,0,0-1.4,3.4l-4.5,5.9a3.8,3.8,0,0,0,.4,5l1.2,1.2' +
+    'a3.8,3.8,0,0,0,5,.4l5.4-3.9,0.5-.6h0a4.8,4.8,0,0,0,3.4-1.4' +
+    'l1.4-1.4A4.8,4.8,0,0,0,83.2,20ZM70.3,31.1H70.1l-1.2-1.2' +
+    'a0.2,0.2,0,0,1,0-.2l3.3-4.4,2.6,2.6Zm10.3-6.9-1.4,1.4' +
+    'a1.1,1.1,0,0,1-1.6,0l-3.2-3.2a1.1,1.1,0,0,1,0-1.6l1.4-1.4' +
+    'a1.1,1.1,0,0,1,1.6,0l3.2,3.2A1.1,1.1,0,0,1,80.6,24.2Z' +
+    'M33.7,67.5l-1.2-1.2a3.8,3.8,0,0,0-5-.4l-5.9,4.5' +
+    'a4.8,4.8,0,0,0-3.4,1.4l-1.4,1.4a4.8,4.8,0,0,0,0,6.8L20,83.2' +
+    'a4.8,4.8,0,0,0,6.8,0l1.4-1.4a4.8,4.8,0,0,0,1.4-3.5l4.5-5.9' +
+    'A3.8,3.8,0,0,0,33.7,67.5ZM25.6,79.3l-1.4,1.4' +
+    'a1.2,1.2,0,0,1-1.6,0l-3.2-3.2a1.1,1.1,0,0,1,0-1.6l1.4-1.4' +
+    'a1.1,1.1,0,0,1,.8-0.3,1.1,1.1,0,0,1,.8.3l3.2,3.2' +
+    'A1.1,1.1,0,0,1,25.6,79.3Zm5.6-9-3.3,4.4-2.5-2.5,4.3-3.3h0.2' +
+    'l1.2,1.2A0.2,0.2,0,0,1,31.1,70.3ZM65.4,61.9a6.2,6.2,0,0,1-8.8,0' +
+    'L37.2,42.5A2.5,2.5,0,1,0,33.7,46l7,7a6.2,6.2,0,0,1,.4,8.3h0' +
+    'l-0.2.2-0.2.2-3.6,3.6a1.8,1.8,0,0,1-2.6-2.6l3.6-3.6h0.1' +
+    'a2.5,2.5,0,0,0-.1-3.4l-7-7a6.2,6.2,0,0,1,8.8-8.8L59.3,59.3' +
+    'a2.5,2.5,0,0,0,3.5-3.5l-6.2-6.2a6.2,6.2,0,0,1,0-8.8l6.1-6.1' +
+    'a1.8,1.8,0,0,1,2.6,2.6l-6.1,6.1a2.5,2.5,0,0,0,0,3.5l6.2,6.2' +
+    'A6.2,6.2,0,0,1,65.4,61.9Z'],
+
+    ['m_switch', m_switch_arrows],
+
+    ['m_roadm', m_switch_arrows + m_octagon],
+
+    ['m_router', 'M29.3,60.9l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V55.2H21.4' +
+    'a1.8,1.8,0,0,1-1.8-1.8V46.7a1.8,1.8,0,0,1,1.8-1.8h3.2' +
+    'a1.8,1.8,0,0,1,0,3.7H23.2v3h6.1a1.8,1.8,0,0,1,1.8,1.8V56' +
+    'l11.1-5.9L31.1,44v2.7a1.8,1.8,0,0,1-3.7,0V40.8' +
+    'a1.8,1.8,0,0,1,2.7-1.6L47,48.5a1.8,1.8,0,0,1,0,3.2l-16.8,9Z' +
+    'M70.6,60.9l-0.9-.2L52.9,51.4a1.8,1.8,0,0,1,0-3.2l16.8-9' +
+    'a1.8,1.8,0,0,1,2.7,1.6v3.9h6.1a1.8,1.8,0,0,1,1.8,1.8v6.7' +
+    'a1.8,1.8,0,0,1-1.8,1.8H75.4a1.8,1.8,0,0,1,0-3.7h1.3v-3H70.6' +
+    'a1.8,1.8,0,0,1-1.8-1.8V43.9L57.7,49.8l11.1,6.1V53.2' +
+    'a1.8,1.8,0,0,1,3.7,0v5.8a1.8,1.8,0,0,1-.9,1.6Z' +
+    'M49.8,82.5h0a1.8,1.8,0,0,1-1.6-1l-9-16.8A1.8,1.8,0,0,1,40.8,62' +
+    'h3.9V55.9a1.8,1.8,0,0,1,1.8-1.8h6.7a1.8,1.8,0,0,1,1.8,1.8v3.2' +
+    'a1.8,1.8,0,0,1-3.7,0V57.8h-3v6.1a1.8,1.8,0,0,1-1.8,1.8H43.9' +
+    'l5.9,11.1,6.1-11.1H53.2a1.8,1.8,0,0,1,0-3.7h5.8' +
+    'a1.8,1.8,0,0,1,1.6,2.7L51.4,81.5A1.8,1.8,0,0,1,49.8,82.5Z' +
+    'M53.3,45.8H46.7A1.8,1.8,0,0,1,44.8,44V40.8a1.8,1.8,0,0,1,3.7,0' +
+    'v1.3h3V36a1.8,1.8,0,0,1,1.8-1.8H56L50.1,23.1,44,34.2h2.7' +
+    'a1.8,1.8,0,0,1,0,3.7H40.8a1.8,1.8,0,0,1-1.6-2.7l9.2-16.8' +
+    'a1.8,1.8,0,0,1,1.6-1h0a1.8,1.8,0,0,1,1.6,1l9,16.8' +
+    'a1.8,1.8,0,0,1-1.6,2.7H55.2V44A1.8,1.8,0,0,1,53.3,45.8Z' +
+    'M50,91.2A41.3,41.3,0,1,1,91.2,50,41.3,41.3,0,0,1,50,91.2Z' +
+    'm0-78.9A37.6,37.6,0,1,0,87.6,50,37.6,37.6,0,0,0,50,12.4Z'],
+
+    ['m_uiAttached', 'M73.6,61.3H26.4a4.9,4.9,0,0,1-4.9-4.9V27.7' +
+    'a4.9,4.9,0,0,1,4.9-4.9H73.6a4.9,4.9,0,0,1,4.9,4.9V56.5' +
+    'A4.9,4.9,0,0,1,73.6,61.3ZM26.4,26.5a1.2,1.2,0,0,0-1.2,1.2' +
+    'V56.5a1.2,1.2,0,0,0,1.2,1.2H73.6a1.2,1.2,0,0,0,1.2-1.2V27.7' +
+    'a1.2,1.2,0,0,0-1.2-1.2H26.4ZM63.5,75.3a1.8,1.8,0,0,1-1.8,1.8' +
+    'H38.4a1.8,1.8,0,0,1,0-3.7h9.8V65.7a1.8,1.8,0,0,1,3.7,0v7.8' +
+    'h9.8A1.8,1.8,0,0,1,63.5,75.3Z'],
+
+    ['m_office', 'M31.7,28.6h-6a1.8,1.8,0,1,1,0-3.7h6A1.8,1.8,0,0,' +
+    '1,31.7,28.6ZM45,28.6h-6a1.8,1.8,0,0,1,0-3.7h6A1.8,1.8,0,1,' +
+    '1,45,28.6ZM31.7,38.3h-6a1.8,1.8,0,1,1,0-3.7h6A1.8,1.8,0,0,' +
+    '1,31.7,38.3ZM45,38.3h-6a1.8,1.8,0,0,1,0-3.7h6A1.8,1.8,0,1,' +
+    '1,45,38.3ZM31.7,48.1h-6a1.8,1.8,0,1,1,0-3.7h6A1.8,1.8,0,0,' +
+    '1,31.7,48.1ZM45,48.1h-6a1.8,1.8,0,0,1,0-3.7h6A1.8,1.8,0,1,' +
+    '1,45,48.1ZM63.7,51.1h-6a1.8,1.8,0,1,1,0-3.7h6A1.8,1.8,0,0,' +
+    '1,63.7,51.1ZM77,51.1H71a1.8,1.8,0,0,1,0-3.7h6A1.8,1.8,0,0,' +
+    '1,77,51.1ZM63.7,60.9h-6a1.8,1.8,0,1,1,0-3.7h6A1.8,1.8,0,0,' +
+    '1,63.7,60.9ZM77,60.9H71a1.8,1.8,0,0,1,0-3.7h6A1.8,1.8,0,0,' +
+    '1,77,60.9ZM45,57.9h-6a1.8,1.8,0,0,1,0-3.7h6A1.8,1.8,0,1,1,' +
+    '45,57.9ZM45,67.6h-6a1.8,1.8,0,0,1,0-3.7h6A1.8,1.8,0,1,1,45,67.6Z' +
+    'M82.3,37.9H52.6V18a1.8,1.8,0,0,0-1.8-1.8H19.2A1.8,1.8,0,0,' +
+    '0,17.3,18V50.1a1.8,1.8,0,0,0,3.7,0V19.8H48.9V80.2H27.2V75.4' +
+    'c4-1.2,6.5-3.3,7.3-6.3,2-7.2-6.8-16.2-7.8-17.2a1.9,1.9,0,0,' +
+    '0-2.6,0c-1,1-9.9,10-7.9,17.2,0.8,3,3.3,5.1,7.3,6.3v4.7H21V77.9' +
+    'a1.8,1.8,0,0,0-3.7,0V82a1.8,1.8,0,0,0,1.9,1.8H82.3A1.8,1.8,0,0,' +
+    '0,84.1,82V39.7A1.8,1.8,0,0,0,82.3,37.9ZM25.4,62.5a1.9,1.9,0,0,' +
+    '0-1.9,1.8v7.2c-2.1-.8-3.4-1.9-3.8-3.4-1-3.7,3-9.2,5.6-12.2,2.6,' +
+    '3,6.6,8.5,5.6,12.2-0.4,1.5-1.7,2.6-3.8,3.4V64.4A1.8,1.8,0,0,' +
+    '0,25.4,62.5ZM71.5,80.2H63.3V70h8.2V80.2Zm8.9,0H75.2V68.1' +
+    'a1.8,1.8,0,0,0-1.8-1.8H61.5a1.8,1.8,0,0,0-1.8,1.8V80.2H52.6' +
+    'V41.6H80.4V80.2Z'],
+
+    ['m_home', 'M79.2,52.2a1.7,1.7,0,0,1-2.3,0l-5.8-5.8V77a1.7,1.7,0,0,' +
+    '1-1.7,1.7H30.6A1.7,1.7,0,0,1,28.9,77V52.1a1.7,1.7,0,1,1,3.3,0' +
+    'V75.3H67.8V43.8a1.7,1.7,0,0,1,.1-0.7L50.1,25.4l-26.9,27' +
+    'a1.7,1.7,0,0,1-1.2.5A1.7,1.7,0,0,1,20.8,50L48.9,21.8' +
+    'a1.7,1.7,0,0,1,2.4,0l28,28A1.7,1.7,0,0,1,79.2,52.2ZM57.1,61.6' +
+    'H42.9a1.7,1.7,0,0,1-1.7-1.7V45.6A1.7,1.7,0,0,1,42.9,44H57.1' +
+    'a1.7,1.7,0,0,1,1.7,1.7V59.9A1.7,1.7,0,0,1,57.1,61.6ZM44.5,58.2' +
+    'H55.5V47.3H44.5V58.2Z'],
+
+    ['m_summary', 'M73.9,79.9H26.1A4.9,4.9,0,0,1,21.2,75V25' +
+    'a4.9,4.9,0,0,1,4.9-4.9H73.9A4.9,4.9,0,0,1,78.8,25V75' +
+    'A4.9,4.9,0,0,1,73.9,79.9Zm-47.8-56A1.2,1.2,0,0,0,24.9,25V75' +
+    'a1.2,1.2,0,0,0,1.2,1.2H73.9A1.2,1.2,0,0,0,75.1,75V25' +
+    'a1.2,1.2,0,0,0-1.2-1.2H26.1ZM42.4,46.7h-8a4.9,4.9,0,0,1-4.9-4.9' +
+    'v-8A4.9,4.9,0,0,1,34.4,29h8a4.9,4.9,0,0,1,4.9,4.9v8' +
+    'A4.9,4.9,0,0,1,42.4,46.7Zm-8-14.1a1.2,1.2,0,0,0-1.2,1.2v8' +
+    'A1.2,1.2,0,0,0,34.4,43h8a1.2,1.2,0,0,0,1.2-1.2v-8' +
+    'a1.2,1.2,0,0,0-1.2-1.2h-8ZM68.7,57.3H31.3a1.8,1.8,0,0,1,0-3.7' +
+    'H68.7A1.8,1.8,0,0,1,68.7,57.3ZM68.7,66.5H31.3a1.8,1.8,0,0,1,0-3.7' +
+    'H68.7A1.8,1.8,0,0,1,68.7,66.5Z'],
+
+    ['m_details', 'M68.7,33.2H31.3a1.8,1.8,0,0,1,0-3.7H68.7' +
+    'A1.8,1.8,0,1,1,68.7,33.2ZM73.9,17.5H26.1a4.9,4.9,0,0,0-4.9,4.9' +
+    'V72.4a4.9,4.9,0,0,0,4.9,4.9h8.2l-4.1,6.6a1.9,1.9,0,0,0,.6,2.5' +
+    'l1,0.3a1.8,1.8,0,0,0,1.6-.9L46.1,64.9a15.1,15.1,0,0,' +
+    '0,4,.5,15.6,15.6,0,0,0,3.7-.4A15.9,15.9,0,0,0,57,63.9H68.7' +
+    'a1.8,1.8,0,0,0,0-3.7H61.9a16.6,16.6,0,0,0,1.6-2.2,15.5,15.5,0,0,' +
+    '0,2.2-9.4h2.9a1.8,1.8,0,0,0,0-3.7H65a15.7,15.7,0,0,0-29.8,0' +
+    'H31.3a1.8,1.8,0,1,0,0,3.7h3.2a15.7,15.7,0,0,0,.4,4.9,15.5,15.5,' +
+    '0,0,0,3.5,6.7H31.3a1.8,1.8,0,1,0,0,3.7H42.4l-6,9.8H26.1' +
+    'a1.2,1.2,0,0,1-1.2-1.2V22.4a1.2,1.2,0,0,1,1.2-1.2H73.9' +
+    'a1.2,1.2,0,0,1,1.2,1.2V72.4a1.2,1.2,0,0,1-1.2,1.2H44.5' +
+    'a1.8,1.8,0,0,0,0,3.7H73.9a4.9,4.9,0,0,0,4.9-4.9V22.4' +
+    'A4.9,4.9,0,0,0,73.9,17.5ZM38.5,52.6A12,12,0,0,1,50.2,37.8,12,12,' +
+    '0,0,1,61.8,47a10.7,10.7,0,0,1,.3,1.6,12,12,0,0,1-5.9,11.6,12,12,' +
+    '0,0,1-12,0L43.9,60A12,12,0,0,1,38.5,52.6Z'],
+
+    ['m_oblique', 'M82.1,30.5a1.8,1.8,0,0,0-1.7-1.1H30.2' +
+    'a1.8,1.8,0,0,0-1.4.7L18.2,42.8a1.8,1.8,0,0,0,1.4,3H40.9V62.2' +
+    'a1.8,1.8,0,1,0,3.7,0V45.8H55.4V62.2a1.9,1.9,0,1,0,3.7,0V45.8' +
+    'H69.8a1.9,1.9,0,0,0,1.4-.7L81.8,32.5A1.8,1.8,0,0,0,82.1,30.5Z' +
+    'M69,42.2H23.5l7.5-9H76.5ZM69.8,70.5H19.6a1.8,1.8,0,0,1-1.4-3' +
+    'L28.7,54.8a1.8,1.8,0,0,1,1.4-.7h7a1.8,1.8,0,0,1,0,3.7H31l-7.5,9' +
+    'H69l7.5-9H62.4a1.8,1.8,0,0,1,0-3.7H80.4a1.8,1.8,0,0,1,1.4,3' +
+    'L71.3,69.9A1.8,1.8,0,0,1,69.8,70.5ZM52.3,57.8H47.6' +
+    'a1.8,1.8,0,0,1,0-3.7h4.8A1.8,1.8,0,0,1,52.3,57.8Z'],
+
+    ['m_filters', 'M69.9,45.8H19.5a1.9,1.9,0,0,1-1.4-3L28.7,30' +
+    'a1.8,1.8,0,0,1,1.4-.7H80.5a1.8,1.8,0,0,1,1.4,3L71.3,45.1' +
+    'A1.8,1.8,0,0,1,69.9,45.8ZM23.5,42.1H69l7.5-9H31ZM69.9,58.2' +
+    'H19.5a1.8,1.8,0,0,1-1.4-3l6.1-7.3a1.8,1.8,0,0,1,2.8,2.4l-3.6,4.3' +
+    'H69l7.6-9.1a1.8,1.8,0,0,1,.5-3.6h3.4a1.8,1.8,0,0,1,1.4,3' +
+    'L71.3,57.5A1.8,1.8,0,0,1,69.9,58.2ZM69.9,70.7H19.5' +
+    'a1.9,1.9,0,0,1-1.4-3l5.6-6.7a1.8,1.8,0,0,1,2.8,2.4L23.5,67H69' +
+    'l7.5-9H76.3a1.8,1.8,0,0,1,0-3.7h4.1a1.9,1.9,0,0,1,1.4,3L71.3,70' +
+    'A1.8,1.8,0,0,1,69.9,70.7Z'],
+
+    ['m_cycleLabels', 'M78.5,74.7a34.2,34.2,0,0,1-47.7,6.8l-0.3-.3' +
+    'L30.4,81V80.8a1.8,1.8,0,0,1,.8-2.5l0.7-.3-5.2-3.4v6.2l0.7-.3' +
+    'a1.8,1.8,0,1,1,1.7,3.3l-3.4,1.8-0.9.2-1-.3a1.9,1.9,0,0,1-.9-1.6' +
+    'V71.1a1.8,1.8,0,0,1,2.8-1.5l10.7,7a1.9,1.9,0,0,1,.8,1.6,1.9,1.9,' +
+    '0,0,1-1,1.5l-0.7.4a30.5,30.5,0,0,0,40.1-7.7A1.8,1.8,0,0,' +
+    '1,78.5,74.7ZM30.8,25.7L29.5,38.4a1.8,1.8,0,0,1-1.1,1.5l-0.8.2' +
+    'a1.9,1.9,0,0,1-1.1-.3l-1.7-1.2a30.1,30.1,0,0,0-2.7,6,1.9,1.9,0,0,' +
+    '1-1.8,1.3H19.9a1.8,1.8,0,0,1-1.2-2.3A33.9,33.9,0,0,1,22.8,35' +
+    'a1.8,1.8,0,0,1,1.6-.8,1.9,1.9,0,0,1,1.2.3L26.2,35l0.7-6.2-5.5,2.8' +
+    'L21.9,32a1.8,1.8,0,1,1-2.1,3l-3.1-2.2a1.8,1.8,0,0,1,.2-3.2' +
+    'l11.3-5.8A1.8,1.8,0,0,1,30.8,25.7ZM85.2,30.8L84.7,43.4' +
+    'a1.8,1.8,0,0,1-1,1.6l-0.8.2a1.8,1.8,0,0,1-1.1-.4L71.4,37.4' +
+    'a1.8,1.8,0,0,1,.3-3.2l1.9-.9a30.5,30.5,0,0,0-3.9-5.3,1.8,1.8,0,0,' +
+    '1,2.7-2.5,34.3,34.3,0,0,1,5.3,7.7,2,2,0,0,1-.9,2.6' +
+    'l-0.7.3,5.1,3.6,0.3-6.2-0.7.3a1.8,1.8,0,0,1-1.6-3.3L82.6,29' +
+    'a1.8,1.8,0,0,1,1.8.1A1.9,1.9,0,0,1,85.2,30.8ZM62.4,32.2H40.2' +
+    'a4.9,4.9,0,0,1-4.9-4.9V16.6a4.9,4.9,0,0,1,4.9-4.9H62.4' +
+    'a4.9,4.9,0,0,1,4.9,4.9V27.3A4.9,4.9,0,0,1,62.4,32.2ZM40.2,15.4' +
+    'A1.2,1.2,0,0,0,39,16.6V27.3a1.2,1.2,0,0,0,1.2,1.2H62.4' +
+    'a1.2,1.2,0,0,0,1.2-1.2V16.6a1.2,1.2,0,0,0-1.2-1.2H40.2Z' +
+    'M88.4,68.1H66.2a4.9,4.9,0,0,1-4.9-4.9V52.6a4.9,4.9,0,0,1,4.9-4.9' +
+    'H88.4a4.9,4.9,0,0,1,4.9,4.9V63.2A4.9,4.9,0,0,1,88.4,68.1Z' +
+    'M66.2,51.4A1.2,1.2,0,0,0,65,52.6V63.2a1.2,1.2,0,0,0,1.2,1.2H88.4' +
+    'a1.2,1.2,0,0,0,1.2-1.2V52.6a1.2,1.2,0,0,0-1.2-1.2H66.2Z' +
+    'M33.8,68.1H11.6a4.9,4.9,0,0,1-4.9-4.9V52.6a4.9,4.9,0,0,1,4.9-4.9' +
+    'H33.8a4.9,4.9,0,0,1,4.9,4.9V63.2A4.9,4.9,0,0,1,33.8,68.1Z' +
+    'M11.6,51.4a1.2,1.2,0,0,0-1.2,1.2V63.2a1.2,1.2,0,0,0,1.2,1.2' +
+    'H33.8A1.2,1.2,0,0,0,35,63.2V52.6a1.2,1.2,0,0,0-1.2-1.2H11.6Z'],
+
+    ['m_cycleGridDisplay', 'M 78.5,74.7 A 34.2,34.2 0 0 1 30.8,81.5 ' +
+    'L 30.5,81.2 30.4,81 v -0.2 a 1.8,1.8 0 0 1 0.8,-2.5 L 31.9,78 26.7,74.6 ' +
+    'v 6.2 l 0.7,-0.3 a 1.8560711,1.8560711 0 1 1 1.7,3.3 ' +
+    'l -3.4,1.8 -0.9,0.2 -1,-0.3 A 1.9,1.9 0 0 1 22.9,83.9 V 71.1 ' +
+    'a 1.8,1.8 0 0 1 2.8,-1.5 l 10.7,7 a 1.9,1.9 0 0 1 0.8,1.6 1.9,1.9 0 0 1 -1,1.5 ' +
+    'l -0.7,0.4 a 30.5,30.5 0 0 0 40.1,-7.7 1.8506756,1.8506756 0 0 1 2.9,2.3 z ' +
+    'M 18.500163,7.4998005 V 20.50004 H 2.5001058 v 2.999817 H 18.500163 V 50.499758 ' +
+    'H 2.5001058 v 3.000335 H 18.500163 v 13.999663 h 2.999816 V 53.500093 h 26.999902 ' +
+    'v 13.999663 h 3.000333 V 53.500093 h 26.999903 v 13.999663 h 2.999817 V 53.500093 ' +
+    'H 97.499992 V 50.499758 H 81.499934 V 23.499857 H 97.499992 V 20.50004 H 81.499934 ' +
+    'V 7.4998005 H 78.500117 V 20.50004 H 51.500214 V 7.4998005 H 48.499881 V 20.50004 ' +
+    'H 21.499979 V 7.4998005 Z M 21.499979,23.499857 H 48.499881 V 50.499758 H 21.499979 ' +
+    'Z m 30.000235,0 H 78.500117 V 50.499758 H 51.500214 Z'],
+
+    ['m_prev', 'M59.8,72l-0.9-.2L21.8,51.3a1.8,1.8,0,0,1,0-3.2L58.9,28.2' +
+    'a1.8,1.8,0,0,1,2.7,1.6V40.7H77.3a1.8,1.8,0,0,1,1.8,1.8V57.3' +
+    'a1.8,1.8,0,0,1-1.8,1.8h-7a1.8,1.8,0,1,1,0-3.7h5.2v-11H59.8' +
+    'A1.8,1.8,0,0,1,58,42.6V33L26.6,49.7,58,67V57.3' +
+    'a1.8,1.8,0,0,1,3.7,0V70.1a1.8,1.8,0,0,1-.9,1.6Z'],
+
+    ['m_next', 'M40.2,72l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V57.3' +
+    'a1.8,1.8,0,0,1,3.7,0V67L73.5,49.7,42,32.9v9.6' +
+    'a1.8,1.8,0,0,1-1.8,1.8H24.5v11h5.2a1.8,1.8,0,1,1,0,3.7h-7' +
+    'a1.8,1.8,0,0,1-1.8-1.8V42.6a1.8,1.8,0,0,1,1.8-1.8H38.3V29.9' +
+    'a1.8,1.8,0,0,1,2.7-1.6L78.2,48.1a1.8,1.8,0,0,1,0,3.2L41.1,71.8Z'],
+
+    ['m_flows', 'M50.1,26.2a7.4,7.4,0,1,1,7.4-7.4A7.4,7.4,0,0,1,50.1,26.2Z' +
+    'm0-11.2a3.8,3.8,0,1,0,3.8,3.8A3.8,3.8,0,0,0,50.1,15Z' +
+    'M50.1,88.6a7.4,7.4,0,1,1,7.4-7.4A7.4,7.4,0,0,1,50.1,88.6Z' +
+    'm0-11.2a3.8,3.8,0,1,0,3.8,3.8A3.8,3.8,0,0,0,50.1,77.4Z' +
+    'M72,50.1a1.8,1.8,0,0,1-1.8,1.8H58.1V55a3,3,0,0,1-3,3H51.9V70.2' +
+    'a1.8,1.8,0,0,1-3.6,0V58.1h-3a3,3,0,0,1-3-3V51.9H29.8' +
+    'a1.8,1.8,0,1,1,0-3.6H42.3v-3a3,3,0,0,1,3-3h3a1.7,1.7,0,0,1,0-.3' +
+    'V29.8a1.8,1.8,0,0,1,3.6,0V41.9a1.7,1.7,0,0,1,0,.3h3.2' +
+    'a3,3,0,0,1,3,3v3H70.2A1.8,1.8,0,0,1,72,50.1ZM18.8,57.5' +
+    'a7.4,7.4,0,1,1,7.4-7.4A7.4,7.4,0,0,1,18.8,57.5Zm0-11.2' +
+    'a3.8,3.8,0,1,0,3.8,3.8A3.8,3.8,0,0,0,18.8,46.3ZM81.2,57.5' +
+    'a7.4,7.4,0,1,1,7.4-7.4A7.4,7.4,0,0,1,81.2,57.5Zm0-11.2' +
+    'A3.8,3.8,0,1,0,85,50.1,3.8,3.8,0,0,0,81.2,46.3Z'],
+
+    ['m_allTraffic', m_diamond + m_trafficArrows],
+
+    ['m_xMark', 'M76.8,73.7a2.2,2.2,0,0,1-3.1,3.1L50,53.1,26.3,76.7' +
+    'a2.2,2.2,0,1,1-3.1-3.1L46.9,50,23.2,26.3a2.2,2.2,0,0,1,3.1-3.1' +
+    'L50,46.9,73.7,23.2a2.2,2.2,0,1,1,3.1,3.1L53.1,50Z'],
+
+    ['m_resetZoom', 'M73.9,76.8H64.2a1.8,1.8,0,1,1,0-3.7h9.7' +
+    'a1.2,1.2,0,0,0,1.2-1.2V62.6a1.8,1.8,0,0,1,3.7,0v9.3' +
+    'A4.9,4.9,0,0,1,73.9,76.8ZM77,33.3a1.8,1.8,0,0,1-1.8-1.8V22' +
+    'a1.2,1.2,0,0,0-1.2-1.2H64.2a1.8,1.8,0,1,1,0-3.7h9.7' +
+    'A4.9,4.9,0,0,1,78.8,22v9.5A1.8,1.8,0,0,1,77,33.3ZM23,33.6' +
+    'a1.8,1.8,0,0,1-1.8-1.8V22a4.9,4.9,0,0,1,4.9-4.9h9.8' +
+    'a1.8,1.8,0,0,1,0,3.7H26.1A1.2,1.2,0,0,0,24.9,22v9.7' +
+    'A1.8,1.8,0,0,1,23,33.6ZM65.3,42.3A15.7,15.7,0,1,0,42.6,59.8' +
+    'L34.4,73.1H26.1a1.2,1.2,0,0,1-1.2-1.2V62.1a1.9,1.9,0,0,0-3.7,0' +
+    'v9.8a4.9,4.9,0,0,0,4.9,4.9h6.1l-2,3.3' +
+    'a1.8,1.8,0,0,0,.6,2.5,1.8,1.8,0,0,0,1,.3,1.8,1.8,0,0,0,1.6-.9' +
+    'L46,61.2a15.7,15.7,0,0,0,7.7.1A15.7,15.7,0,0,0,65.3,42.3Z' +
+    'm-5,9.9A12,12,0,1,1,50.1,34a12,12,0,0,1,11.7,9.2' +
+    'A11.9,11.9,0,0,1,60.3,52.3Z'],
+
+    ['m_eqMaster', 'M63,79.9H37.4a1.8,1.8,0,0,1-1.2-3.2L48.8,65.1' +
+    'a1.8,1.8,0,0,1,2.5,0L64.2,76.7A1.8,1.8,0,0,1,63,79.9Z' +
+    'M42.1,76.2H58.2l-8.1-7.3ZM19.7,65.8a1.8,1.8,0,0,1-.3-3.7' +
+    'l61-11.3a1.8,1.8,0,1,1,.7,3.6L20,65.8H19.7ZM34.2,53.5' +
+    'A16.7,16.7,0,1,1,50.9,36.8,16.7,16.7,0,0,1,34.2,53.5Zm0-29.7' +
+    'a13,13,0,1,0,13,13A13,13,0,0,0,34.2,23.8ZM70.7,45.7' +
+    'a8.6,8.6,0,1,1,8.6-8.6A8.6,8.6,0,0,1,70.7,45.7Zm0-13.6' +
+    'a4.9,4.9,0,1,0,4.9,4.9A4.9,4.9,0,0,0,70.7,32.2Z'],
+
+    ['m_unknown', 'M63.2,20.6H36.8A16.2,16.2,0,0,0,20.6,36.8V63.2' +
+    'A16.2,16.2,0,0,0,36.8,79.4H63.2A16.2,16.2,0,0,0,79.4,63.2V36.8' +
+    'A16.2,16.2,0,0,0,63.2,20.6ZM75.7,63.2A12.5,12.5,0,0,1,63.2,75.7' +
+    'H36.8A12.5,12.5,0,0,1,24.3,63.2V36.8A12.5,12.5,0,0,1,36.8,24.3' +
+    'H63.2A12.5,12.5,0,0,1,75.7,36.8V63.2ZM67.3,64.7' +
+    'a1.8,1.8,0,0,1-2.6,2.6L50,52.6,35.3,67.3a1.8,1.8,0,0,1-2.6-2.6' +
+    'L47.4,50,32.7,35.3a1.8,1.8,0,0,1,2.6-2.6L50,47.4,64.7,32.7' +
+    'a1.8,1.8,0,0,1,2.6,2.6L52.6,50Z'],
+
+    ['m_controller', 'M73.9,20.1H26.1A4.9,4.9,0,0,0,21.2,25V75' +
+    'a4.9,4.9,0,0,0,4.9,4.9H73.9A4.9,4.9,0,0,0,78.8,75V25' +
+    'A4.9,4.9,0,0,0,73.9,20.1ZM75.1,75a1.2,1.2,0,0,1-1.2,1.2H26.1' +
+    'A1.2,1.2,0,0,1,24.9,75V25a1.2,1.2,0,0,1,1.2-1.2H73.9' +
+    'A1.2,1.2,0,0,1,75.1,25V75ZM70.5,63a1.8,1.8,0,0,1-1.9,1.8H38.3' +
+    'v2.4a1.8,1.8,0,0,1-3.7,0V64.9H31.3a1.8,1.8,0,1,1,0-3.7h3.3V58.3' +
+    'a1.8,1.8,0,0,1,3.7,0v2.9H68.7A1.9,1.9,0,0,1,70.5,63ZM70.5,36.6' +
+    'a1.9,1.9,0,0,1-1.9,1.9H44.5v2.3a1.8,1.8,0,0,1-3.7,0V38.5H31.3' +
+    'a1.8,1.8,0,1,1,0-3.7h9.5v-3a1.8,1.8,0,0,1,3.7,0v3H68.7' +
+    'A1.8,1.8,0,0,1,70.5,36.6ZM70.5,49.8a1.9,1.9,0,0,1-1.9,1.8H60.9' +
+    'v2.1a1.8,1.8,0,1,1-3.7,0V51.7H31.3a1.8,1.8,0,1,1,0-3.7H57.2' +
+    'V44.8a1.8,1.8,0,1,1,3.7,0V48h7.8A1.9,1.9,0,0,1,70.5,49.8Z'],
+
+    ['m_virtual', 'M56.6,53.5l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V45.8' +
+    'a1.8,1.8,0,1,1,3.7,0v2.7l11.1-6.1L58.4,36.5v2.7' +
+    'A1.8,1.8,0,0,1,56.6,41H50.5v3h1.3a1.8,1.8,0,0,1,0,3.7H48.6' +
+    'a1.8,1.8,0,0,1-1.8-1.8V39.2a1.8,1.8,0,0,1,1.8-1.8h6.1V33.4' +
+    'a1.8,1.8,0,0,1,2.7-1.6l16.8,9a1.8,1.8,0,0,1,0,3.2L57.5,53.3Z' +
+    'M56.6,77.9l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V70.2a1.8,1.8,0,1,1,3.7,0' +
+    'v2.7l11.1-6.1L58.4,60.9v2.7a1.8,1.8,0,0,1-1.8,1.8H50.5v3h1.3' +
+    'a1.8,1.8,0,0,1,0,3.7H48.6a1.8,1.8,0,0,1-1.8-1.8V63.6' +
+    'a1.8,1.8,0,0,1,1.8-1.8h6.1V57.8a1.8,1.8,0,0,1,2.7-1.6l16.8,9' +
+    'a1.8,1.8,0,0,1,0,3.2L57.5,77.7ZM38.5,64.9l-0.9-.2L20.8,55.4' +
+    'a1.8,1.8,0,0,1,0-3.2l16.8-9a1.8,1.8,0,0,1,2.7,1.6v3.9h6.1' +
+    'a1.8,1.8,0,0,1,1.8,1.8v6.7a1.8,1.8,0,0,1-1.8,1.8H43.2' +
+    'a1.8,1.8,0,1,1,0-3.7h1.3v-3H38.5a1.8,1.8,0,0,1-1.8-1.8V47.9' +
+    'L25.6,53.8l11.1,6.1V57.2a1.8,1.8,0,0,1,3.7,0v5.8' +
+    'a1.8,1.8,0,0,1-.9,1.6ZM38.5,89.3l-0.9-.2L20.8,79.8' +
+    'a1.8,1.8,0,0,1,0-3.2l16.8-9a1.8,1.8,0,0,1,2.7,1.6v3.9h6.1' +
+    'A1.8,1.8,0,0,1,48.3,75v6.7a1.8,1.8,0,0,1-1.8,1.8H43.2' +
+    'a1.8,1.8,0,1,1,0-3.7h1.3v-3H38.5A1.8,1.8,0,0,1,36.6,75V72.3' +
+    'L25.6,78.2l11.1,6.1V81.6a1.8,1.8,0,1,1,3.7,0v5.8' +
+    'a1.8,1.8,0,0,1-.9,1.6ZM60.8,29.1H60.3A1.8,1.8,0,0,1,59,26.8' +
+    'a9.7,9.7,0,0,0-18.7-5.4,1.8,1.8,0,1,1-3.5-1,13.4,13.4,0,0,' +
+    '1,25.8,7.4A1.8,1.8,0,0,1,60.8,29.1ZM77.4,45.7a1.8,1.8,0,0,' +
+    '1-1.3-3.1,9.7,9.7,0,0,0-9.9-16A1.8,1.8,0,1,1,65,23.1,13.4,13.4,' +
+    '0,0,1,78.7,45.1,1.8,1.8,0,0,1,77.4,45.7ZM22.7,45.8a1.8,1.8,0,0,' +
+    '1-1.3-.6A13.4,13.4,0,0,1,39.6,25.6a1.8,1.8,0,1,1-2.4,2.8' +
+    'A9.7,9.7,0,0,0,24,42.6,1.8,1.8,0,0,1,22.7,45.8Z'],
+
+
+    ['m_other', 'M78.9,64.9H21.1A4.9,4.9,0,0,1,16.2,60V24.3a4.9,4.9,0,0,' +
+    '1,4.9-4.9H78.9a4.9,4.9,0,0,1,4.9,4.9V60A4.9,4.9,0,0,1,78.9,64.9Z' +
+    'M21.1,23.1a1.2,1.2,0,0,0-1.2,1.2V60a1.2,1.2,0,0,0,1.2,1.2H78.9' +
+    'A1.2,1.2,0,0,0,80.1,60V24.3a1.2,1.2,0,0,0-1.2-1.2H21.1Z' +
+    'M65.8,78.8a1.9,1.9,0,0,1-1.8,1.8H36.1a1.9,1.9,0,1,1,0-3.7H48.2' +
+    'V70.4a1.8,1.8,0,1,1,3.7,0v6.5H63.9A1.9,1.9,0,0,1,65.8,78.8Z' +
+    'M47.2,49.3V48.1c-0.3-2.3.5-4.9,2.7-7.5s3-4,3-5.9-1.4-3.7-4.1-3.7' +
+    'a7.7,7.7,0,0,0-4.4,1.3l-1-2.7A11.3,11.3,0,0,1,49.5,28' +
+    'c5,0,7.2,3.1,7.2,6.3s-1.6,5-3.7,7.5-2.6,4.1-2.5,6.3v1.1H47.2Z' +
+    'm-1,6a2.5,2.5,0,0,1,2.6-2.7A2.7,2.7,0,1,1,46.3,55.3Z'],
+
+    ['m_endstation', 'M75,49.5H25a1.8,1.8,0,0,1-1.8-1.8V27.1' +
+    'A1.8,1.8,0,0,1,25,25.3H75a1.8,1.8,0,0,1,1.8,1.8V47.7' +
+    'A1.8,1.8,0,0,1,75,49.5ZM26.9,45.8H73.1V28.9H26.9V45.8Z' +
+    'M35.5,43.2H30.7a1.8,1.8,0,1,1,0-3.7h4.8A1.8,1.8,0,1,1,35.5,43.2Z' +
+    'M72.1,72.9a1.8,1.8,0,0,1-1.8,1.8H29.7a1.8,1.8,0,1,1,0-3.7' +
+    'H48.2V53.5a1.8,1.8,0,1,1,3.7,0V71.1H70.3A1.9,1.9,0,0,1,72.1,72.9Z'],
+
+    ['m_bgpSpeaker', 'M59.2,49.6l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V43.3' +
+    'a1.8,1.8,0,1,1,3.7,0v1.3l7-3.9L61,37v1.3a1.8,1.8,0,0,1-1.8,1.8' +
+    'H47.5a1.8,1.8,0,1,1,0-3.7h9.8V33.9A1.8,1.8,0,0,1,60,32.3' +
+    'l12.8,6.8a1.8,1.8,0,0,1,0,3.2l-12.8,7Z' +
+    'M40,58.8l-0.9-.2-12.8-7a1.8,1.8,0,0,1,0-3.2l12.8-6.8' +
+    'a1.8,1.8,0,0,1,2.7,1.6v2.5h9.8a1.8,1.8,0,0,1,0,3.7H40' +
+    'a1.8,1.8,0,0,1-1.8-1.8V46.2l-7,3.8,7,3.9V52.5' +
+    'a1.8,1.8,0,0,1,3.7,0v4.4a1.8,1.8,0,0,1-.9,1.6Z' +
+    'M83.6,45.8c0,13.8-15.1,25-33.6,25a43.8,43.8,0,0,1-6.1-.4' +
+    'c-2.5,2.7-7.2,6.1-15.6,8.7H27.8a1.8,1.8,0,0,1-1.2-3.2' +
+    's5.4-5,5.8-10.2a1.8,1.8,0,0,1,2-1.7A1.8,1.8,0,0,1,36,66' +
+    'a16.2,16.2,0,0,1-2.5,7.2,24.4,24.4,0,0,0,8.3-5.9L42,67l0.2-.2' +
+    'h0l0.6-.2h0.6a41.4,41.4,0,0,0,6.5.5c16.5,0,29.9-9.5,29.9-21.3' +
+    'S66.5,24.5,50,24.5,20.1,34.1,20.1,45.8c0,5.4,2.9,10.6,8.2,14.6' +
+    'a1.8,1.8,0,0,1-2.2,2.9' +
+    'c-6.2-4.7-9.7-10.9-9.7-17.5,0-13.8,15.1-25,33.6-25' +
+    'S83.6,32,83.6,45.8Z'],
+
+    ['m_otn', m_otn_base],
+
+    ['m_roadm_otn', m_otn_base + m_octagon],
+
+    ['m_fiberSwitch', 'M47.5,37.2a1.8,1.8,0,0,1-1.8-1.8V25.6H43.4' +
+    'a1.8,1.8,0,0,1-1.6-2.7l6.4-12a1.8,1.8,0,0,1,1.6-1h0' +
+    'a1.8,1.8,0,0,1,1.6,1l6.6,12a1.8,1.8,0,0,1-1.6,2.7H53.2' +
+    'a1.8,1.8,0,0,1,0-3.7h0.1l-3.5-6.3L46.5,22h1a1.8,1.8,0,0,1,1.8,1.8' +
+    'V35.3A1.8,1.8,0,0,1,47.5,37.2Z' +
+    'M75.9,46.9H75.8a1.8,1.8,0,0,1-1.5-1l-1.8-3.4' +
+    'a1.8,1.8,0,1,1,3.2-1.7l0.4,0.8,4.2-6-7.1-.3,0.5,0.9' +
+    'a1.8,1.8,0,0,1-.8,2.5l-10.4,5a1.8,1.8,0,0,1-1.6-3.3l8.7-4.2-1-2' +
+    'a1.8,1.8,0,0,1,.1-1.8,1.8,1.8,0,0,1,1.6-.8l13.6,0.5' +
+    'A1.8,1.8,0,0,1,85.3,35L77.4,46.1A1.8,1.8,0,0,1,75.9,46.9Z' +
+    'M81.2,72.5H81.1l-13.6-.8A1.8,1.8,0,0,1,66,69l1.7-3.1' +
+    'a1.8,1.8,0,1,1,3.2,1.8l-0.3.5,7.3,0.4-3.5-6.2-0.5.9' +
+    'a1.8,1.8,0,0,1-2.5.6l-9.8-6.1a1.8,1.8,0,0,1,1.9-3.1' +
+    'l8.2,5.1,1.2-1.9a1.9,1.9,0,0,1,1.6-.9,1.8,1.8,0,0,1,1.6.9' +
+    'l6.8,11.8A1.8,1.8,0,0,1,81.2,72.5Z' +
+    'M48.8,89.7a1.8,1.8,0,0,1-1.6-1l-6.6-12a1.8,1.8,0,0,1,1.6-2.7h3.3' +
+    'a1.8,1.8,0,0,1,0,3.7H45.3L48.7,84l3.4-6.3h-1' +
+    'a1.8,1.8,0,0,1-1.8-1.8V64.4a1.8,1.8,0,0,1,1.8-1.8h0' +
+    'a1.8,1.8,0,0,1,1.8,1.8v9.7h2.3a1.8,1.8,0,0,1,1.6,2.7l-6.4,12' +
+    'a1.8,1.8,0,0,1-1.6,1h0Z' +
+    'M16.9,71.3a1.8,1.8,0,0,1-1.6-2.7l6.7-12' +
+    'a1.8,1.8,0,0,1,1.6-.9,1.9,1.9,0,0,1,1.6.9l1.7,2.8' +
+    'a1.8,1.8,0,1,1-3.1,1.9H23.7l-3.5,6.3,7.1-.5L26.8,66' +
+    'a1.8,1.8,0,0,1,.6-2.5l9.8-6.1a1.8,1.8,0,0,1,1.9,3.1l-8.2,5.1' +
+    'L32,67.6a1.8,1.8,0,0,1-1.4,2.8l-13.5.9H16.9Z' +
+    'M35.8,46.9L35,46.7l-8.7-4.3-1,2a1.8,1.8,0,0,1-3.1.3l-8-11' +
+    'a1.8,1.8,0,0,1,1.4-2.9l13.6-.7' +
+    'a1.8,1.8,0,0,1,1.6.8,1.8,1.8,0,0,1,.1,1.8l-1.6,3' +
+    'a1.8,1.8,0,1,1-3.3-1.7h0.1l-7.1.4,4.2,5.8,0.5-.9' +
+    'a1.8,1.8,0,0,1,2.5-.8l10.4,5.1A1.8,1.8,0,0,1,35.8,46.9Z' +
+    'M60.5,49.9a11.3,11.3,0,1,1-3.3-8A11.2,11.2,0,0,1,60.5,49.9Z'],
+
+    ['m_microwave', 'M63.5,38.9a2.8,2.8,0,0,0-2.1,1H49.6V15.1' +
+    'a1.8,1.8,0,0,0-.5-1.3,1.8,1.8,0,0,0-1.3-.5h0' +
+    'a28.4,28.4,0,0,0-15.7,52V82.5H22.8a1.8,1.8,0,0,0,0,3.7H45' +
+    'a1.8,1.8,0,1,0,0-3.7H35.8V67.4a28.4,28.4,0,0,0,12,2.8h0' +
+    'a1.9,1.9,0,0,0,1.8-1.8V43.6H61.3a2.8,2.8,0,0,0,2.1,1' +
+    'A2.8,2.8,0,1,0,63.5,38.9ZM46,66.4a24.8,24.8,0,0,1,0-49.4V66.4Z' +
+    'M41.7,64H41.2a24.3,24.3,0,0,1-12.9-9.7L28,53.9' +
+    'a1.8,1.8,0,0,1,3.1-2l0.2,0.3a20.3,20.3,0,0,0,10.9,8.2' +
+    'A1.8,1.8,0,0,1,41.7,64Z' +
+    'M66.4,53.1a1.8,1.8,0,0,1-.5-3.6A8.1,8.1,0,0,0,66,34' +
+    'a1.8,1.8,0,0,1,1.1-3.5A11.8,11.8,0,0,1,67,53H66.4Z' +
+    'M66.5,58.2a1.8,1.8,0,0,1-.4-3.6,13.1,13.1,0,0,0,0-25.7,' +
+    '1.8,1.8,0,1,1,.7-3.6,16.8,16.8,0,0,1,0,32.9H66.5Z'],
+
+    ['m_relatedIntents', 'M34.5,73.1A7.5,7.5,0,1,1,42,65.6,7.5,7.5,' +
+    '0,0,1,34.5,73.1Zm0-11.3a3.8,3.8,0,1,0,3.8,3.8' +
+    'A3.8,3.8,0,0,0,34.5,61.8Z' +
+    'M56.7,34.3a1.8,1.8,0,0,1-1.8,1.9H42.6v3.1a3.1,3.1,0,0,1-3,3' +
+    'H36.4V54.5a1.9,1.9,0,0,1-3.7,0V42.3h-3a3.1,3.1,0,0,1-3-3' +
+    'V29.5a3,3,0,0,1,3-3h9.8a3,3,0,0,1,3,3v3H54.8' +
+    'A1.8,1.8,0,0,1,56.7,34.3Z' +
+    'M66,41.8a7.5,7.5,0,1,1,7.5-7.5A7.5,7.5,0,0,1,66,41.8Z' +
+    'm0-11.3a3.8,3.8,0,1,0,3.8,3.8A3.8,3.8,0,0,0,66,30.5Z'],
+
+    ['m_intentTraffic', 'M28.9,77.6H28.5a1.8,1.8,0,0,1-1.4-2.2L39,23.3' +
+    'a1.8,1.8,0,0,1,3.6.8l-12,52.1A1.8,1.8,0,0,1,28.9,77.6Z' +
+    'M71.1,77.6a1.8,1.8,0,0,1-1.8-1.4l-12-52.1a1.8,1.8,0,0,1,3.6-.8' +
+    'l12,52.1a1.8,1.8,0,0,1-1.4,2.2H71.1Z' +
+    'M49.9,26.2A1.8,1.8,0,0,1,48,24.4V23.7a1.8,1.8,0,0,1,3.7,0' +
+    'v0.7A1.8,1.8,0,0,1,49.9,26.2Z' +
+    'M49.9,43.6A1.8,1.8,0,0,1,48,41.8V40.2a1.8,1.8,0,0,1,3.7,0' +
+    'v1.6A1.8,1.8,0,0,1,49.9,43.6Zm0-8.7A1.8,1.8,0,0,1,48,33.1' +
+    'V31.5a1.8,1.8,0,0,1,3.7,0v1.6A1.8,1.8,0,0,1,49.9,34.9Z' +
+    'M49.9,69.8A1.8,1.8,0,0,1,48,67.9V66.3a1.8,1.8,0,0,1,3.7,0' +
+    'v1.6A1.8,1.8,0,0,1,49.9,69.8Zm0-8.7A1.8,1.8,0,0,1,48,59.2' +
+    'V57.6a1.8,1.8,0,0,1,3.7,0v1.6A1.8,1.8,0,0,1,49.9,61.1Zm0-8.7' +
+    'A1.8,1.8,0,0,1,48,50.5V48.9a1.8,1.8,0,0,1,3.7,0v1.6' +
+    'A1.8,1.8,0,0,1,49.9,52.3Z' +
+    'M49.9,77.6A1.8,1.8,0,0,1,48,75.7V75a1.8,1.8,0,0,1,3.7,0v0.7' +
+    'A1.8,1.8,0,0,1,49.9,77.6Z'],
+
+    ['m_firewall', 'M75.3,88.8H65.6a4.9,4.9,0,0,1-4.9-4.9V79.1' +
+    'a4.9,4.9,0,0,1,4.9-4.9h9.7a4.9,4.9,0,0,1,4.9,4.9v4.8' +
+    'A4.9,4.9,0,0,1,75.3,88.8ZM65.6,77.9a1.2,1.2,0,0,0-1.2,1.2v4.8' +
+    'a1.2,1.2,0,0,0,1.2,1.2h9.7a1.2,1.2,0,0,0,1.2-1.2V79.1' +
+    'a1.2,1.2,0,0,0-1.2-1.2H65.6Z' +
+    'M53.9,88.8H24.7a4.9,4.9,0,0,1-4.9-4.9V79.1a4.9,4.9,0,0,1,4.9-4.9' +
+    'H53.9a4.9,4.9,0,0,1,4.9,4.9v4.8A4.9,4.9,0,0,1,53.9,88.8Z' +
+    'M24.7,77.9a1.2,1.2,0,0,0-1.2,1.2v4.8a1.2,1.2,0,0,0,1.2,1.2' +
+    'H53.9a1.2,1.2,0,0,0,1.2-1.2V79.1a1.2,1.2,0,0,0-1.2-1.2H24.7Z' +
+    'M34.4,72.1H24.7a4.9,4.9,0,0,1-4.9-4.9V62.4a4.9,4.9,0,0,1,4.9-4.9' +
+    'h9.7a4.9,4.9,0,0,1,4.9,4.9v4.8A4.9,4.9,0,0,1,34.4,72.1Z' +
+    'M24.7,61.2a1.2,1.2,0,0,0-1.2,1.2v4.8a1.2,1.2,0,0,0,1.2,1.2h9.7' +
+    'a1.2,1.2,0,0,0,1.2-1.2V62.4a1.2,1.2,0,0,0-1.2-1.2H24.7Z' +
+    'M75.3,72.1H46.1a4.9,4.9,0,0,1-4.9-4.9V62.4a4.9,4.9,0,0,1,4.9-4.9' +
+    'H75.3a4.9,4.9,0,0,1,4.9,4.9v4.8A4.9,4.9,0,0,1,75.3,72.1Z' +
+    'M46.1,61.2a1.2,1.2,0,0,0-1.2,1.2v4.8a1.2,1.2,0,0,0,1.2,1.2H75.3' +
+    'a1.2,1.2,0,0,0,1.2-1.2V62.4a1.2,1.2,0,0,0-1.2-1.2H46.1Z' +
+    'M67.7,40.7c-0.2-4.8-3.6-8.8-6.3-12s-3-3.6-3.3-4.8' +
+    'a13.1,13.1,0,0,1,1-9.7,2.2,2.2,0,0,0,.3-1.1,1.9,1.9,0,0,' +
+    '0-.8-1.5,1.8,1.8,0,0,0-1.7-.2c-0.3.1-8.1,3-10.4,14.7-1.4-2.3' +
+    '-3.4-4.6-5.9-5.4a1.8,1.8,0,0,0-2,.6,1.9,1.9,0,0,0-.2,2.1,6.8,6.8,' +
+    '0,0,1-.6,7.1c-1.4,1.8-5.9,8.3-4.1,14.9,1.2,4.1,4.6,7.3,10.2,9.4' +
+    'h1.6l0.2-.2h0.1l0.2-.3,0.2-.3h0V53.2a1.6,1.6,0,0,0,0-.4,1.7,1.7,' +
+    '0,0,0,0-.3h0V52.2h0V51.8L46,51.6H45.9c-0.3-.4-2.4-3-2.1-5.9' +
+    'a5,5,0,0,1,.7-2.1c1,2,2.7,4.4,5.5,4.6a5.1,5.1,0,0,0,3.9-1.3' +
+    'A8.2,8.2,0,0,0,56,43.5a7.3,7.3,0,0,1,.3,2.6,7.6,7.6,0,0,' +
+    '1-3.1,5.3,1.8,1.8,0,0,0,1.2,3.3h0c0.3,0,6.3-.1,10.1-4.3' +
+    'C66.9,47.9,67.9,44.6,67.7,40.7Zm-5.8,7.1a9,9,0,0,1-2.7,2,9.9,9.9,' +
+    '0,0,0,.9-3.5c0.3-5.1-3.4-9.2-3.5-9.4a1.8,1.8,0,0,0-3.2,1.2' +
+    'c0,1.4-.5,4.8-1.8,5.9a1.4,1.4,0,0,1-1.2.4c-1.7-.1-2.9-3.3-3.3-4.8' +
+    'a1.8,1.8,0,0,0-1.2-1.4,1.8,1.8,0,0,0-1.8.3,9.7,9.7,0,0,' +
+    '0-4,6.8,9.4,9.4,0,0,0,.3,3.1,8.4,8.4,0,0,1-3.1-4.4' +
+    'c-1.4-5,2.6-10.5,3.5-11.7A10,10,0,0,0,42.8,27,27.2,27.2,0,0,' +
+    '1,46,33.7a1.8,1.8,0,0,0,3.6-.6,33.2,33.2,0,0,1,.4-5.4,19.3,19.3,' +
+    '0,0,1,4-9.7,19.1,19.1,0,0,0,.5,6.7c0.5,2.1,2.2,4,4.1,6.3' +
+    's5.3,6.2,5.5,9.8A9.1,9.1,0,0,1,61.9,47.8Z'],
+
+    ['m_balancer', 'M33.4,56.6H26.7a3.1,3.1,0,0,1-3-3V46.9a3.1,3.1,0,0,1,3-3' +
+    'h6.7a3.1,3.1,0,0,1,3,3v6.7A3.1,3.1,0,0,1,33.4,56.6Z' +
+    'M73.3,36.5H66.6a3.1,3.1,0,0,1-3-3V26.7a3.1,3.1,0,0,1,3-3' +
+    'h6.7a3.1,3.1,0,0,1,3,3v6.7A3.1,3.1,0,0,1,73.3,36.5Z' +
+    'M73.3,56.1H66.6a3.1,3.1,0,0,1-3-3V46.4a3.1,3.1,0,0,1,3-3' +
+    'h6.7a3.1,3.1,0,0,1,3,3v6.7A3.1,3.1,0,0,1,73.3,56.1Z' +
+    'M73.3,76.3H66.6a3.1,3.1,0,0,1-3-3V66.6a3.1,3.1,0,0,1,3-3' +
+    'h6.7a3.1,3.1,0,0,1,3,3v6.7A3.1,3.1,0,0,1,73.3,76.3Z' +
+    'M62.7,70.4a1.9,1.9,0,0,1-1.8,1.5H60.5a15.2,15.2,0,0,1-10.9-9.1' +
+    'c-3.9-8.6-4.7-9.8-10.8-10.4a1.8,1.8,0,0,1-1.6-1.7,0.3,0.3,0,0,1,0-.1' +
+    'h0V50.1h0a1.8,1.8,0,0,1,1.7-2c6.2-.5,7-1.7,10.9-10.4' +
+    'a15.2,15.2,0,0,1,10.7-9.1,1.8,1.8,0,1,1,.8,3.6,11.5,11.5,0,0,0-8.2,7' +
+    'c-2,4.4-3.4,7.3-5.3,9.2H60.8a1.8,1.8,0,1,1,0,3.7h-13' +
+    'c1.8,1.9,3.2,4.8,5.2,9.1a11.5,11.5,0,0,0,8.3,7' +
+    'A1.9,1.9,0,0,1,62.7,70.4Z'],
+
+    ['m_ips', 'M79.4,26.8a0.9,0.9,0,0,0-.1-0.3V26.1l-0.3-.4-0.2-.2' +
+    'H78.6c-1.3-1-11-8-28.3-8H49.7c-18.7,0-28.1,7.8-28.5,8.1' +
+    'a2,2,0,0,0-.7,1.3c-1.1,22.8,6.7,36.8,13.4,44.4S48.3,82.4,49.1,82.7' +
+    'h1.6c0.3-.1,7.9-3.1,15.3-11.4S80.5,49.7,79.4,26.8ZM50,79' +
+    'c-3.9-1.7-27.3-13.4-25.8-51.1,2.3-1.6,10.7-6.8,25.5-6.8h0.5' +
+    'c14.4,0,23.2,5.2,25.5,6.8C77.2,65.6,53.9,77.3,50,79Z' +
+    'M41.7,41.9a1.8,1.8,0,0,1-1.8-1.9V37.3a9.8,9.8,0,0,1,9.8-9.8h0.6' +
+    'a9.8,9.8,0,0,1,9.8,9.8v2.6a1.8,1.8,0,1,1-3.7,0V37.3' +
+    'a6.1,6.1,0,0,0-6.1-6.1H49.6a6.1,6.1,0,0,0-6.1,6.1v2.8' +
+    'A1.8,1.8,0,0,1,41.7,41.9Z' +
+    'M58.6,63.1H41.3a3.7,3.7,0,0,1-3.6-3.7V47.3a3.7,3.7,0,0,1,3.6-3.7' +
+    'H58.6a3.8,3.8,0,0,1,3.6,3.7V59.4A3.7,3.7,0,0,1,58.6,63.1Z'],
+
+    ['m_ids', 'M69.7,41.5c-2.9,3.6-9.1,5-18.6,4.2a18.4,18.4,0,0,0-8.9,1.4' +
+    'H41.5a1.8,1.8,0,0,1-.7-3.5A22.2,22.2,0,0,1,51.5,42' +
+    'c7.4,0.6,12.5-.2,14.8-2.3L63.2,22.6c-5,2.9-12.5,2.4-15.9,1.9' +
+    'a18.7,18.7,0,0,0-12.7,3.4H34.3l9.5,52.4a1.9,1.9,0,0,1-1.5,2.1' +
+    'H42a1.8,1.8,0,0,1-1.8-1.5L29.9,24.3a1.8,1.8,0,1,1,3.6-.6v0.4' +
+    'a22.3,22.3,0,0,1,14.1-3.2C55,21.8,60.9,20.7,63,18' +
+    'a1.8,1.8,0,0,1,1.9-.6,1.8,1.8,0,0,1,1.4,1.5L70.1,40' +
+    'A1.9,1.9,0,0,1,69.7,41.5Z'],
+
+    ['m_olt', 'M83,47.6H64.5a1.8,1.8,0,0,0-1.9,1.9V74.3' +
+    'a1.8,1.8,0,0,0,1.9,1.8H83a1.8,1.8,0,0,0,1.8-1.8V49.5' +
+    'A1.8,1.8,0,0,0,83,47.6ZM81.1,72.5H77.7v-5a1.4,1.4,0,0,0-1.4-1.4' +
+    'H72.1a1.4,1.4,0,0,0-1.4,1.4v5H66.3V51.3H81.1V72.5Z' +
+    'M70.9,56.9H69.5a1.8,1.8,0,0,1,0-3.7h1.3A1.8,1.8,0,1,1,70.9,56.9Z' +
+    'M77.5,56.9H76.1a1.8,1.8,0,0,1,0-3.7h1.3A1.8,1.8,0,1,1,77.5,56.9Z' +
+    'M70.9,62.5H69.5a1.8,1.8,0,0,1,0-3.7h1.3A1.8,1.8,0,1,1,70.9,62.5Z' +
+    'M77.5,62.5H76.1a1.8,1.8,0,0,1,0-3.7h1.3A1.8,1.8,0,1,1,77.5,62.5Z' +
+    'M40.9,56.2H36.1a1.8,1.8,0,1,1,0-3.7h4.8A1.8,1.8,0,0,1,40.9,56.2Z' +
+    'M82.2,25.7V43.9a1.8,1.8,0,0,1-3.7,0V27.5H32.3V59.8H58.4' +
+    'a1.8,1.8,0,0,1,0,3.7h-28a1.9,1.9,0,0,1-1.8-1.9V45.5H17' +
+    'a1.8,1.8,0,0,1,0-3.7H28.6V25.7a1.8,1.8,0,0,1,1.8-1.8h50' +
+    'A1.8,1.8,0,0,1,82.2,25.7Z'],
+
+    ['m_onu', 'M39.8,58.7H35a1.8,1.8,0,1,1,0-3.7h4.9' +
+    'A1.8,1.8,0,1,1,39.8,58.7Z' +
+    'M81.4,28.3V46.5a1.9,1.9,0,0,1-3.7,0V30.2H31.2V62.4H57.4' +
+    'a1.8,1.8,0,1,1,0,3.7H29.3a1.8,1.8,0,0,1-1.8-1.9V48.1H15.9' +
+    'a1.8,1.8,0,1,1,0-3.7H27.5V28.3a1.8,1.8,0,0,1,1.8-1.8H79.5' +
+    'A1.8,1.8,0,0,1,81.4,28.3Z' +
+    'M85.8,60.3L75.1,49.6a1.8,1.8,0,0,0-2.6,0L61.6,60.3' +
+    'a1.8,1.8,0,0,0,0,2.6,1.8,1.8,0,0,0,2.6,0l0.2-.2v8.8' +
+    'a1.8,1.8,0,0,0,1.8,1.8H81.2a1.8,1.8,0,0,0,1.8-1.8V62.7l0.2,0.2' +
+    'A1.8,1.8,0,0,0,85.8,60.3Zm-6.5,9.4H68.1V59.1l5.6-5.6L79.4,59V69.7Z'],
+
+    ['m_swap', 'M62.2,54.7l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V47.1' +
+    'a1.8,1.8,0,1,1,3.7,0v2.6l10.8-6L64,38v2.6a1.8,1.8,0,0,1-1.8,1.8' +
+    'H47.1a1.8,1.8,0,0,1,0-3.7H60.3V34.9a1.8,1.8,0,0,1,2.7-1.6' +
+    'l16.5,8.8a1.8,1.8,0,0,1,0,3.2L63.1,54.5Z' +
+        'M37.4,66.6l-0.9-.2L20,57.2a1.8,1.8,0,0,1,0-3.2l16.5-8.8' +
+    'a1.8,1.8,0,0,1,2.7,1.6v3.8H52.5a1.8,1.8,0,0,1,0,3.7H37.4' +
+    'a1.8,1.8,0,0,1-1.8-1.8V49.9L24.8,55.7l10.8,6V59' +
+    'a1.8,1.8,0,1,1,3.7,0v5.7a1.8,1.8,0,0,1-.9,1.6Z'],
+
+    ['m_shortestGeoPath', 'M49.7,17.5A32.3,32.3,0,1,0,81.9,49.8,32.3,32.3,0,' +
+    '0,0,49.7,17.5Zm0,60.9A28.6,28.6,0,1,1,78.2,49.8,28.6,28.6,0,0,1,' +
+    '49.7,78.4Z M60.9,49a1.5,1.5,0,0,1-.1-0.4,0.8,0.8,0,0,1,0-.3' +
+    'C60.9,48.5,60.9,48.8,60.9,49Z' +
+    'M75.4,48.6a1.8,1.8,0,0,1-1.8,1.8h-9' +
+    'a38.2,38.2,0,0,1-8.3,22.8,1.8,1.8,0,0,1-1.4.7,1.8,1.8,0,0,' +
+    '1-1.1-.4,1.8,1.8,0,0,1-.3-2.6,34.6,34.6,0,0,0,7.5-21.6V49' +
+    'c0-.3,0-0.5,0-0.7a0.1,0.1,0,0,1,0-.1,33.2,33.2,0,0,0-.7-6.1H60' +
+    'a5.1,5.1,0,0,1-3.1-9.1A33,33,0,0,0,52.9,27' +
+    'a1.8,1.8,0,1,1,2.8-2.4,36.2,36.2,0,0,1,4.7,7.2,5.1,5.1,0,0,' +
+    '1,3.1,8.7,38.3,38.3,0,0,1,.9,6.2h9.1A1.8,1.8,0,0,1,75.4,48.6Z' +
+    'M46.5,71a1.8,1.8,0,0,1-1.4,3,1.8,1.8,0,0,1-1.4-.7,36.3,36.3,' +
+    '0,0,1-4.3-6.7H39a5.1,5.1,0,0,1-2.9-9.3,40.3,40.3,0,0,1-.8-6.9H26' +
+    'a1.8,1.8,0,0,1,0-3.7h9.3a38,38,0,0,1,8.3-21.5,1.8,1.8,0,1,1,2.8,' +
+    '2.3,34.5,34.5,0,0,0-7.6,21.8,36,36,0,0,0,.7,7.2,5.1,5.1,0,0,' +
+    '1,4.5,5.1,5,5,0,0,1-1.4,3.5A33.4,33.4,0,0,0,46.5,71Z' +
+    'M44.9,56.9a1.8,1.8,0,0,1-1.1-.4,1.8,1.8,0,0,1-.3-2.6l0.6-.8' +
+    'A1.8,1.8,0,1,1,47,55.3l-0.6.8A1.8,1.8,0,0,1,44.9,56.9Zm3.9-5' +
+    'a1.8,1.8,0,0,1-1.1-.4,1.8,1.8,0,0,1-.3-2.6l0.6-.8' +
+    'a1.8,1.8,0,1,1,2.9,2.3l-0.6.8A1.8,1.8,0,0,1,48.8,51.9Zm3.9-5' +
+    'a1.8,1.8,0,0,1-1.1-.4,1.8,1.8,0,0,1-.3-2.6l0.6-.8' +
+    'a1.8,1.8,0,1,1,2.9,2.3l-0.6.8A1.8,1.8,0,0,1,52.7,46.9Z'],
+
+    ['m_source', 'M71,63.8l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V57.1' +
+    'a1.8,1.8,0,0,1,3.7,0v1.7l8.2-4.5-8.2-4.4v1.7A1.8,1.8,0,0,1,71,53.4' +
+    'H58.2a1.8,1.8,0,0,1,0-3.7H69.2V46.8a1.8,1.8,0,0,1,2.7-1.6l14,7.5' +
+    'a1.8,1.8,0,0,1,0,3.2l-14,7.7Z' +
+    'M32.7,77.7a3.6,3.6,0,0,1-3.1-1.8L16.1,52.1l-0.9-1.6' +
+    'a19.3,19.3,0,0,1-2.1-8.7,19.6,19.6,0,0,1,6.1-14.2' +
+    'A19.5,19.5,0,0,1,52.1,40.5h0a19.5,19.5,0,0,1-2,9.9l-1,1.8' +
+    'L35.8,75.9A3.6,3.6,0,0,1,32.7,77.7Zm0-51.7' +
+    'A15.8,15.8,0,0,0,18.5,48.8l0.8,1.4L32.7,73.9,46,50.3l0.8-1.4' +
+    'a15.9,15.9,0,0,0,1.6-8.1h0A15.8,15.8,0,0,0,33.6,26h-1Z' +
+    'm0,21.1A8.2,8.2,0,1,1,40.8,39,8.2,8.2,0,0,1,32.7,47.1Z' +
+    'm0-12.6A4.5,4.5,0,1,0,37.1,39,4.5,4.5,0,0,0,32.7,34.5Z'],
+
+    ['m_destination', 'M30.3,63.8l-0.9-.3a1.8,1.8,0,0,1-.9-1.6V57.1' +
+    'a1.8,1.8,0,0,1,3.7,0v1.7l8.2-4.5-8.2-4.4v1.7a1.8,1.8,0,0,1-1.8,1.8' +
+    'H17.5a1.8,1.8,0,1,1,0-3.7H28.4V46.8a1.8,1.8,0,0,1,2.7-1.6l14,7.5' +
+    'a1.8,1.8,0,0,1,0,3.2l-14,7.7Z' +
+    'M64.9,77.7a3.6,3.6,0,0,1-3.1-1.8L48.3,52.1l-0.9-1.6' +
+    'a19.3,19.3,0,0,1-2-8.6,19.6,19.6,0,0,1,6.1-14.2' +
+    'A19.5,19.5,0,0,1,84.3,40.5a19.5,19.5,0,0,1-1.9,9.8v0.2l-0.9,1.7' +
+    'L68,75.9A3.6,3.6,0,0,1,64.9,77.7ZM50.7,48.8l0.8,1.4' +
+    'L64.9,73.9,78.2,50.3,79,48.8h0a15.8,15.8,0,0,0,1.6-8' +
+    'a15.8,15.8,0,1,0-29.9,8h0Zm14.1-1.7' +
+    'A8.2,8.2,0,1,1,73,39,8.2,8.2,0,0,1,64.9,47.1Z' +
+    'm0-12.6A4.5,4.5,0,1,0,69.3,39,4.5,4.5,0,0,0,64.9,34.5Z'],
+
+    ['m_topo', 'M31.3,73H21.5a3.1,3.1,0,0,1-3-3V60.1a3.1,3.1,0,0,1,3-3' +
+    'h9.8a3.2,3.2,0,0,1,3,3V70A3.1,3.1,0,0,1,31.3,73Z' +
+    'M78.5,46.7H68.7a3.1,3.1,0,0,1-3-3V33.8a3.1,3.1,0,0,1,3-3h9.8' +
+    'a3.1,3.1,0,0,1,3,3v9.8A3.1,3.1,0,0,1,78.5,46.7Z' +
+    'M40.8,42.9H31a3.1,3.1,0,0,1-3-3V30a3.1,3.1,0,0,1,3-3' +
+    'h9.9a2.9,2.9,0,0,1,2.9,3v9.8A3.1,3.1,0,0,1,40.8,42.9Z' +
+    'M37.4,66.1a1.8,1.8,0,0,1-1.2-3.3L61.1,42a1.8,1.8,0,1,1,2.4,2.8' +
+    'L38.6,65.7A1.8,1.8,0,0,1,37.4,66.1Z' +
+    'M27.4,55.5a1.8,1.8,0,0,1-1.5-3l5.8-7.7a1.8,1.8,0,1,1,2.9,2.2' +
+    'l-5.8,7.7A1.8,1.8,0,0,1,27.4,55.5Z' +
+    'M62.5,39.7H62.2L46.6,36.5a1.8,1.8,0,1,1,.7-3.6L62.9,36' +
+    'A1.8,1.8,0,0,1,62.5,39.7Z'],
+
+    ['m_shortestPath', 'M28.2,31.2H19.5a3,3,0,0,1-3-3V19.5a3,3,0,0,1,3-3' +
+    'h8.6c1.8,0.4,3,1.4,3,3v8.6A3,3,0,0,1,28.2,31.2Z' +
+    'M80.2,83.2H71.6a3,3,0,0,1-3-3V71.5a3,3,0,0,1,3-3h8.6' +
+    'c1.7,0.4,3,1.4,3,3v8.6A3,3,0,0,1,80.2,83.2Z' +
+    'M67.6,67.4a1.8,1.8,0,0,1-1.3.5,1.9,1.9,0,0,1-1.3-.5L54.7,57.1' +
+    'H45.6a3,3,0,0,1-3-3V45.5a2.1,2.1,0,0,1,.1-0.5L32.3,34.7' +
+    'a1.8,1.8,0,0,1,0-2.6,1.9,1.9,0,0,1,2.6,0L45.3,42.5h8.9' +
+    'a3,3,0,0,1,3,3v8.6a0.9,0.9,0,0,1,0,.2L67.6,64.8' +
+    'A1.8,1.8,0,0,1,67.6,67.4Z'],
+
+    ['m_disjointPaths', 'M67.7,59.8h9.4a2.9,2.9,0,0,1,3,3v9.4' +
+    'a3.1,3.1,0,0,1-3,3H67.7a3.1,3.1,0,0,1-3-3V62.9' +
+    'A3.1,3.1,0,0,1,67.7,59.8Z' +
+    'M22.9,59.8h9.4a2.9,2.9,0,0,1,3,3v9.4a3.1,3.1,0,0,1-3,3H22.9' +
+    'a3.1,3.1,0,0,1-3-3V62.9A3.1,3.1,0,0,1,22.9,59.8Z' +
+    'M45.3,24.7h9.4a2.9,2.9,0,0,1,3,3v9.4a3.1,3.1,0,0,1-3,3H45.3' +
+    'a3.1,3.1,0,0,1-3-3V27.7A3.1,3.1,0,0,1,45.3,24.7Z' +
+    'M65.9,58.5a1.8,1.8,0,0,1-1.5-.8L55.3,44.3a1.8,1.8,0,0,1,3.1-2.1' +
+    'l9.1,13.4A1.8,1.8,0,0,1,65.9,58.5Z' +
+    'M61.1,68.8H39.1a1.8,1.8,0,1,1,0-3.7H61.1A1.8,1.8,0,0,1,61.1,68.8Z' +
+    'M35.5,59.9l-1-.3a1.8,1.8,0,0,1-.6-2.5l8.8-14.3' +
+    'a1.8,1.8,0,0,1,3.1,1.9L37.1,59A1.8,1.8,0,0,1,35.5,59.9Z'],
+
+    ['m_region', 'M49.8,70.9a3.4,3.4,0,0,1-3-1.8L34.3,47l-0.8-1.5' +
+    'a18.2,18.2,0,0,1-1.9-8.2,18.4,18.4,0,0,1,5.8-13.3' +
+    'A18.3,18.3,0,0,1,68.1,36.1h0a18.4,18.4,0,0,1-1.9,9.3' +
+    'L65.3,47,52.8,69.2A3.4,3.4,0,0,1,49.8,70.9Zm0-48.3' +
+    'A14.6,14.6,0,0,0,36.7,43.8l0.7,1.3L49.8,67,62.2,45.2l0.8-1.3' +
+    'a14.7,14.7,0,0,0,1.5-7.5h0A14.8,14.8,0,0,0,50.7,22.7H49.8Z' +
+    'm0,19.7a7.7,7.7,0,1,1,7.7-7.7A7.7,7.7,0,0,1,49.8,42.4Zm0-11.8' +
+    'a4.1,4.1,0,1,0,4.1,4.1A4.1,4.1,0,0,0,49.8,30.6Z' +
+    'M81.7,80.8H17.9a1.8,1.8,0,0,1-1.6-2.7l9.2-16.8' +
+    'a1.8,1.8,0,0,1,1.6-1h9.5a1.8,1.8,0,1,1,0,3.7H28.2L21,77.1H78.6' +
+    'L71.4,64H61.9a1.8,1.8,0,1,1,0-3.7H72.5a1.8,1.8,0,0,1,1.6,1' +
+    'l9.2,16.8A1.8,1.8,0,0,1,81.7,80.8Z'],
+]);
+
+export const extraGlyphs = new Map<string, string>([
+        ['_yang', '0 0 400 400'],
+        ['yang', 'M323.3,199.2a33.2,33.2,0,1,1-66.4,0' +
+        'c0-18.4,14.9-34.1,33.2-33.3S323.3,180.8,323.3,199.2Z' +
+        'M286.5,289.9c-78.2-.3-86.6-72.2-86.9-89.1s-14.6-91.8-88.3-88.3' +
+        'c-7.5.3-34.6,1.2-56.9,20.1-25.8,21.8-29,53.9-30.5,68.2-0.2,' +
+        '2.2-.4,4.4-0.5,6.5h0a175.5,175.5,0,0,0,171,172.9H199' +
+        'a175.5,175.5,0,0,0,58.6-10l3.9-1.4,3.9-1.5,3.9-1.7h0' +
+        'l3.9-1.7,2.8-1.3,2.7-1.4a175.6,175.6,0,0,0,95.9-155.1' +
+        'C372.4,226.7,358,290.1,286.5,289.9ZM110.1,237.7' +
+        'A33.6,33.6,0,1,1,143.7,204,33.6,33.6,0,0,1,110.1,237.7Z'],
+]);
+
+
+/**
+ * ONOS GUI -- SVG -- Glyph Data Service
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class GlyphDataService {
+
+    constructor(
+        private log: LogService
+    ) {
+        this.log.debug('GlyphDataService constructed');
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon.service.spec.ts b/web/gui2-fw-lib/lib/svg/icon.service.spec.ts
new file mode 100644
index 0000000..094baef
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon.service.spec.ts
@@ -0,0 +1,50 @@
+/*
+ *  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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { IconService } from './icon.service';
+import { GlyphService } from './glyph.service';
+import { SvgUtilService } from './svgutil.service';
+
+class MockGlyphService {}
+
+class MockSvgUtilService {}
+
+/**
+ * ONOS GUI -- SVG -- Icon Service - Unit Tests
+ */
+describe('IconService', () => {
+
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [IconService,
+                { provide: LogService, useValue: log },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: SvgUtilService, useClass: MockSvgUtilService },
+            ]
+        });
+    });
+
+    it('should be created', inject([IconService], (service: IconService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/svg/icon.service.ts b/web/gui2-fw-lib/lib/svg/icon.service.ts
new file mode 100644
index 0000000..f3c6432
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon.service.ts
@@ -0,0 +1,201 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { GlyphService } from './glyph.service';
+import { LogService } from '../log.service';
+import { SvgUtilService } from './svgutil.service';
+import * as d3 from 'd3';
+
+const vboxSize = 50;
+
+export const glyphMapping = new Map<string, string>([
+    // Maps icon ID to the glyph ID it uses.
+    // NOTE: icon ID maps to a CSS class for styling that icon
+    ['active', 'checkMark'],
+    ['inactive', 'xMark'],
+
+    ['plus', 'plus'],
+    ['minus', 'minus'],
+    ['play', 'play'],
+    ['stop', 'stop'],
+
+    ['upload', 'upload'],
+    ['download', 'download'],
+    ['delta', 'delta'],
+    ['nonzero', 'nonzero'],
+    ['close', 'xClose'],
+
+    ['m_cloud', 'm_cloud'],
+    ['m_map', 'm_map'],
+    ['m_selectMap', 'm_selectMap'],
+    ['thatsNoMoon', 'thatsNoMoon'],
+    ['m_ports', 'm_ports'],
+    ['m_switch', 'm_switch'],
+    ['switch', 'm_switch'],
+    ['m_roadm', 'm_roadm'],
+    ['roadm', 'm_roadm'],
+    ['m_router', 'm_router'],
+    ['router', 'm_router'],
+    ['m_uiAttached', 'm_uiAttached'],
+    ['m_endstation', 'm_endstation'],
+    ['endstation', 'm_endstation'],
+    ['m_summary', 'm_summary'],
+    ['m_details', 'm_details'],
+    ['m_oblique', 'm_oblique'],
+    ['m_filters', 'm_filters'],
+    ['m_cycleLabels', 'm_cycleLabels'],
+    ['m_cycleGridDisplay', 'm_cycleGridDisplay'],
+    ['m_prev', 'm_prev'],
+    ['m_next', 'm_next'],
+    ['m_flows', 'm_flows'],
+    ['m_allTraffic', 'm_allTraffic'],
+    ['m_xMark', 'm_xMark'],
+    ['m_resetZoom', 'm_resetZoom'],
+    ['m_eqMaster', 'm_eqMaster'],
+    ['m_unknown', 'm_unknown'],
+    ['m_controller', 'm_controller'],
+    ['m_eqMaster', 'm_eqMaster'],
+    ['m_virtual', 'm_virtual'],
+    ['m_other', 'm_other'],
+    ['m_bgpSpeaker', 'm_bgpSpeaker'],
+    ['bgpSpeaker', 'm_bgpSpeaker'],
+    ['m_otn', 'm_otn'],
+    ['otn', 'm_otn'],
+    ['m_terminal_device', 'm_otn'],
+    ['m_ols', 'm_roadm'],
+    ['m_roadm_otn', 'm_roadm_otn'],
+    ['roadm_otn', 'm_roadm_otn'],
+    ['m_fiberSwitch', 'm_fiberSwitch'],
+    ['fiber_switch', 'm_fiberSwitch'],
+    ['m_microwave', 'm_microwave'],
+    ['microwave', 'm_microwave'],
+    ['m_relatedIntents', 'm_relatedIntents'],
+    ['m_intentTraffic', 'm_intentTraffic'],
+    ['m_firewall', 'm_firewall'],
+    ['m_balancer', 'm_balancer'],
+    ['m_ips', 'm_ips'],
+    ['m_ids', 'm_ids'],
+    ['m_olt', 'm_olt'],
+    ['m_onu', 'm_onu'],
+    ['m_swap', 'm_swap'],
+    ['m_shortestGeoPath', 'm_shortestGeoPath'],
+    ['m_source', 'm_source'],
+    ['m_destination', 'm_destination'],
+    ['m_topo', 'm_topo'],
+    ['m_shortestPath', 'm_shortestPath'],
+    ['m_disjointPaths', 'm_disjointPaths'],
+    ['m_region', 'm_region'],
+    ['virtual', 'cord'],
+
+    ['topo', 'topo'],
+    ['bird', 'bird'],
+
+    ['refresh', 'refresh'],
+    ['query', 'query'],
+    ['garbage', 'garbage'],
+
+
+    ['upArrow', 'triangleUp'],
+    ['downArrow', 'triangleDown'],
+    ['triangleLeft', 'triangleLeft'],
+    ['triangleRight', 'triangleRight'],
+
+    ['appInactive', 'unknown'],
+    ['uiAttached', 'uiAttached'],
+
+    ['node', 'node'],
+    ['devIcon_SWITCH', 'switch'],
+    ['devIcon_ROADM', 'roadm'],
+    ['devIcon_OTN', 'otn'],
+
+    ['portIcon_DEFAULT', 'm_ports'],
+
+    ['meter', 'meterTable'], // TODO: m_meter icon?
+
+    ['deviceTable', 'switch'],
+    ['flowTable', 'flowTable'],
+    ['portTable', 'portTable'],
+    ['groupTable', 'groupTable'],
+    ['meterTable', 'meterTable'],
+    ['pipeconfTable', 'pipeconfTable'],
+
+    ['hostIcon_endstation', 'endstation'],
+    ['hostIcon_router', 'router'],
+    ['hostIcon_bgpSpeaker', 'bgpSpeaker'],
+
+    // navigation menu icons...
+    ['nav_apps', 'bird'],
+    ['nav_settings', 'cog'],
+    ['nav_cluster', 'node'],
+    ['nav_processors', 'allTraffic'],
+    ['nav_partitions', 'unknown'],
+
+    ['nav_topo', 'topo'],
+    ['nav_topo2', 'm_cloud'],
+    ['nav_devs', 'switch'],
+    ['nav_links', 'ports'],
+    ['nav_hosts', 'endstation'],
+    ['nav_intents', 'relatedIntents'],
+    ['nav_tunnels', 'ports'], // TODO: use tunnel glyph, when available
+    ['nav_yang', 'yang'],
+    ['clock', 'clock'],
+    ['clocks', 'clocks'],
+]);
+
+/**
+ * ONOS GUI -- SVG -- Icon Service
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class IconService {
+
+    constructor(
+        private gs: GlyphService,
+        private log: LogService,
+        private sus: SvgUtilService
+    ) {
+
+        this.log.debug('IconService constructed');
+    }
+
+    ensureIconLibDefs() {
+        const body = d3.select('body');
+        let svg = body.select('svg#IconLibDefs');
+        if (svg.empty()) {
+            svg = body.append('svg').attr('id', 'IconLibDefs');
+            svg.append('defs');
+        }
+        return svg.select('defs');
+    }
+
+    /**
+     * Load an icon only to the svg defs collection
+     *
+     * Note: This is added for use with IconComponent, where the icon's
+     * svg element is defined in the component template (and not built
+     * inline using d3 manipulation
+     *
+     * @param iconCls The icon class as a string
+     */
+    loadIconDef(iconCls: string): void {
+        let glyphName: string = glyphMapping.get(iconCls);
+        if (!glyphName) {
+            glyphName = iconCls;
+        }
+        this.gs.loadDefs(this.ensureIconLibDefs(), [glyphName], true, [iconCls]);
+    }
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon/button-theme.css b/web/gui2-fw-lib/lib/svg/icon/button-theme.css
new file mode 100644
index 0000000..8eff0a0
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/button-theme.css
@@ -0,0 +1,135 @@
+/*
+ * 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 -- Button Service (theme) -- CSS file
+ */
+
+
+/* === SELECTED BUTTONS === */
+
+/* Selected toggle / radio button */
+svg.embeddedIcon .toggleButton.selected .icon rect,
+svg.embeddedIcon .radioButton.selected .icon rect {
+    fill: #e4f0f6;
+}
+
+/* Selected:hover (normal) button */
+svg.embeddedIcon .button:hover .icon rect {
+    stroke: black;
+    stroke-width: 1px;
+}
+
+/* Selected:hover toggle-button */
+svg.embeddedIcon .toggleButton.selected:hover .icon rect {
+    fill: #c0d8f0;
+    stroke: black;
+    stroke-width: 1px;
+}
+
+/* Selected toggle/radio button and normal button glyph color */
+svg.embeddedIcon .button .glyph,
+svg.embeddedIcon .toggleButton.selected .glyph,
+svg.embeddedIcon .radioButton.selected .glyph {
+    fill: #5b99d2;
+}
+
+
+/* === UNSELECTED BUTTONS === */
+
+/* Unselected toggle / radio button */
+svg.embeddedIcon .toggleButton.icon rect,
+svg.embeddedIcon .radioButton.icon rect {
+    /* no fill */
+}
+
+/* Unselected:hover toggle / radio button */
+svg.embeddedIcon .icon.toggleButton:hover rect,
+svg.embeddedIcon .icon.radioButton:hover:not(.selected) rect {
+    fill: #e4f0f6;
+    stroke: black;
+    stroke-width: 1px;
+}
+
+/* Unselected toggle / radio button */
+svg.embeddedIcon .toggleButton .glyph,
+svg.embeddedIcon .radioButton .glyph {
+    fill: #bbb;
+}
+
+/* Unselected:hover toggle / radio button */
+svg.embeddedIcon .toggleButton:hover:not(.selected) .glyph,
+svg.embeddedIcon .radioButton:hover:not(.selected) .glyph {
+    fill: #5b99d2;
+}
+
+
+/* ========== DARK Theme ========== */
+
+/* Selected toggle / radio button */
+.dark .toggleButton.selected svg.embeddedIcon .icon rect,
+.dark .radioButton.selected svg.embeddedIcon .icon rect {
+    fill: #353e45;
+}
+
+/* Selected:hover (normal) button */
+.dark .button:hover svg.embeddedIcon .icon rect {
+    stroke: white;
+    stroke-width: 1px;
+}
+
+/* Selected:hover toggle-button */
+.dark .toggleButton.selected:hover svg.embeddedIcon .icon rect {
+    fill: #444d54;
+    stroke: white;
+    stroke-width: 1px;
+}
+
+/* Selected toggle/radio button and normal button glyph color */
+.dark .button svg.embeddedIcon .glyph,
+.dark .toggleButton.selected svg.embeddedIcon .glyph,
+.dark .radioButton.selected svg.embeddedIcon .glyph {
+    fill: #5b99d2;
+}
+
+
+/* === UNSELECTED BUTTONS === */
+
+/* Unselected toggle / radio button */
+.dark .toggleButton svg.embeddedIcon .icon rect,
+.dark .radioButton svg.embeddedIcon .icon rect {
+    /* no fill */
+}
+
+/* Unselected:hover toggle / radio button */
+.dark .toggleButton:hover svg.embeddedIcon .icon rect,
+.dark .radioButton:hover:not(.selected) svg.embeddedIcon .icon rect {
+    fill: #353e45;
+    stroke: white;
+    stroke-width: 1px;
+}
+
+/* Unselected toggle / radio button */
+.dark .toggleButton svg.embeddedIcon .glyph,
+.dark .radioButton svg.embeddedIcon .glyph {
+    fill: #bbb;
+}
+
+/* Unselected:hover toggle / radio button */
+.dark .toggleButton:hover:not(.selected) svg.embeddedIcon .glyph,
+.dark .radioButton:hover:not(.selected) svg.embeddedIcon .glyph {
+    fill: #5b99d2;
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon/glyph-theme.css b/web/gui2-fw-lib/lib/svg/icon/glyph-theme.css
new file mode 100644
index 0000000..12cdf30
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/glyph-theme.css
@@ -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.
+ */
+
+/*
+ ONOS GUI -- Glyph Service (theme) -- CSS file
+ */
+
+.light svg .glyph,
+.dark svg .glyph.overlay {
+    fill: blue;
+}
+
+/*
+* NOTE: Keeping the theme black while we
+* wait for the mockup theme designs to be made
+*/
+.dark svg .glyph,
+.light svg .glyph.overlay {
+    fill: blue;
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon/glyph.css b/web/gui2-fw-lib/lib/svg/icon/glyph.css
new file mode 100644
index 0000000..0743ff2
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/glyph.css
@@ -0,0 +1,24 @@
+/*
+ * 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 -- Glyph Service (layout) -- CSS file
+ */
+
+svg .glyph {
+    stroke: none;
+    fill-rule: evenodd;
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon/icon.component.css b/web/gui2-fw-lib/lib/svg/icon/icon.component.css
new file mode 100644
index 0000000..8350a75
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/icon.component.css
@@ -0,0 +1,87 @@
+/*
+ * 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 -- Icon Service (layout) -- CSS file
+ */
+
+svg#IconLibDefs {
+  display: none;
+}
+
+svg .svgIcon {
+    fill-rule: evenodd;
+}
+
+svg.embeddedIcon g.icon {
+    fill: none;
+}
+
+.ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #e0dfd6;
+}
+
+.ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+.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-fw-lib/lib/svg/icon/icon.component.html b/web/gui2-fw-lib/lib/svg/icon/icon.component.html
new file mode 100644
index 0000000..e7e2cd4
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/icon.component.html
@@ -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.
+-->
+<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>
+        <use width="50" height="50" class="glyph" [attr.href]="'#'+iconId"></use>
+    </g>
+</svg>
+<!-- I'm fixing class as light as view encapsulation changes how the hirerarchy of css is handled -->
+
+<!-- <p id="tooltip" class="light" *ngIf="toolTip" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none'}">{{ toolTipDisp }}</p> -->
+
+    <span class="tooltiptext" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none'}">{{toolTipDisp}}</span>
+</div>
diff --git a/web/gui2-fw-lib/lib/svg/icon/icon.component.spec.ts b/web/gui2-fw-lib/lib/svg/icon/icon.component.spec.ts
new file mode 100644
index 0000000..8234551
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/icon.component.spec.ts
@@ -0,0 +1,30 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LogService } from '../../log.service';
+import { ConsoleLoggerService } from '../../consolelogger.service';
+import { IconComponent } from './icon.component';
+import { IconService } from '../icon.service';
+
+class MockIconService {}
+
+describe('IconComponent', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            declarations: [ IconComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                { provide: IconService, useClass: MockIconService },
+            ]
+        });
+    });
+
+    it('should create', () => {
+        const fixture = TestBed.createComponent(IconComponent);
+        const component = fixture.componentInstance;
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-fw-lib/lib/svg/icon/icon.component.ts b/web/gui2-fw-lib/lib/svg/icon/icon.component.ts
new file mode 100644
index 0000000..138bb82
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/icon.component.ts
@@ -0,0 +1,62 @@
+/*
+ * 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, OnChanges, Input } from '@angular/core';
+import { IconService, glyphMapping } from '../icon.service';
+import { LogService } from '../../log.service';
+
+/**
+ * Icon Component
+ *
+ * Note: This is an alternative to the Icon Directive from ONOS 1.0.0
+ * It has been implemented as a Component because it was inadvertently adding
+ * in a template through d3 DOM manipulations - it's better to make it a Comp
+ * and build a template the Angular 7 way
+ *
+ * Remember: The CSS files applied here only apply to this component
+ */
+@Component({
+  selector: 'onos-icon',
+  templateUrl: './icon.component.html',
+  styleUrls: [
+    './icon.component.css', './icon.theme.css',
+    './glyph.css', './glyph-theme.css',
+    './tooltip.css', './button-theme.css'
+    ]
+})
+export class IconComponent implements OnChanges {
+    @Input() iconId: string;
+    @Input() iconSize: number = 20;
+    @Input() toolTip: string = undefined;
+    @Input() classes: string = undefined;
+
+    // The displayed tooltip - undefined until mouse hovers over, then equals toolTip
+    toolTipDisp: string = undefined;
+
+    constructor(
+        private is: IconService,
+        private log: LogService
+    ) {
+        // Note: iconId is not available until initialization
+    }
+
+    /**
+     * This is needed in case the iconId changes while icon component
+     * is displayed on screen.
+     */
+    ngOnChanges() {
+        this.is.loadIconDef(this.iconId);
+    }
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon/icon.theme.css b/web/gui2-fw-lib/lib/svg/icon/icon.theme.css
new file mode 100644
index 0000000..f267b10
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/icon.theme.css
@@ -0,0 +1,108 @@
+/*
+ * 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 -- Icon Service (theme) -- CSS file
+ */
+
+div.close-btn svg.embeddedIcon g.icon .glyph {
+    fill: #333333;
+}
+
+/* Sortable table headers */
+div.tableColSort svg.embeddedIcon .icon .glyph {
+    fill: #353333;
+}
+
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+svg.embeddedIcon g.icon use {
+    fill: #e0dfd6;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+.ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+
+svg.embeddedIcon g.icon.active:hover use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+svg.embeddedIcon g.icon.current-view rect {
+    fill: #518ecc;
+}
+svg.embeddedIcon g.icon.current-view use {
+    fill: white;
+}
+
+/* REFRESH */
+svg.embeddedIcon g.icon.refresh use {
+    fill: #cdeff2;
+}
+svg.embeddedIcon g.icon.refresh:hover use {
+    fill: #ce5b58;
+}
+svg.embeddedIcon g.icon.refresh.active use {
+    fill: #009fdb;
+}
+svg.embeddedIcon g.icon.refresh.active:hover use {
+    fill: #ce5b58;
+}
+
+
+/* ========== DARK Theme ========== */
+
+ div.close-btn svg.embeddedIcon g.icon .glyph {
+    fill: #8d8d8d;
+}
+
+ div.tableColSort svg.embeddedIcon .icon .glyph {
+    fill: #888888;
+}
+
+ /*svg.embeddedIcon .icon.active .glyph {
+    fill: #04bf34;
+}
+ svg.embeddedIcon .icon.inactive .glyph {
+    fill: #c0242b;
+}*/
+
+ table svg.embeddedIcon .icon .glyph {
+    fill: #9999aa;
+}
+
+/*
+svg.embeddedIcon g.icon .glyph {
+    fill: #007dc4;
+}
+
+svg.embeddedIcon:hover g.icon .glyph {
+    fill: #20b2ff;
+}
+*/
+
+svg.embeddedIcon g.icon.devIcon_SWITCH .glyph {
+    fill: #0071bd;;
+}
+
+svg.embeddedIcon g.icon.hostIcon_endstation .glyph {
+    fill: #0071bd;;
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon/tooltip-theme.css b/web/gui2-fw-lib/lib/svg/icon/tooltip-theme.css
new file mode 100644
index 0000000..bcdcd55
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/tooltip-theme.css
@@ -0,0 +1,30 @@
+/*
+ * 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 -- Tooltip Service (theme) -- CSS file
+ */
+.light#tooltip {
+    background-color: #dbeffc;
+    color: #3c3a3a;
+    border-color: #c7c7c0;
+}
+
+.dark#tooltip {
+    background-color: #3a3a60;
+    border-color: #6c6a6a;
+    color: #c7c7c0;
+}
diff --git a/web/gui2-fw-lib/lib/svg/icon/tooltip.css b/web/gui2-fw-lib/lib/svg/icon/tooltip.css
new file mode 100644
index 0000000..eda38d9
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/icon/tooltip.css
@@ -0,0 +1,61 @@
+/*
+ * 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 -- Tooltip Service (layout) -- CSS file
+ */
+
+#tooltip {
+    text-align: center;
+    font-size: 80%;
+    border: 1px solid;
+    padding: 5px;
+    position: absolute;
+    z-index: 5000;
+    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-fw-lib/lib/svg/svgutil.service.spec.ts b/web/gui2-fw-lib/lib/svg/svgutil.service.spec.ts
new file mode 100644
index 0000000..7165b33
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/svgutil.service.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { SvgUtilService } from './svgutil.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+/**
+ * ONOS GUI -- SVG -- Svg Util Service - Unit Tests
+ */
+describe('SvgUtilService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [SvgUtilService,
+                { provide: LogService, useValue: log },
+                { provide: FnService, useClass: MockFnService },
+            ]
+        });
+    });
+
+    it('should be created', inject([SvgUtilService], (service: SvgUtilService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/svg/svgutil.service.ts b/web/gui2-fw-lib/lib/svg/svgutil.service.ts
new file mode 100644
index 0000000..7510486
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/svgutil.service.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 { Injectable } from '@angular/core';
+import { FnService } from '../util/fn.service';
+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
+ *
+ * The SVG Util Service provides a miscellany of utility functions.
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class SvgUtilService {
+
+    constructor(
+        private fs: FnService,
+        private log: LogService
+    ) {
+
+
+
+        this.log.debug('SvgUtilService constructed');
+    }
+
+    translate(x: number[], y?: any): string {
+        if (this.fs.isA(x) && x.length === 2 && !y) {
+            return 'translate(' + x[0] + ',' + x[1] + ')';
+        }
+        return 'translate(' + x + ',' + y + ')';
+    }
+
+    scale(x, y) {
+        return 'scale(' + x + ',' + y + ')';
+    }
+
+    skewX(x) {
+        return 'skewX(' + x + ')';
+    }
+
+    rotate(deg) {
+        return 'rotate(' + deg + ')';
+    }
+
+    cat7() {
+        const tcid = 'd3utilTestCard';
+
+        function getColor(id, muted, theme) {
+            // 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 = 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 {
+                return muted ? lm : ln;
+            }
+        }
+
+        function testCard(svg) {
+            let g = svg.select('g#' + tcid);
+            const dom = d3.range(7);
+            let k;
+            let muted;
+            let theme;
+            let what;
+
+            if (!g.empty()) {
+                g.remove();
+
+            } else {
+                g = svg.append('g')
+                    .attr('id', tcid)
+                    .attr('transform', 'scale(4)translate(20,20)');
+
+                for (k = 0; k < 4; k++) {
+                    muted = k % 2;
+                    what = muted ? ' muted' : ' normal';
+                    theme = k < 2 ? 'light' : 'dark';
+                    dom.forEach((id, i) => {
+                        const x = i * 20;
+                        const y = k * 20;
+                        g.append('circle')
+                            .attr('cx', x)
+                            .attr('cy', y)
+                            .attr('r', 5)
+                            .attr('fill', getColor(id, muted, theme));
+                    });
+                    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)
+                        .attr('y', k * 20 + 2)
+                        .attr('fill', 'white');
+                        // .style('font-size', '4pt');
+                }
+            }
+        }
+
+        return {
+            testCard: testCard,
+            getColor: getColor,
+        };
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/svg/zoomable.directive.spec.ts b/web/gui2-fw-lib/lib/svg/zoomable.directive.spec.ts
new file mode 100644
index 0000000..36b6e37
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/zoomable.directive.spec.ts
@@ -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.
+ */
+import { ZoomableDirective } from './zoomable.directive';
+import {inject, TestBed} from '@angular/core/testing';
+import {ElementRef} from '@angular/core';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {FnService} from '../util/fn.service';
+import {LogService} from '../log.service';
+import {ConsoleLoggerService} from '../consolelogger.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+describe('ZoomableDirective', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let log: LogService;
+    let mockWindow: Window;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            },
+            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, mockWindow);
+
+        TestBed.configureTestingModule({
+            providers: [ZoomableDirective,
+                { provide: FnService, useValue: fs },
+                { provide: LogService, useValue: log },
+                { provide: 'Window', useValue: mockWindow },
+                { provide: ElementRef, useValue: mockWindow }
+            ]
+        });
+    });
+
+    it('should create an instance', inject([ZoomableDirective], (directive: ZoomableDirective) => {
+
+        expect(directive).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/svg/zoomable.directive.ts b/web/gui2-fw-lib/lib/svg/zoomable.directive.ts
new file mode 100644
index 0000000..96fbbfb
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/zoomable.directive.ts
@@ -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.
+ */
+import {
+    Directive,
+    ElementRef,
+    Input,
+    OnChanges,
+    OnInit,
+    SimpleChanges
+} from '@angular/core';
+import * as d3 from 'd3';
+import {TopoZoomPrefs} from './zoomutils';
+import {LogService} from '../log.service';
+import {PrefsService} from '../util/prefs.service';
+
+const TOPO_ZOOM_PREFS = 'topo_zoom';
+
+const ZOOM_PREFS_DEFAULT: TopoZoomPrefs = <TopoZoomPrefs>{
+    tx: 0, ty: 0, sc: 1.0
+};
+
+/**
+ * A directive that takes care of Zooming and Panning the Topology view
+ *
+ * It wraps the D3 Pan and Zoom functionality
+ * See https://github.com/d3/d3-zoom/blob/master/README.md
+ */
+@Directive({
+  selector: '[onosZoomableOf]'
+})
+export class ZoomableDirective implements OnChanges, OnInit {
+    @Input() zoomableOf: ElementRef;
+
+    zoom: any; // The d3 zoom behaviour
+    zoomCached: TopoZoomPrefs = <TopoZoomPrefs>{tx: 0, ty: 0, sc: 1.0};
+
+    constructor(
+        private _element: ElementRef,
+        private log: LogService,
+        private ps: PrefsService
+    ) {
+        const container = d3.select(this._element.nativeElement);
+
+        const zoomed = () => {
+            const transform = d3.event.transform;
+            container.attr('transform', 'translate(' + transform.x + ',' + transform.y + ') scale(' + transform.k + ')');
+            this.updateZoomState(<TopoZoomPrefs>{tx: transform.x, ty: transform.y, sc: transform.k});
+        };
+
+        this.zoom = d3.zoom().on('zoom', zoomed);
+    }
+
+    ngOnInit() {
+        this.zoomCached = this.ps.getPrefs(TOPO_ZOOM_PREFS, ZOOM_PREFS_DEFAULT);
+        const svg = d3.select(this.zoomableOf);
+
+        svg.call(this.zoom);
+
+        svg.transition().call(this.zoom.transform,
+            d3.zoomIdentity.translate(this.zoomCached.tx, this.zoomCached.ty).scale(this.zoomCached.sc));
+        this.log.debug('Loaded topo_zoom_prefs',
+            this.zoomCached.tx, this.zoomCached.ty, this.zoomCached.sc);
+
+    }
+
+    /**
+     * Updates the cache of zoom preferences locally and onwards to the PrefsService
+     */
+    updateZoomState(zoomPrefs: TopoZoomPrefs): void {
+        this.zoomCached = zoomPrefs;
+        this.ps.setPrefs(TOPO_ZOOM_PREFS, zoomPrefs);
+    }
+
+    /**
+     * If the input object is changed then re-establish the zoom
+     */
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes['zoomableOf']) {
+            const svg = d3.select(changes['zoomableOf'].currentValue);
+            svg.call(this.zoom);
+            this.log.debug('Applying zoomable behaviour on', this.zoomableOf, this._element.nativeElement);
+        }
+    }
+
+    /**
+     * Change the zoom level when a map is chosen in Topology view
+     *
+     * Animated to run over 750ms
+     */
+    changeZoomLevel(zoomState: TopoZoomPrefs, fast?: boolean): void {
+        const svg = d3.select(this.zoomableOf);
+        svg.transition().duration(fast ? 0 : 750).call(this.zoom.transform,
+            d3.zoomIdentity.translate(zoomState.tx, zoomState.ty).scale(zoomState.sc));
+        this.updateZoomState(zoomState);
+        this.log.debug('Pan to', zoomState.tx, zoomState.ty, 'and zoom to', zoomState.sc);
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/svg/zoomutils.spec.ts b/web/gui2-fw-lib/lib/svg/zoomutils.spec.ts
new file mode 100644
index 0000000..c3b8cae
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/zoomutils.spec.ts
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import {TestBed} from '@angular/core/testing';
+import {LocMeta, MapBounds, ZoomUtils} from './zoomutils';
+
+describe('ZoomUtils', () => {
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+
+        });
+    });
+
+    it('should be created', () => {
+        const zu = new ZoomUtils();
+        expect(zu).toBeTruthy();
+    });
+
+    it('should covert GEO origin to Canvas', () => {
+        const canvas = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+            lng: 0, lat: 0
+        });
+
+        expect(canvas).not.toBeNull();
+        expect(canvas.x).toEqual(500);
+        expect(canvas.y).toEqual(500);
+    });
+
+    it('should covert GEO positive to Canvas', () => {
+        const canvas = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+            lng: 30, lat: 30
+        });
+
+        expect(canvas).not.toBeNull();
+        expect(Math.round(canvas.x)).toEqual(667);
+        expect(canvas.y).toEqual(300);
+    });
+
+    it('should covert GEO negative to Canvas', () => {
+        const canvas = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+            lng: -30, lat: -30
+        });
+
+        expect(canvas).not.toBeNull();
+        expect(Math.round(canvas.x)).toEqual(333);
+        expect(canvas.y).toEqual(700);
+    });
+
+    it('should convert XY origin to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(0, 0);
+        expect(geo.equivLoc.lng).toEqual(-90);
+        expect(geo.equivLoc.lat).toEqual(75);
+    });
+
+    it('should convert XY centre to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(500, 500);
+        expect(geo.equivLoc.lng).toEqual(0);
+        expect(geo.equivLoc.lat).toEqual(0);
+    });
+
+    it('should convert XY 1000 to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(1000, 1000);
+        expect(geo.equivLoc.lng).toEqual(90);
+        expect(geo.equivLoc.lat).toEqual(-75);
+    });
+
+    it('should convert XY leftmost to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(-500, 500);
+        expect(geo.equivLoc.lng).toEqual(-180);
+        expect(geo.equivLoc.lat).toEqual(0);
+    });
+
+    it('should convert XY rightmost to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(1500, 500);
+        expect(geo.equivLoc.lng).toEqual(+180);
+        expect(geo.equivLoc.lat).toEqual(0);
+    });
+
+    it('should convert MapBounds in upper left quadrant to Zoom level', () => {
+        const zoomParams = ZoomUtils.convertBoundsToZoomLevel(
+            <MapBounds>{ latMin: 40, lngMin: -40, latMax: 50, lngMax: -30 });
+
+        expect(zoomParams.sc).toEqual(11.18);
+        expect(Math.round(zoomParams.tx)).toEqual(-2916);
+        expect(Math.round(zoomParams.ty)).toEqual(-1736);
+    });
+
+    it('should convert MapBounds in lower right quadrant to Zoom level', () => {
+        const zoomParams = ZoomUtils.convertBoundsToZoomLevel(
+            <MapBounds>{ latMin: -50, lngMin: 30, latMax: -40, lngMax: 40 });
+
+        expect(zoomParams.sc).toEqual(11.18);
+        expect(Math.round(zoomParams.tx)).toEqual(-7264);
+        expect(Math.round(zoomParams.ty)).toEqual(-8444);
+    });
+
+    it('should convert MapBounds around equator to Zoom level', () => {
+        const zoomParams = ZoomUtils.convertBoundsToZoomLevel(
+            <MapBounds>{ latMin: -10, lngMin: -10, latMax: 10, lngMax: 10 });
+
+        expect(Math.round(zoomParams.sc * 100)).toEqual(644);
+        expect(Math.round(zoomParams.tx)).toEqual(-2721);
+        expect(Math.round(zoomParams.ty)).toEqual(-2721);
+    });
+});
diff --git a/web/gui2-fw-lib/lib/svg/zoomutils.ts b/web/gui2-fw-lib/lib/svg/zoomutils.ts
new file mode 100644
index 0000000..1150c39
--- /dev/null
+++ b/web/gui2-fw-lib/lib/svg/zoomutils.ts
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2019-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 {LogService} from '../log.service';
+
+const LONGITUDE_EXTENT = 180;
+const LATITUDE_EXTENT = 75;
+const GRID_EXTENT_X = 2000;
+const GRID_EXTENT_Y = 1000;
+const GRID_DIAGONAL = 2236; // 2236 is the length of the diagonal of the 2000x1000 box
+const GRID_CENTRE_X = 500;
+const GRID_CENTRE_Y = 500;
+
+
+/**
+ * A model of the map bounds bottom left to top right in lat and long
+ */
+export interface MapBounds {
+    lngMin: number;
+    latMin: number;
+    lngMax: number;
+    latMax: number;
+}
+
+/**
+ * model of the topo2CurrentRegion Loc part of the MetaUi below
+ */
+export interface LocMeta {
+    lng: number;
+    lat: number;
+}
+
+/**
+ * model of the topo2CurrentRegion MetaUi from Device below
+ */
+export interface MetaUi {
+    equivLoc: LocMeta;
+    x: number;
+    y: number;
+}
+
+/**
+ * Model of the Zoom preferences
+ */
+export interface TopoZoomPrefs {
+    tx: number;
+    ty: number;
+    sc: number;
+}
+
+/**
+ * Utility class with static functions for scaling maps
+ *
+ * This is left as a class, so that the functions are loaded only as needed
+ */
+export class ZoomUtils {
+    static convertGeoToCanvas(location: LocMeta ): MetaUi {
+        const calcX = (LONGITUDE_EXTENT + location.lng) / (LONGITUDE_EXTENT * 2) * GRID_EXTENT_X - GRID_CENTRE_X;
+        const calcY = (LATITUDE_EXTENT - location.lat) / (LATITUDE_EXTENT * 2) * GRID_EXTENT_Y;
+        return <MetaUi>{
+            x: calcX,
+            y: calcY,
+            equivLoc: {
+                lat: location.lat,
+                lng: location.lng
+            }
+        };
+    }
+
+    static convertXYtoGeo(x: number, y: number): MetaUi {
+        const calcLong: number = (x + GRID_CENTRE_X) * 2 * LONGITUDE_EXTENT / GRID_EXTENT_X - LONGITUDE_EXTENT;
+        const calcLat: number = -(y * 2 * LATITUDE_EXTENT / GRID_EXTENT_Y - LATITUDE_EXTENT);
+        return <MetaUi>{
+            x: x,
+            y: y,
+            equivLoc: <LocMeta>{
+                lat: (calcLat === -0) ? 0 : calcLat,
+                lng: calcLong
+            }
+        };
+    }
+
+    /**
+     * This converts the bounds of a map loaded from a TopoGson file that has been
+     * converted in to a GEOJson format by d3
+     *
+     * The bounds are in latitude and longitude from bottom left (min) to top right (max)
+     *
+     * First they are converted in to SVG viewbox coordinates 0,0 top left 1000x1000
+     *
+     * The the zoom level is calculated by scaling to the grid diagonal
+     *
+     * Finally the translation is calculated by applying the zoom first, and then
+     * translating on the zoomed coordinate system
+     * @param mapBounds - the bounding box of the chosen map in lat and long
+     * @param log The LogService
+     */
+    static convertBoundsToZoomLevel(mapBounds: MapBounds, log?: LogService): TopoZoomPrefs {
+
+        const min: MetaUi = this.convertGeoToCanvas(<LocMeta>{
+            lng: mapBounds.lngMin,
+            lat: mapBounds.latMin
+        });
+
+        const max: MetaUi = this.convertGeoToCanvas(<LocMeta>{
+            lng: mapBounds.lngMax,
+            lat: mapBounds.latMax
+        });
+
+        const diagonal = Math.sqrt(Math.pow(max.x - min.x, 2) + Math.pow(max.y - min.y, 2));
+        const centreX = (max.x - min.x) / 2 + min.x;
+        const centreY = (max.y - min.y) / 2 + min.y;
+        // Zoom works from the top left of the 1000x1000 viewbox
+        // The scale is applied first and then the translate is on the scaled coordinates
+        const zoomscale = 0.5 * GRID_DIAGONAL / ((diagonal < 100) ? 100 : diagonal); // Don't divide by zero
+        const zoomx = -centreX * zoomscale + GRID_CENTRE_X;
+        const zoomy = -centreY * zoomscale + GRID_CENTRE_Y;
+
+        // log.debug('MapBounds', mapBounds, 'XYMin', min, 'XYMax', max, 'Diag', diagonal,
+        //     'Centre', centreX, centreY, 'translate', zoomx, zoomy, 'Scale', zoomscale);
+
+        return <TopoZoomPrefs>{tx: zoomx, ty: zoomy, sc: zoomscale};
+    }
+
+    /**
+     * Calculate Zoom settings to fit the 1000x1000 grid in to the available window height
+     * less the banner height
+     *
+     * Scaling always happens from the top left 0,0
+     * If the height is greater than the width then no scaling is required - grid will
+     * need to fill the SVG canvas
+     * @param bannerHeight - the top band of the screen for the mast
+     * @param innerWidth - the actual width of the screen
+     * @param innerHeight - the actual height of the screen
+     * @return Zoom settings - scale and translate
+     */
+    static zoomToWindowSize(bannerHeight: number, innerWidth: number, innerHeight: number): TopoZoomPrefs {
+        const newHeight = innerHeight - bannerHeight;
+        if (newHeight > innerWidth) {
+            return <TopoZoomPrefs>{
+                sc: 1.0,
+                tx: 0,
+                ty: 0
+            };
+        } else {
+            const scale = newHeight / innerWidth;
+            return <TopoZoomPrefs>{
+                sc: scale,
+                tx: (500 / scale - 500) * scale,
+                ty: 0
+            };
+        }
+    }
+}
diff --git a/web/gui2-fw-lib/lib/util/fn.service.spec.ts b/web/gui2-fw-lib/lib/util/fn.service.spec.ts
new file mode 100644
index 0000000..e9b7c2a
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/fn.service.spec.ts
@@ -0,0 +1,493 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { FnService } from './fn.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import * as d3 from 'd3';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Util -- General Purpose Functions - Unit Tests
+ */
+describe('FnService', () => {
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let mockWindow: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const someFunction = () => {};
+    const someArray = [1, 2, 3];
+    const someObject = { foo: 'bar'};
+    const someNumber = 42;
+    const someString = 'xyyzy';
+    const someDate = new Date();
+    const stringArray = ['foo', 'bar'];
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            }
+        };
+
+
+        TestBed.configureTestingModule({
+            providers: [FnService,
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+
+        fs = TestBed.get(FnService);
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    it('should be created', () => {
+        expect(fs).toBeTruthy();
+    });
+
+    // === Tests for isF()
+    it('isF(): null for undefined', () => {
+        expect(fs.isF(undefined)).toBeNull();
+    });
+
+    it('isF(): null for null', () => {
+        expect(fs.isF(null)).toBeNull();
+    });
+    it('isF(): the reference for function', () => {
+        expect(fs.isF(someFunction)).toBe(someFunction);
+    });
+    it('isF(): null for string', () => {
+        expect(fs.isF(someString)).toBeNull();
+    });
+    it('isF(): null for number', () => {
+        expect(fs.isF(someNumber)).toBeNull();
+    });
+    it('isF(): null for Date', () => {
+        expect(fs.isF(someDate)).toBeNull();
+    });
+    it('isF(): null for array', () => {
+        expect(fs.isF(someArray)).toBeNull();
+    });
+    it('isF(): null for object', () => {
+        expect(fs.isF(someObject)).toBeNull();
+    });
+
+    // === Tests for isA()
+    it('isA(): null for undefined', () => {
+        expect(fs.isA(undefined)).toBeNull();
+    });
+    it('isA(): null for null', () => {
+        expect(fs.isA(null)).toBeNull();
+    });
+    it('isA(): null for function', () => {
+        expect(fs.isA(someFunction)).toBeNull();
+    });
+    it('isA(): null for string', () => {
+        expect(fs.isA(someString)).toBeNull();
+    });
+    it('isA(): null for number', () => {
+        expect(fs.isA(someNumber)).toBeNull();
+    });
+    it('isA(): null for Date', () => {
+        expect(fs.isA(someDate)).toBeNull();
+    });
+    it('isA(): the reference for array', () => {
+        expect(fs.isA(someArray)).toBe(someArray);
+    });
+    it('isA(): null for object', () => {
+        expect(fs.isA(someObject)).toBeNull();
+    });
+
+    // === Tests for isS()
+    it('isS(): null for undefined', () => {
+        expect(fs.isS(undefined)).toBeNull();
+    });
+    it('isS(): null for null', () => {
+        expect(fs.isS(null)).toBeNull();
+    });
+    it('isS(): null for function', () => {
+        expect(fs.isS(someFunction)).toBeNull();
+    });
+    it('isS(): the reference for string', () => {
+        expect(fs.isS(someString)).toBe(someString);
+    });
+    it('isS(): null for number', () => {
+        expect(fs.isS(someNumber)).toBeNull();
+    });
+    it('isS(): null for Date', () => {
+        expect(fs.isS(someDate)).toBeNull();
+    });
+    it('isS(): null for array', () => {
+        expect(fs.isS(someArray)).toBeNull();
+    });
+    it('isS(): null for object', () => {
+        expect(fs.isS(someObject)).toBeNull();
+    });
+
+    // === Tests for isO()
+    it('isO(): null for undefined', () => {
+        expect(fs.isO(undefined)).toBeNull();
+    });
+    it('isO(): null for null', () => {
+        expect(fs.isO(null)).toBeNull();
+    });
+    it('isO(): null for function', () => {
+        expect(fs.isO(someFunction)).toBeNull();
+    });
+    it('isO(): null for string', () => {
+        expect(fs.isO(someString)).toBeNull();
+    });
+    it('isO(): null for number', () => {
+        expect(fs.isO(someNumber)).toBeNull();
+    });
+    it('isO(): null for Date', () => {
+        expect(fs.isO(someDate)).toBeNull();
+    });
+    it('isO(): null for array', () => {
+        expect(fs.isO(someArray)).toBeNull();
+    });
+    it('isO(): the reference for object', () => {
+        expect(fs.isO(someObject)).toBe(someObject);
+    });
+
+
+    // === Tests for contains()
+    it('contains(): false for non-array', () => {
+        expect(fs.contains(null, 1)).toBeFalsy();
+    });
+    it('contains(): true for contained item', () => {
+        expect(fs.contains(someArray, 1)).toBeTruthy();
+        expect(fs.contains(stringArray, 'bar')).toBeTruthy();
+    });
+    it('contains(): false for non-contained item', () => {
+        expect(fs.contains(someArray, 109)).toBeFalsy();
+        expect(fs.contains(stringArray, 'zonko')).toBeFalsy();
+    });
+
+    // === Tests for areFunctions()
+    it('areFunctions(): true for empty-array', () => {
+        expect(fs.areFunctions({}, [])).toBeTruthy();
+    });
+    it('areFunctions(): true for some api', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: () => {}
+        }, ['b', 'a'])).toBeTruthy();
+    });
+    it('areFunctions(): false for some other api', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: 'not-a-function'
+        }, ['b', 'a'])).toBeFalsy();
+    });
+    it('areFunctions(): extraneous stuff NOT ignored', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: () => {},
+            c: 1,
+            d: 'foo'
+        }, ['a', 'b'])).toBeFalsy();
+    });
+    it('areFunctions(): extraneous stuff ignored (alternate fn)', () => {
+        expect(fs.areFunctionsNonStrict({
+            a: () => {},
+            b: () => {},
+            c: 1,
+            d: 'foo'
+        }, ['a', 'b'])).toBeTruthy();
+    });
+
+    // == use the now-tested areFunctions() on our own api:
+    it('should define api functions', () => {
+        expect(fs.areFunctions(fs, [
+            'isF', 'isA', 'isS', 'isO', 'contains',
+            'areFunctions', 'areFunctionsNonStrict', 'windowSize',
+            'isMobile', 'isChrome', 'isChromeHeadless', 'isSafari',
+            'isFirefox', 'parseDebugFlags',
+            'debugOn', 'debug', 'find', 'inArray', 'removeFromArray',
+            'isEmptyObject', 'cap', 'noPx', 'noPxStyle', 'endsWith',
+            'inEvilList', 'analyze', 'sanitize', 'sameObjProps', 'containsObj',
+            'addToTrie', 'removeFromTrie', 'trieLookup'
+//            'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
+//            'eecode', 'noPx', 'noPxStyle', 'endsWith', 'addToTrie', 'removeFromTrie', 'trieLookup',
+//            'classNames', 'extend', 'sanitize'
+        ])).toBeTruthy();
+    });
+
+
+    // === Tests for windowSize()
+    it('windowSize(): adjust height', () => {
+        const dim = fs.windowSize(50);
+        expect(dim.width).toEqual(400);
+        expect(dim.height).toEqual(150);
+    });
+
+    it('windowSize(): adjust width', () => {
+        const dim = fs.windowSize(0, 50);
+        expect(dim.width).toEqual(350);
+        expect(dim.height).toEqual(200);
+    });
+
+    it('windowSize(): adjust width and height', () => {
+        const dim = fs.windowSize(101, 201);
+        expect(dim.width).toEqual(199);
+        expect(dim.height).toEqual(99);
+    });
+
+    // === Tests for isMobile()
+    const uaMap = {
+        chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) ' +
+                'AppleWebKit/537.36 (KHTML, like Gecko) ' +
+                'Chrome/41.0.2272.89 Safari/537.36',
+
+        iPad: 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) ' +
+                'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+                'Mobile/11A465 Safari/9537.53',
+
+        iPhone: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) ' +
+                'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+                'Mobile/11A465 Safari/9537.53'
+    };
+
+    function setUa(key) {
+        const str = uaMap[key];
+        expect(str).toBeTruthy();
+        (<any>mockWindow.navigator).userAgent = str;
+    }
+
+    it('isMobile(): should be false for Chrome on Mac OS X', () => {
+        setUa('chrome');
+        expect(fs.isMobile()).toBe(false);
+    });
+    it('isMobile(): should be true for Safari on iPad', () => {
+        setUa('iPad');
+        expect(fs.isMobile()).toBe(true);
+    });
+    it('isMobile(): should be true for Safari on iPhone', () => {
+        setUa('iPhone');
+        expect(fs.isMobile()).toBe(true);
+    });
+
+    // === Tests for find()
+    const dataset = [
+        { id: 'foo', name: 'Furby'},
+        { id: 'bar', name: 'Barbi'},
+        { id: 'baz', name: 'Basil'},
+        { id: 'goo', name: 'Gabby'},
+        { id: 'zoo', name: 'Zevvv'}
+    ];
+
+    it('should not find ooo', () => {
+        expect(fs.find('ooo', dataset)).toEqual(-1);
+    });
+    it('should find foo', () => {
+        expect(fs.find('foo', dataset)).toEqual(0);
+    });
+    it('should find zoo', () => {
+        expect(fs.find('zoo', dataset)).toEqual(4);
+    });
+
+    it('should not find Simon', () => {
+        expect(fs.find('Simon', dataset, 'name')).toEqual(-1);
+    });
+    it('should find Furby', () => {
+        expect(fs.find('Furby', dataset, 'name')).toEqual(0);
+    });
+    it('should find Zevvv', () => {
+        expect(fs.find('Zevvv', dataset, 'name')).toEqual(4);
+    });
+
+
+    // === Tests for inArray()
+    const objRef = { x: 1, y: 2 };
+    const array = [1, 3.14, 'hey', objRef, 'there', true];
+    const array2 = ['b', 'a', 'd', 'a', 's', 's'];
+
+    it('should not find HOO', () => {
+        expect(fs.inArray('HOO', array)).toEqual(-1);
+    });
+    it('should find 1', () => {
+        expect(fs.inArray(1, array)).toEqual(0);
+    });
+    it('should find pi', () => {
+        expect(fs.inArray(3.14, array)).toEqual(1);
+    });
+    it('should find hey', () => {
+        expect(fs.inArray('hey', array)).toEqual(2);
+    });
+    it('should find the object', () => {
+        expect(fs.inArray(objRef, array)).toEqual(3);
+    });
+    it('should find there', () => {
+        expect(fs.inArray('there', array)).toEqual(4);
+    });
+    it('should find true', () => {
+        expect(fs.inArray(true, array)).toEqual(5);
+    });
+
+    it('should find the first occurrence A', () => {
+        expect(fs.inArray('a', array2)).toEqual(1);
+    });
+    it('should find the first occurrence S', () => {
+        expect(fs.inArray('s', array2)).toEqual(4);
+    });
+    it('should not find X', () => {
+        expect(fs.inArray('x', array2)).toEqual(-1);
+    });
+
+    // === Tests for removeFromArray()
+    it('should keep the array the same, for non-match', () => {
+        const array1 = [1, 2, 3];
+        expect(fs.removeFromArray(4, array1)).toBe(false);
+        expect(array1).toEqual([1, 2, 3]);
+    });
+    it('should remove a value', () => {
+        const array1a = [1, 2, 3];
+        expect(fs.removeFromArray(2, array1a)).toBe(true);
+        expect(array1a).toEqual([1, 3]);
+    });
+    it('should remove the first occurrence', () => {
+        const array1b = ['x', 'y', 'z', 'z', 'y'];
+        expect(fs.removeFromArray('y', array1b)).toBe(true);
+        expect(array1b).toEqual(['x', 'z', 'z', 'y']);
+        expect(fs.removeFromArray('x', array1b)).toBe(true);
+        expect(array1b).toEqual(['z', 'z', 'y']);
+    });
+
+    // === Tests for isEmptyObject()
+    it('should return true if an object is empty', () => {
+        expect(fs.isEmptyObject({})).toBe(true);
+    });
+    it('should return false if an object is not empty', () => {
+        expect(fs.isEmptyObject({foo: 'bar'})).toBe(false);
+    });
+
+    // === Tests for cap()
+    it('should ignore non-alpha', () => {
+        expect(fs.cap('123')).toEqual('123');
+    });
+    it('should capitalize first char', () => {
+        expect(fs.cap('Foo')).toEqual('Foo');
+        expect(fs.cap('foo')).toEqual('Foo');
+        expect(fs.cap('foo bar')).toEqual('Foo bar');
+        expect(fs.cap('FOO BAR')).toEqual('Foo bar');
+        expect(fs.cap('foo Bar')).toEqual('Foo bar');
+    });
+
+    // === Tests for noPx()
+    it('should return the value without px suffix', () => {
+        expect(fs.noPx('10px')).toBe(10);
+        expect(fs.noPx('500px')).toBe(500);
+        expect(fs.noPx('-80px')).toBe(-80);
+    });
+
+    // === Tests for noPxStyle()
+    it('should give a style\'s property without px suffix', () => {
+        const d3Elem = d3.select('body')
+            .append('div')
+            .attr('id', 'fooElem')
+            .style('width', '500px')
+            .style('height', '200px')
+            .style('font-size', '12px');
+        expect(fs.noPxStyle(d3Elem, 'width')).toBe(500);
+        expect(fs.noPxStyle(d3Elem, 'height')).toBe(200);
+        expect(fs.noPxStyle(d3Elem, 'font-size')).toBe(12);
+        d3.select('#fooElem').remove();
+    });
+
+    // === Tests for endsWith()
+    it('should return true if string ends with foo', () => {
+        expect(fs.endsWith('barfoo', 'foo')).toBe(true);
+    });
+
+    it('should return false if string doesnt end with foo', () => {
+        expect(fs.endsWith('barfood', 'foo')).toBe(false);
+    });
+
+    // === Tests for sanitize()
+    it('should return foo', () => {
+        expect(fs.sanitize('foo')).toEqual('foo');
+    });
+    it('should retain < b > tags', () => {
+        const str = 'foo <b>bar</b> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < i > tags', () => {
+        const str = 'foo <i>bar</i> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < p > tags', () => {
+        const str = 'foo <p>bar</p> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < em > tags', () => {
+        const str = 'foo <em>bar</em> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < strong > tags', () => {
+        const str = 'foo <strong>bar</strong> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+
+    it('should reject < a > tags', () => {
+        expect(fs.sanitize('test <a href="hah">something</a> this'))
+            .toEqual('test something this');
+    });
+
+    it('should log a warning for < script > tags', () => {
+        expect(fs.sanitize('<script>alert("foo");</script>'))
+            .toEqual('');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <script> detected!'
+        );
+    });
+    it('should log a warning for < style > tags', () => {
+        expect(fs.sanitize('<style> h1 {color:red;} </style>'))
+            .toEqual('');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <style> detected!'
+        );
+    });
+
+    it('should log a warning for < iframe > tags', () => {
+        expect(fs.sanitize('Foo<iframe><body><h1>fake</h1></body></iframe>Bar'))
+            .toEqual('FooBar');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <iframe> detected!'
+        );
+    });
+
+    it('should completely strip < script >, remove < a >, retain < i >', () => {
+        expect(fs.sanitize('Hey <i>this</i> is <script>alert("foo");</script> <a href="meh">cool</a>'))
+            .toEqual('Hey <i>this</i> is  cool');
+    });
+});
diff --git a/web/gui2-fw-lib/lib/util/fn.service.ts b/web/gui2-fw-lib/lib/util/fn.service.ts
new file mode 100644
index 0000000..6694182
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/fn.service.ts
@@ -0,0 +1,501 @@
+/*
+ * 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 {Inject, Injectable} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {LogService} from '../log.service';
+import {Trie, TrieOp} from './trie';
+
+// Angular>=2 workaround for missing definition
+declare const InstallTrigger: any;
+
+const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
+const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
+const evillist: string[] = ['script', 'style', 'iframe'];
+
+/**
+ * Used with the Window size function;
+ **/
+export interface WindowSize {
+    width: number;
+    height: number;
+}
+
+/**
+ * For the sanitize() and analyze() functions
+ */
+export interface Match {
+    full: string;
+    name: string;
+}
+
+/**
+ * ONOS GUI -- Util -- General Purpose Functions
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class FnService {
+    // internal state
+    private debugFlags = new Map<string, boolean>([
+//        [ "LoadingService", true ]
+    ]);
+
+    constructor(
+        private route: ActivatedRoute,
+        private log: LogService,
+        // TODO: Change the any type to Window when https://github.com/angular/angular/issues/15640 is fixed.
+        @Inject('Window') private w: any
+    ) {
+        this.route.queryParams.subscribe(params => {
+            const debugparam: string = params['debug'];
+//            log.debug('Param:', debugparam);
+            this.parseDebugFlags(debugparam);
+        });
+//        this.log.debug('FnService constructed');
+    }
+
+    /**
+     * Test if an argument is a function
+     *
+     * Note: the need for this would go away if all functions
+     * were strongly typed
+     */
+    isF(f: any): any {
+        return typeof f === 'function' ? f : null;
+    }
+
+    /**
+     * Test if an argument is an array
+     *
+     * Note: the need for this would go away if all arrays
+     * were strongly typed
+     */
+    isA(a: any): any {
+    // NOTE: Array.isArray() is part of EMCAScript 5.1
+        return Array.isArray(a) ? a : null;
+    }
+
+    /**
+     * Test if an argument is a string
+     *
+     * Note: the need for this would go away if all strings
+     * were strongly typed
+     */
+    isS(s: any): string {
+        return typeof s === 'string' ? s : null;
+    }
+
+    /**
+     * Test if an argument is an object
+     *
+     * Note: the need for this would go away if all objects
+     * were strongly typed
+     */
+    isO(o: any): Object {
+        return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
+    }
+
+    /**
+     * Test that an array contains an object
+     */
+    contains(a: any[], x: any): boolean {
+        return this.isA(a) && a.indexOf(x) > -1;
+    }
+
+    /**
+     * Returns width and height of window inner dimensions.
+     * offH, offW : offset width/height are subtracted, if present
+     */
+    windowSize(offH: number = 0, offW: number = 0): WindowSize {
+        return {
+            height: this.w.innerHeight - offH,
+            width: this.w.innerWidth - offW
+        };
+    }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise.
+     * Also returns false if there are properties on the api that are NOT
+     * listed in the array of names.
+     *
+     * This gets extra complicated when the api Object is an
+     * Angular service - while the functions can be retrieved
+     * by an indexed get, the ownProperties does not show the
+     * functions of the class. We have to dive in to the prototypes
+     * properties to get these - and even then we have to filter
+     * out the constructor and any member variables
+     */
+    // areFunctions(api: Object, fnNames: string[]): boolean {
+    //     const fnLookup: Map<string, boolean> = new Map();
+    //     let extraFound: boolean = false;
+    //
+    //     if (!this.isA(fnNames)) {
+    //         return false;
+    //     }
+    //
+    //     const n: number = fnNames.length;
+    //     let i: number;
+    //     let name: string;
+    //
+    //     for (i = 0; i < n; i++) {
+    //         name = fnNames[i];
+    //         if (!this.isF(api[name])) {
+    //             return false;
+    //         }
+    //         fnLookup.set(name, true);
+    //     }
+    //
+    //     // check for properties on the API that are not listed in the array,
+    //     const keys = Object.getOwnPropertyNames(api);
+    //     if (keys.length === 0) {
+    //         return true;
+    //     }
+    //     // If the api is a class it will have a name,
+    //     //  else it will just be called 'Object'
+    //     const apiObjectName: string = api.constructor.name;
+    //     if (apiObjectName === 'Object') {
+    //         Object.keys(api).forEach((key) => {
+    //             if (!fnLookup.get(key)) {
+    //                 extraFound = true;
+    //             }
+    //         });
+    //     } else { // It is a class, so its functions will be in the child (prototype)
+    //         const pObj: Object = Object.getPrototypeOf(api);
+    //         for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
+    //             if (key === 'constructor') { // Filter out constructor
+    //                 continue;
+    //             }
+    //             const value = Object.getOwnPropertyDescriptor(pObj, key);
+    //             // Only compare functions. Look for any not given in the map
+    //             if (this.isF(value.value) && !fnLookup.get(key)) {
+    //                 extraFound = true;
+    //             }
+    //         }
+    //     }
+    //     return !extraFound;
+    // }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise. This is a non-strict version
+     * that does not care about other properties on the api.
+     */
+    areFunctionsNonStrict(api, fnNames): boolean {
+        if (!this.isA(fnNames)) {
+            return false;
+        }
+        const n = fnNames.length;
+        let i;
+        let name;
+
+        for (i = 0; i < n; i++) {
+            name = fnNames[i];
+            if (!this.isF(api[name])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if current browser determined to be a mobile device
+     */
+    isMobile() {
+        const ua = this.w.navigator.userAgent;
+        const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
+        return patt.test(ua);
+    }
+
+    /**
+     * Returns true if the current browser determined to be Chrome
+     */
+    isChrome() {
+        const isChromium = (this.w as any).chrome;
+        const vendorName = this.w.navigator.vendor;
+
+        const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
+        return (isChromium !== null &&
+        isChromium !== undefined &&
+        vendorName === 'Google Inc.' &&
+        isOpera === false);
+    }
+
+    isChromeHeadless() {
+        const vendorName = this.w.navigator.vendor;
+        const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
+
+        return (vendorName === 'Google Inc.' && headlessChrome === true);
+    }
+
+    /**
+     * Returns true if the current browser determined to be Safari
+     */
+    isSafari() {
+        return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
+        this.w.navigator.userAgent.indexOf('Chrome') === -1);
+    }
+
+    /**
+     * Returns true if the current browser determined to be Firefox
+     */
+    isFirefox() {
+        return typeof InstallTrigger !== 'undefined';
+    }
+
+    /**
+     * search through an array of objects, looking for the one with the
+     * tagged property matching the given key. tag defaults to 'id'.
+     * returns the index of the matching object, or -1 for no match.
+     */
+    find(key: string, array: Object[], tag: string = 'id'): number {
+        let idx: number;
+        const n: number = array.length;
+
+        for (idx = 0 ; idx < n; idx++) {
+            const d: Object = array[idx];
+            if (d[tag] === key) {
+                return idx;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * search through array to find (the first occurrence of) item,
+     * returning its index if found; otherwise returning -1.
+     */
+    inArray(item: any, array: any[]): number {
+        if (this.isA(array)) {
+            for (let i = 0; i < array.length; i++) {
+                if (array[i] === item) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * remove (the first occurrence of) the specified item from the given
+     * array, if any. Return true if the removal was made; false otherwise.
+     */
+    removeFromArray(item: any, array: any[]): boolean {
+        const i: number = this.inArray(item, array);
+        if (i >= 0) {
+            array.splice(i, 1);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * return true if the object is empty, return false otherwise
+     */
+    isEmptyObject(obj: Object): boolean {
+        for (const key in obj) {
+            if (true) { return false; }
+        }
+        return true;
+    }
+
+    /**
+     * returns true if the two objects have all the same properties
+     */
+    sameObjProps(obj1: Object, obj2: Object): boolean {
+        for (const key in obj1) {
+            if (obj1.hasOwnProperty(key)) {
+                if (!(obj1[key] === obj2[key])) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * returns true if the array contains the object
+     * does NOT use strict object reference equality,
+     * instead checks each property individually for equality
+     */
+    containsObj(arr: any[], obj: Object): boolean {
+        const len = arr.length;
+        for (let i = 0; i < len; i++) {
+            if (this.sameObjProps(arr[i], obj)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the given string with the first character capitalized.
+     */
+    cap(s: string): string {
+        return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
+    }
+
+    /**
+     * return the parameter without a px suffix
+     */
+    noPx(num: string): number {
+        return Number(num.replace(/px$/, ''));
+    }
+
+    /**
+     * return an element's given style property without px suffix
+     */
+    noPxStyle(elem: any, prop: string): number {
+        return Number(elem.style(prop).replace(/px$/, ''));
+    }
+
+    /**
+     * Return true if a str ends with suffix
+     */
+    endsWith(str: string, suffix: string) {
+        return str.indexOf(suffix, str.length - suffix.length) !== -1;
+    }
+
+    /**
+     * output debug message to console, if debug tag set...
+     * e.g. fs.debug('mytag', arg1, arg2, ...)
+     */
+    debug(tag, ...args) {
+        if (this.debugFlags.get(tag)) {
+//            this.log.debug(tag, args.join());
+        }
+    }
+
+    private parseDebugFlags(dbgstr: string): void {
+        const bits = dbgstr ? dbgstr.split(',') : [];
+        bits.forEach((key) => {
+            this.debugFlags.set(key, true);
+        });
+//        this.log.debug('Debug flags:', dbgstr);
+    }
+
+    /**
+      * Return true if the given debug flag was specified in the query params
+      */
+    debugOn(tag: string): boolean {
+        return this.debugFlags.get(tag);
+    }
+
+
+
+    // -----------------------------------------------------------------
+    // The next section deals with sanitizing external strings destined
+    // to be loaded via a .html() function call.
+    //
+    // See definition of matcher, evillist and whitelist at the top of this file
+
+    /*
+     * Returns true if the tag is in the evil list, (and is not an end-tag)
+     */
+    inEvilList(tag: any): boolean {
+        return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+    }
+
+    /*
+     * Returns an array of Matches of matcher in html
+     */
+    analyze(html: string): Match[] {
+        const matches: Match[] = [];
+        let match;
+
+        // extract all tags
+        while ((match = matcher.exec(html)) !== null) {
+            matches.push({
+                full: match[0],
+                name: match[1],
+                // NOTE: ignoring attributes {match[2].split(' ')} for now
+            });
+        }
+
+        return matches;
+    }
+
+    /*
+     * Returns a cleaned version of html
+     */
+    sanitize(html: string): string {
+        const matches: Match[] = this.analyze(html);
+
+        // completely obliterate evil tags and their contents...
+        evillist.forEach((tag) => {
+            const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
+            html = html.replace(re, '');
+        });
+
+        // filter out all but white-listed tags and end-tags
+        matches.forEach((tag) => {
+            if (whitelist.indexOf(tag.name) === -1) {
+                html = html.replace(tag.full, '');
+                if (this.inEvilList(tag)) {
+                    this.log.warn('Unsanitary HTML input -- ' +
+                        tag.full + ' detected!');
+                }
+            }
+        });
+
+        // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
+
+        return html;
+    }
+
+    /**
+     * add word to trie (word will be converted to uppercase)
+     * data associated with the word
+     * returns 'added' or 'updated'
+     */
+    addToTrie(trie, word, data) {
+        return new Trie(TrieOp.PLUS, trie, word, data);
+    }
+
+    /**
+     * remove word from trie (word will be converted to uppercase)
+     * returns 'removed' or 'absent'
+     */
+    removeFromTrie(trie, word) {
+        return new Trie(TrieOp.MINUS, trie, word);
+    }
+
+    /**
+     * lookup word (converted to uppercase) in trie
+     * returns:
+     *    undefined if the word is not in the trie
+     *    -1 for a partial match (word is a prefix to an existing word)
+     *    data for the word for an exact match
+     */
+    trieLookup(trie, word) {
+        const s = word.toUpperCase().split('');
+        let p = trie;
+        let n;
+
+        while (s.length) {
+            n = s.shift();
+            p = p[n];
+            if (!p) {
+                return undefined;
+            }
+        }
+        if (p._data) {
+            return p._data;
+        }
+        return -1;
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/util/keys.service.spec.ts b/web/gui2-fw-lib/lib/util/keys.service.spec.ts
new file mode 100644
index 0000000..6f2f69a
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/keys.service.spec.ts
@@ -0,0 +1,315 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import {ActivatedRoute, Params} from '@angular/router';
+
+import { KeysService, KeysToken } from './keys.service';
+import { FnService } from './fn.service';
+import { LogService } from '../log.service';
+import { NavService } from '../nav/nav.service';
+
+import {of} from 'rxjs';
+import * as d3 from 'd3';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockNavService {}
+
+/*
+ ONOS GUI -- Key Handler Service - Unit Tests
+ */
+describe('KeysService', () => {
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let ks: KeysService;
+    let mockWindow: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const qhs: any = {};
+    let d3Elem: any;
+    let elem: any;
+    let last: any;
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            },
+            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, mockWindow);
+
+        d3Elem = d3.select('body').append('p').attr('id', 'ptest');
+        elem = d3Elem.node();
+        last = {
+            view: null,
+            key: null,
+            code: null,
+            ev: null
+        };
+
+        TestBed.configureTestingModule({
+            providers: [KeysService,
+                { provide: FnService, useValue: fs},
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: NavService, useClass: MockNavService},
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+        ks = TestBed.get(KeysService);
+        ks.installOn(d3Elem);
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    afterEach(() => {
+        d3.select('#ptest').remove();
+    });
+
+    it('should be created', () => {
+        expect(ks).toBeTruthy();
+    });
+
+    it('should define api functions', () => {
+        expect(fs.areFunctions(ks, [
+            'installOn', 'keyBindings', 'unbindKeys', 'dialogKeys',
+            'addSeq', 'remSeq', 'gestureNotes', 'enableKeys', 'enableGlobalKeys',
+            'checkNotGlobal', 'getKeyBindings',
+            'matchSeq', 'whatKey', 'textFieldInput', 'keyIn', 'qhlion', 'qhlionShowHide',
+            'qhlionHintEsc', 'qhlionHintT', 'setupGlobalKeys', 'quickHelp',
+            'escapeKey', 'toggleTheme', 'filterMaskedKeys', 'unexParam',
+            'setKeyBindings', 'bindDialogKeys', 'unbindDialogKeys'
+        ])).toBeTruthy();
+    });
+
+    function jsKeyDown(element, code: string, keyName: string) {
+        const ev = new KeyboardEvent('keydown',
+            { code: code, key: keyName });
+
+        // Chromium Hack
+        // if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
+        //     Object.defineProperty(ev, 'keyCode', {
+        //         get: () => { return this.keyCodeVal; }
+        //     });
+        //     Object.defineProperty(ev, 'which', {
+        //         get: () => { return this.keyCodeVal; }
+        //     });
+        // }
+
+        if (ev.code !== code.toString()) {
+            console.warn('keyCode mismatch ' + ev.code +
+                '(' + ev.toString() + ') -> ' + code);
+        }
+        element.dispatchEvent(ev);
+    }
+
+    // === Key binding related tests
+    it('should start with default key bindings', () => {
+        const state = ks.getKeyBindings();
+        const gk = state.globalKeys;
+        const mk = state.maskedKeys;
+        const vk = state.viewKeys;
+        const vf = state.viewFunction;
+
+        expect(gk.length).toEqual(4);
+        ['backSlash', 'slash', 'esc', 'T'].forEach((k) => {
+            expect(fs.contains(gk, k)).toBeTruthy();
+        });
+
+        expect(mk.length).toEqual(3);
+        ['backSlash', 'slash', 'T'].forEach((k) => {
+            expect(fs.contains(mk, k)).toBeTruthy();
+        });
+
+        expect(vk.length).toEqual(0);
+        expect(vf).toBeFalsy();
+    });
+
+    function bindTestKeys(withDescs?) {
+        const keys = ['A', '1', 'F5', 'equals'];
+        const kb = {};
+
+        function cb(view, key, code, ev) {
+            last.view = view;
+            last.key = key;
+            last.code = code;
+            last.ev = ev;
+        }
+
+        function bind(k) {
+            return withDescs ?
+                [(view, key, code, ev) => {cb(view, key, code, ev); }, 'desc for key ' + k] :
+                (view, key, code, ev) => {cb(view, key, code, ev); };
+        }
+
+        keys.forEach((k) => {
+            kb[k] = bind(k);
+        });
+
+        ks.keyBindings(kb);
+    }
+
+    function verifyCall(key, code) {
+        // TODO: update expectation, when view tokens are implemented
+        expect(last.view).toEqual(KeysToken.KEYEV);
+        last.view = null;
+
+        expect(last.key).toEqual(key);
+        last.key = null;
+
+        expect(last.code).toEqual(code);
+        last.code = null;
+
+        expect(last.ev).toBeTruthy();
+        last.ev = null;
+    }
+
+    function verifyNoCall() {
+        expect(last.view).toBeNull();
+        expect(last.key).toBeNull();
+        expect(last.code).toBeNull();
+        expect(last.ev).toBeNull();
+    }
+
+    function verifyTestKeys() {
+        jsKeyDown(elem, '65', 'A'); // 'A'
+        verifyCall('A', '65');
+        jsKeyDown(elem, '66', 'B'); // 'B'
+        verifyNoCall();
+
+        jsKeyDown(elem, '49', '1'); // '1'
+        verifyCall('1', '49');
+        jsKeyDown(elem, '50', '2'); // '2'
+        verifyNoCall();
+
+        jsKeyDown(elem, '116', 'F5'); // 'F5'
+        verifyCall('F5', '116');
+        jsKeyDown(elem, '117', 'F6'); // 'F6'
+        verifyNoCall();
+
+        jsKeyDown(elem, '187', '='); // 'equals'
+        verifyCall('equals', '187');
+        jsKeyDown(elem, '189', '-'); // 'dash'
+        verifyNoCall();
+
+        const vk = ks.getKeyBindings().viewKeys;
+
+        expect(vk.length).toEqual(4);
+        ['A', '1', 'F5', 'equals'].forEach((k) => {
+            expect(fs.contains(vk, k)).toBeTruthy();
+        });
+
+        expect(ks.getKeyBindings().viewFunction).toBeFalsy();
+    }
+
+    it('should allow specific key bindings', () => {
+        bindTestKeys();
+        verifyTestKeys();
+    });
+
+    it('should allow specific key bindings with descriptions', () => {
+        bindTestKeys(true);
+        verifyTestKeys();
+    });
+
+    it('should warn about masked keys', () => {
+        const k = {
+            'space': (token, key, code, ev) => cb(token, key, code, ev),
+            'T': (token, key, code, ev) => cb(token, key, code, ev)
+        };
+        let count = 0;
+
+        function cb(token, key, code, ev) {
+            count++;
+            // console.debug('count = ' + count, token, key, code);
+        }
+
+        ks.keyBindings(k);
+
+        expect(logServiceSpy.warn).toHaveBeenCalledWith('setKeyBindings()\n: Key "T" is reserved');
+
+        // the 'T' key should NOT invoke our callback
+        expect(count).toEqual(0);
+        jsKeyDown(elem, '84', 'T'); // 'T'
+        expect(count).toEqual(0);
+
+        // but the 'space' key SHOULD invoke our callback
+        jsKeyDown(elem, '32', ' '); // 'space'
+        expect(count).toEqual(1);
+    });
+
+    it('should block keys when disabled', () => {
+        let cbCount = 0;
+
+        function cb() { cbCount++; }
+
+        function pressA() { jsKeyDown(elem, '65', 'A'); }  // 65 == 'A' keycode
+
+        ks.keyBindings({ A: () => cb() });
+
+        expect(cbCount).toBe(0);
+
+        pressA();
+        expect(cbCount).toBe(1);
+
+        ks.enableKeys(false);
+        pressA();
+        expect(cbCount).toBe(1);
+
+        ks.enableKeys(true);
+        pressA();
+        expect(cbCount).toBe(2);
+    });
+
+    // === Gesture notes related tests
+    it('should start with no notes', () => {
+        expect(ks.gestureNotes()).toEqual([]);
+    });
+
+    it('should allow us to add nodes', () => {
+        const notes = [
+            ['one', 'something about one'],
+            ['two', 'description of two']
+        ];
+        ks.gestureNotes(notes);
+
+        expect(ks.gestureNotes()).toEqual(notes);
+    });
+
+    it('should ignore non-arrays', () => {
+        ks.gestureNotes({foo: 4});
+        expect(ks.gestureNotes()).toEqual([]);
+    });
+
+    // Consider adding test to ensure array contains 2-tuples of strings
+});
diff --git a/web/gui2-fw-lib/lib/util/keys.service.ts b/web/gui2-fw-lib/lib/util/keys.service.ts
new file mode 100644
index 0000000..d4adb01
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/keys.service.ts
@@ -0,0 +1,405 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import * as d3 from 'd3';
+import { LogService } from '../log.service';
+import { FnService } from '../util/fn.service';
+import { LionService } from './lion.service';
+import { NavService } from '../nav/nav.service';
+
+export interface KeyHandler {
+    globalKeys: Object;
+    maskedKeys: Object;
+    dialogKeys: Object;
+    viewKeys: any;
+    viewFn: any;
+    viewGestures: string[][];
+}
+
+export enum KeysToken {
+    KEYEV = 'keyev'
+}
+
+/**
+ * ONOS GUI -- Keys Service Module.
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class KeysService {
+    enabled: boolean = true;
+    globalEnabled: boolean = true;
+    keyHandler: KeyHandler = <KeyHandler>{
+        globalKeys: {},
+        maskedKeys: {},
+        dialogKeys: {},
+        viewKeys: {},
+        viewFn: null,
+        viewGestures: [],
+    };
+
+    seq: any = {};
+    matching: boolean = false;
+    matched: string = '';
+    lookup: any;
+    textFieldDoesNotBlock: any = {
+        enter: 1,
+        esc: 1,
+    };
+    quickHelpShown: boolean = false;
+
+    constructor(
+        protected log: LogService,
+        protected fs: FnService,
+        protected ls: LionService,
+        protected ns: NavService
+    ) {
+        this.log.debug('KeyService constructed');
+    }
+
+    installOn(elem) {
+        this.log.debug('Installing keys handler');
+        elem.on('keydown', () => { this.keyIn(); });
+        this.setupGlobalKeys();
+    }
+
+    keyBindings(x) {
+        if (x === undefined) {
+            return this.getKeyBindings();
+        } else {
+            this.setKeyBindings(x);
+        }
+    }
+
+    unbindKeys() {
+        this.keyHandler.viewKeys = {};
+        this.keyHandler.viewFn = null;
+        this.keyHandler.viewGestures = [];
+    }
+
+    dialogKeys(x) {
+        if (x === undefined) {
+            this.unbindDialogKeys();
+        } else {
+            this.bindDialogKeys(x);
+        }
+    }
+
+    addSeq(word, data) {
+        this.fs.addToTrie(this.seq, word, data);
+    }
+
+    remSeq(word) {
+        this.fs.removeFromTrie(this.seq, word);
+    }
+
+    gestureNotes(g?) {
+        if (g === undefined) {
+            return this.keyHandler.viewGestures;
+        } else {
+            this.keyHandler.viewGestures = this.fs.isA(g) || [];
+        }
+    }
+
+    enableKeys(b) {
+        this.enabled = b;
+    }
+
+    enableGlobalKeys(b) {
+        this.globalEnabled = b;
+    }
+
+    checkNotGlobal(o) {
+        const oops = [];
+        if (this.fs.isO(o)) {
+            o.forEach((val, key) => {
+                if (this.keyHandler.globalKeys[key]) {
+                    oops.push(key);
+                }
+            });
+            if (oops.length) {
+                this.log.warn('Ignoring reserved global key(s):', oops.join(','));
+                oops.forEach((key) => {
+                    delete o[key];
+                });
+            }
+        }
+    }
+
+    protected matchSeq(key) {
+        if (!this.matching && key === 'shift-shift') {
+            this.matching = true;
+            return true;
+        }
+        if (this.matching) {
+            this.matched += key;
+            this.lookup = this.fs.trieLookup(this.seq, this.matched);
+            if (this.lookup === -1) {
+                return true;
+            }
+            this.matching = false;
+            this.matched = '';
+            if (!this.lookup) {
+                return;
+            }
+            // ee.cluck(lookup);
+            return true;
+        }
+    }
+
+    protected whatKey(code: number): string {
+        switch (code) {
+            case 8: return 'delete';
+            case 9: return 'tab';
+            case 13: return 'enter';
+            case 16: return 'shift';
+            case 27: return 'esc';
+            case 32: return 'space';
+            case 37: return 'leftArrow';
+            case 38: return 'upArrow';
+            case 39: return 'rightArrow';
+            case 40: return 'downArrow';
+            case 186: return 'semicolon';
+            case 187: return 'equals';
+            case 188: return 'comma';
+            case 189: return 'dash';
+            case 190: return 'dot';
+            case 191: return 'slash';
+            case 192: return 'backQuote';
+            case 219: return 'openBracket';
+            case 220: return 'backSlash';
+            case 221: return 'closeBracket';
+            case 222: return 'quote';
+            default:
+                if ((code >= 48 && code <= 57) ||
+                    (code >= 65 && code <= 90)) {
+                    return String.fromCharCode(code);
+                } else if (code >= 112 && code <= 123) {
+                    return 'F' + (code - 111);
+                }
+                return null;
+        }
+    }
+
+    protected textFieldInput() {
+        const t = d3.event.target.tagName.toLowerCase();
+        return t === 'input' || t === 'textarea';
+    }
+
+    protected keyIn() {
+        const event = d3.event;
+        // d3.events can set the keyCode, but unit tests based on KeyboardEvent
+        // cannot set keyCode since the attribute has been deprecated
+        const code = event.keyCode ? event.keyCode : event.code;
+        const codeNum: number = parseInt(code, 10);
+        let key = this.whatKey(codeNum);
+        this.log.debug('Key detected', event, key, event.code, event.keyCode);
+        const textBlockable = !this.textFieldDoesNotBlock[key];
+        const modifiers = [];
+
+        if (event.metaKey) {
+            modifiers.push('cmd');
+        }
+        if (event.altKey) {
+            modifiers.push('alt');
+        }
+        if (event.shiftKey) {
+            modifiers.push('shift');
+        }
+
+        if (!key) {
+            return;
+        }
+
+        modifiers.push(key);
+        key = modifiers.join('-');
+
+        if (textBlockable && this.textFieldInput()) {
+            return;
+        }
+
+        const kh: KeyHandler = this.keyHandler;
+        const gk = kh.globalKeys[key];
+        const gcb = this.fs.isF(gk) || (this.fs.isA(gk) && this.fs.isF(gk[0]));
+        const dk = kh.dialogKeys[key];
+        const dcb = this.fs.isF(dk);
+        const vk = kh.viewKeys[key];
+        const kl = this.fs.isF(kh.viewKeys._keyListener);
+        const vcb = this.fs.isF(vk) || (this.fs.isA(vk) && this.fs.isF(vk[0])) || this.fs.isF(kh.viewFn);
+        const token: KeysToken = KeysToken.KEYEV; // indicate this was a key-pressed event
+
+        event.stopPropagation();
+
+        if (this.enabled) {
+            if (this.matchSeq(key)) {
+                return;
+            }
+
+            // global callback?
+            if (gcb && gcb(token, key, code, event)) {
+                // if the event was 'handled', we are done
+                return;
+            }
+            // dialog callback?
+            if (dcb) {
+                dcb(token, key, code, event);
+                // assume dialog handled the event
+                return;
+            }
+            // otherwise, let the view callback have a shot
+            if (vcb) {
+                this.log.debug('Letting view callback have a shot', vcb, token, key, code, event );
+                vcb(token, key, code, event);
+            }
+            if (kl) {
+                kl(key);
+            }
+        }
+    }
+
+    // functions to obtain localized strings deferred from the setup of the
+    //  global key data structures.
+    protected qhlion() {
+        return this.ls.bundle('core.fw.QuickHelp');
+    }
+    protected qhlionShowHide() {
+        return this.qhlion()('qh_hint_show_hide_qh');
+    }
+
+    protected qhlionHintEsc() {
+        return this.qhlion()('qh_hint_esc');
+    }
+
+    protected qhlionHintT() {
+        return this.qhlion()('qh_hint_t');
+    }
+
+    protected setupGlobalKeys() {
+        (<any>Object).assign(this.keyHandler, {
+            globalKeys: {
+                backSlash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
+                slash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
+                esc: [(view, key, code, ev) => this.escapeKey(view, key, code, ev), this.qhlionHintEsc],
+                T: [(view, key, code, ev) => this.toggleTheme(view, key, code, ev), this.qhlionHintT],
+            },
+            globalFormat: ['backSlash', 'slash', 'esc', 'T'],
+
+            // Masked keys are global key handlers that always return true.
+            // That is, the view will never see the event for that key.
+            maskedKeys: {
+                slash: 1,
+                backSlash: 1,
+                T: 1,
+            },
+        });
+    }
+
+    protected quickHelp(view, key, code, ev) {
+        if (!this.globalEnabled) {
+            return false;
+        }
+        this.quickHelpShown = !this.quickHelpShown;
+        return true;
+    }
+
+    // returns true if we 'consumed' the ESC keypress, false otherwise
+    protected escapeKey(view, key, code, ev) {
+        this.quickHelpShown = false;
+        return this.ns.hideNav();
+    }
+
+    protected toggleTheme(view, key, code, ev) {
+        if (!this.globalEnabled) {
+            return false;
+        }
+        // ts.toggleTheme();
+        return true;
+    }
+
+    protected filterMaskedKeys(map: any, caller: any, remove: boolean): any[] {
+        const masked = [];
+        const msgs = [];
+
+        d3.map(map).keys().forEach((key) => {
+            if (this.keyHandler.maskedKeys[key]) {
+                masked.push(key);
+                msgs.push(caller, ': Key "' + key + '" is reserved');
+            }
+        });
+
+        if (msgs.length) {
+            this.log.warn(msgs.join('\n'));
+        }
+
+        if (remove) {
+            masked.forEach((k) => {
+                delete map[k];
+            });
+        }
+        return masked;
+    }
+
+    protected unexParam(fname, x) {
+        this.log.warn(fname, ': unexpected parameter-- ', x);
+    }
+
+    protected setKeyBindings(keyArg) {
+        const fname = 'setKeyBindings()';
+        const kFunc = this.fs.isF(keyArg);
+        const kMap = this.fs.isO(keyArg);
+
+        if (kFunc) {
+            // set general key handler callback
+            this.keyHandler.viewFn = kFunc;
+        } else if (kMap) {
+            this.filterMaskedKeys(kMap, fname, true);
+            this.keyHandler.viewKeys = kMap;
+        } else {
+            this.unexParam(fname, keyArg);
+        }
+    }
+
+    getKeyBindings() {
+        const gkeys = d3.map(this.keyHandler.globalKeys).keys();
+        const masked = d3.map(this.keyHandler.maskedKeys).keys();
+        const vkeys = d3.map(this.keyHandler.viewKeys).keys();
+        const vfn = !!this.fs.isF(this.keyHandler.viewFn);
+
+        return {
+            globalKeys: gkeys,
+            maskedKeys: masked,
+            viewKeys: vkeys,
+            viewFunction: vfn,
+        };
+    }
+
+    protected bindDialogKeys(map) {
+        const fname = 'bindDialogKeys()';
+        const kMap = this.fs.isO(map);
+
+        if (kMap) {
+            this.filterMaskedKeys(map, fname, true);
+            this.keyHandler.dialogKeys = kMap;
+        } else {
+            this.unexParam(fname, map);
+        }
+    }
+
+    protected unbindDialogKeys() {
+        this.keyHandler.dialogKeys = {};
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/util/lion.service.spec.ts b/web/gui2-fw-lib/lib/util/lion.service.spec.ts
new file mode 100644
index 0000000..b0c252c
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/lion.service.spec.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import { of } from 'rxjs';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { FnService } from '../util/fn.service';
+import { GlyphService } from '../svg/glyph.service';
+import { LionService } from './lion.service';
+import { UrlFnService } from '../remote/urlfn.service';
+import { WSock } from '../remote/wsock.service';
+import { WebSocketService, WsOptions } from '../remote/websocket.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockWSock {}
+
+class MockGlyphService {}
+
+class MockUrlFnService {}
+
+/**
+ * ONOS GUI -- Lion -- Localization Utilities - Unit Tests
+ */
+describe('LionService', () => {
+    let log: LogService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        windowMock = <any>{
+            location: <any> {
+                hostname: '',
+                host: '',
+                port: '',
+                protocol: '',
+                search: { debug: 'true'},
+                href: ''
+            }
+        };
+        fs = new FnService(ar, log, windowMock);
+
+        TestBed.configureTestingModule({
+            providers: [LionService,
+                { provide: FnService, useValue: fs },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: LogService, useValue: log },
+                { provide: UrlFnService, useClass: MockUrlFnService },
+                { provide: WSock, useClass: MockWSock },
+                { provide: WebSocketService, useClass: WebSocketService },
+                { provide: 'Window', useFactory: (() => windowMock ) },
+            ]
+        });
+    });
+
+    it('should be created', inject([LionService], (service: LionService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/util/lion.service.ts b/web/gui2-fw-lib/lib/util/lion.service.ts
new file mode 100644
index 0000000..288d54d
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/lion.service.ts
@@ -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.
+ *
+ */
+import { Injectable } from '@angular/core';
+import { LogService } from '../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+/**
+ * A definition of Lion data
+ */
+export interface Lion {
+    locale: any;
+    lion: any;
+}
+
+/**
+ * ONOS GUI -- Lion -- Localization Utilities
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class LionService {
+
+    ubercache: any[] = [];
+    loadCbs = new Map<string, () => void>([]); // A map of functions
+
+    /**
+     * Handler for uberlion event from WSS
+     */
+    uberlion(data: Lion) {
+        this.ubercache = data.lion;
+
+        this.log.info('LION service: Locale... [' + data.locale + ']');
+        this.log.info('LION service: Bundles installed...');
+
+        for (const p in this.ubercache) {
+            if (this.ubercache[p]) {
+                this.log.info('            :=> ', p);
+            }
+        }
+        // If any component had registered a callback, call it now
+        // that LION is loaded
+        for (const cbname of this.loadCbs.keys()) {
+            this.log.debug('Updating', cbname, 'with LION');
+            this.loadCbs.get(cbname)();
+        }
+
+        this.log.debug('LION service: uber-lion bundle received:', data);
+    }
+
+    constructor(
+        private log: LogService,
+        private wss: WebSocketService
+    ) {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            ['uberlion', (data) => this.uberlion(data) ]
+        ]));
+        this.log.debug('LionService constructed');
+    }
+
+    /**
+     * Returns a lion bundle (function) for the given bundle ID (string)
+     * returns a function that takes a string and returns a string
+     */
+    bundle(bundleId: string): (string) => string {
+        let bundleObj = this.ubercache[bundleId];
+
+        if (!bundleObj) {
+            this.log.warn('No lion bundle registered:', bundleId);
+            bundleObj = {};
+        }
+
+        return (key) =>  {
+            return bundleObj[key] || '?' + key + '?';
+        };
+    }
+}
diff --git a/web/gui2-fw-lib/lib/util/name-input/name-input.component.css b/web/gui2-fw-lib/lib/util/name-input/name-input.component.css
new file mode 100644
index 0000000..c8acf1b
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/name-input/name-input.component.css
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019-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 -- Name Input Component (layout) -- CSS file
+ */
+#name-input-dialog {
+    top: 140px;
+    padding: 12px;
+}
+
+#name-input-dialog h3 {
+    display: inline-block;
+    font-weight: bold;
+    font-size: 18pt;
+}
+
+#name-input-dialog p {
+    font-size: 12pt;
+}
+
+#name-input-dialog p.strong {
+    font-weight: bold;
+    padding: 8px;
+    text-align: center;
+}
diff --git a/web/gui2-fw-lib/lib/util/name-input/name-input.component.html b/web/gui2-fw-lib/lib/util/name-input/name-input.component.html
new file mode 100644
index 0000000..e505780
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/name-input/name-input.component.html
@@ -0,0 +1,22 @@
+<!--
+~ Copyright 2019-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="name-input-dialog" class="floatpanel dialog" [@niDlgState]="title!==''">
+    <h3>{{ title }}</h3><br>
+    <input #newName type="text" pattern="[a-zA-Z0-9\-:_]{3}" [placeholder]="placeholder" name="nameinput" [minLength]="minLen" [maxLength]="maxLen" width="40" required>
+    <p *ngIf="warning" class="warning strong">{{ warning }}</p>
+    <div tabindex="10" class="dialog-button" (click)="choice(true, newName.value)">OK</div>
+    <div tabindex="11" class="dialog-button" (click)="choice(false, '')">Cancel</div>
+</div>
diff --git a/web/gui2-fw-lib/lib/util/name-input/name-input.component.spec.ts b/web/gui2-fw-lib/lib/util/name-input/name-input.component.spec.ts
new file mode 100644
index 0000000..63ec2cb
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/name-input/name-input.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2019-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 {NameInputComponent} from './name-input.component';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+describe('NameInputComponent', () => {
+    let component: NameInputComponent;
+    let fixture: ComponentFixture<NameInputComponent>;
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            imports: [BrowserAnimationsModule],
+            declarations: [NameInputComponent]
+        })
+            .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(NameInputComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-fw-lib/lib/util/name-input/name-input.component.ts b/web/gui2-fw-lib/lib/util/name-input/name-input.component.ts
new file mode 100644
index 0000000..9d7116e
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/name-input/name-input.component.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019-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, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+
+export interface NameInputResult {
+    chosen: boolean;
+    name: string;
+}
+
+@Component({
+    selector: 'onos-name-input',
+    templateUrl: './name-input.component.html',
+    styleUrls: [
+        './name-input.component.css',
+        './name-input.theme.css',
+        '../../layer/dialog.css',
+        '../../layer/dialog.theme.css',
+        '../../widget/panel.css',
+        '../../widget/panel-theme.css'
+    ],
+    animations: [
+        trigger('niDlgState', [
+            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 NameInputComponent implements OnInit {
+    @Input() warning: string;
+    @Input() title: string = '';
+    @Input() pattern;
+    @Input() minLen = 4;
+    @Input() maxLen = 40;
+    @Input() placeholder = 'name';
+    @Output() chosen: EventEmitter<NameInputResult> = new EventEmitter();
+
+    constructor() {
+    }
+
+    ngOnInit() {
+    }
+
+    /**
+     * When OK or Cancel is pressed, send an event to parent with choice
+     */
+    choice(chosen: boolean, newName: string): void {
+        if (chosen && (newName === undefined || newName === '')) {
+            return;
+        }
+        this.chosen.emit(<NameInputResult>{
+            chosen: chosen,
+            name: newName
+        });
+    }
+}
diff --git a/web/gui2-fw-lib/lib/util/name-input/name-input.theme.css b/web/gui2-fw-lib/lib/util/name-input/name-input.theme.css
new file mode 100644
index 0000000..bb88fb2
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/name-input/name-input.theme.css
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019-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 -- name-input-dialog Component (theme) -- CSS file
+ */
+/* temporarily removed .light */
+#name-input-dialog p.strong {
+    color: white;
+    background-color: #ce5b58;
+}
+
+#name-input-dialog.floatpanel.dialog {
+    background-color: #ffffff;
+}
+
+#name-input-dialog p.strong {
+    color: white;
+    background-color: #ce5b58;
+}
diff --git a/web/gui2-fw-lib/lib/util/prefs.service.spec.ts b/web/gui2-fw-lib/lib/util/prefs.service.spec.ts
new file mode 100644
index 0000000..13892af
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/prefs.service.spec.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { PrefsService } from '../util/prefs.service';
+import { FnService } from '../util/fn.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+class MockFnService {}
+
+class MockWebSocketService {
+    createWebSocket() {}
+    isConnected() { return false; }
+    unbindHandlers() {}
+    bindHandlers() {}
+}
+
+/**
+ * ONOS GUI -- Util -- User Preference Service - Unit Tests
+ */
+describe('PrefsService', () => {
+    let log: LogService;
+    let windowMock: Window;
+
+    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'
+        }
+    };
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [PrefsService,
+                { provide: LogService, useValue: log },
+                { provide: FnService, useClass: MockFnService },
+                { provide: 'Window', useFactory: (() => windowMock ) },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+            ]
+        });
+    });
+
+    it('should be created', inject([PrefsService], (service: PrefsService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/util/prefs.service.ts b/web/gui2-fw-lib/lib/util/prefs.service.ts
new file mode 100644
index 0000000..ef55180
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/prefs.service.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 {Inject, Injectable} from '@angular/core';
+import { FnService } from './fn.service';
+import { LogService } from '../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+const UPDATE_PREFS: string = 'updatePrefs';
+const UPDATE_PREFS_REQ: string = 'updatePrefReq';
+
+
+/**
+ * ONOS GUI -- Util -- User Preference Service
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class PrefsService {
+    protected handlers: string[] = [];
+    cache: Object;
+    listeners: ((data) => void)[] = [];
+    constructor(
+        protected fs: FnService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+        @Inject('Window') private window: any
+    ) {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [UPDATE_PREFS, (data) => this.updatePrefs(data)]
+        ]));
+        this.handlers.push(UPDATE_PREFS);
+
+        // When index.html is fetched it is served up by MainIndexResource.java
+        // which fetches userPrefs in to the global scope.
+        // After that updates are done through WebSocket
+        this.cache = (<any>Object).assign({}, this.window['userPrefs']);
+
+        this.log.debug('PrefsService constructed');
+    }
+
+    setPrefs(name: string, obj: Object) {
+        // keep a cached copy of the object and send an update to server
+        this.cache[name] = obj;
+        this.wss.sendEvent(UPDATE_PREFS_REQ, { key: name, value: obj });
+    }
+    updatePrefs(data: any) {
+        this.cache = data;
+        this.listeners.forEach((lsnr) => lsnr(data) );
+    }
+
+    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: Object, qparams?: string) {
+        const obj = (<any>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): void {
+        const merged = this.cache[name] || {};
+        this.setPrefs(name, (<any>Object).assign(merged, obj));
+    }
+
+    /**
+     * Add a listener function
+     * This will get called back when an 'updatePrefs' message is received on WSS
+     * @param listener a function that can accept one param - data
+     */
+    addListener(listener: (data) => void): void {
+        this.listeners.push(listener);
+    }
+
+    removeListener(listener: (data) => void) {
+        this.listeners = this.listeners.filter((obj) => obj !== listener);
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/util/theme.service.spec.ts b/web/gui2-fw-lib/lib/util/theme.service.spec.ts
new file mode 100644
index 0000000..16d38e3
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/theme.service.spec.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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { ThemeService } from './theme.service';
+
+/**
+ * ONOS GUI -- Util -- Theme Service - Unit Tests
+ */
+describe('ThemeService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [ThemeService,
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([ThemeService], (service: ThemeService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/util/theme.service.ts b/web/gui2-fw-lib/lib/util/theme.service.ts
new file mode 100644
index 0000000..993d9f3
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/theme.service.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { LogService } from '../log.service';
+
+
+/**
+ * ONOS GUI -- Util -- Theme Service
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class ThemeService {
+    themes: string[] = ['light', 'dark'];
+    thidx = 0;
+
+    constructor(
+        private log: LogService
+    ) {
+        this.log.debug('ThemeService constructed');
+    }
+
+    getTheme(): string {
+        return this.themes[this.thidx];
+    }
+
+    themeStr(): string {
+        return this.themes.join(' ');
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/util/trie.ts b/web/gui2-fw-lib/lib/util/trie.ts
new file mode 100644
index 0000000..5e08061
--- /dev/null
+++ b/web/gui2-fw-lib/lib/util/trie.ts
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+export interface TrieC {
+    p: any;
+    s: string[];
+}
+
+export interface TrieT {
+    k: any;
+    p: any;
+    q: any;
+}
+
+export enum TrieRemoved {
+    REMOVED = 'removed',
+    ABSENT = 'absent'
+}
+
+export enum TrieInsert {
+    ADDED = 'added',
+    UPDATED = 'updated'
+}
+
+/**
+ * Combine TrieRemoved and TrieInsert in to a union type
+ */
+export type TrieActions = TrieRemoved | TrieInsert;
+
+export enum TrieOp {
+    PLUS = '+',
+    MINUS = '-'
+}
+
+
+export class Trie {
+    p: any;
+    w: string;
+    s: string[];
+    c: TrieC;
+    t: TrieT[];
+    x: number;
+    f1: (TrieC) => TrieC;
+    f2: () => TrieActions;
+    data: any;
+
+
+    constructor(
+        op: TrieOp,
+        trie: any,
+        word: string,
+        data?: any
+    ) {
+        this.p = trie;
+        this.w = word.toUpperCase();
+        this.s = this.w.split('');
+        this.c = { p: this.p, s: this.s },
+        this.t = [];
+        this.x = 0;
+        this.f1 = op === TrieOp.PLUS ? this.add : this.probe;
+        this.f2 = op === TrieOp.PLUS ? this.insert : this.remove;
+        this.data = data;
+        while (this.c.s.length) {
+            this.c = this.f1(this.c);
+        }
+    }
+
+    add(cAdded: TrieC): TrieC {
+        const q = cAdded.s.shift();
+        let np = cAdded.p[q];
+
+        if (!np) {
+            cAdded.p[q] = {};
+            np = cAdded.p[q];
+            this.x = 1;
+        }
+        return { p: np, s: cAdded.s };
+    }
+
+    probe(cProbed: TrieC): TrieC {
+        const q = cProbed.s.shift();
+        const k: number = Object.keys(cProbed.p).length;
+        const np = cProbed.p[q];
+
+        this.t.push({ q: q, k: k, p: cProbed.p });
+        if (!np) {
+            this.t = [];
+            return { p: [], s: [] };
+        }
+        return { p: np, s: cProbed.s };
+    }
+
+    insert(): TrieInsert {
+        this.c.p._data = this.data;
+        return this.x ? TrieInsert.ADDED : TrieInsert.UPDATED;
+    }
+
+    remove(): TrieRemoved {
+        if (this.t.length) {
+            this.t = this.t.reverse();
+            while (this.t.length) {
+                const d = this.t.shift();
+                delete d.p[d.q];
+                if (d.k > 1) {
+                    this.t = [];
+                }
+            }
+            return TrieRemoved.REMOVED;
+        }
+        return TrieRemoved.ABSENT;
+    }
+}
diff --git a/web/gui2-fw-lib/lib/widget/detailspanel.base.ts b/web/gui2-fw-lib/lib/widget/detailspanel.base.ts
new file mode 100644
index 0000000..97efee6
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/detailspanel.base.ts
@@ -0,0 +1,112 @@
+/*
+ * 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 } from '../util/fn.service';
+import { LogService } from '../log.service';
+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
+ */
+export interface DetailsResponse {
+    details: any;
+}
+
+/**
+ * Extends the PanelBase abstract class specifically for showing details
+ *
+ * This makes another call through WSS to the server for specific
+ * details to fill the panel with
+ *
+ * This replaces the detailspanel service in the old gui
+ */
+export abstract class DetailsPanelBaseImpl extends PanelBaseImpl {
+
+    @Input() id: string;
+    @Output() closeEvent = new EventEmitter<string>();
+
+    private root: string;
+    private req: string;
+    private resp: string;
+    private handlers: string[] = [];
+    public detailsData: any = {};
+    public closed: boolean = false;
+
+    constructor(
+        protected fs: FnService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+        protected tag: string,
+    ) {
+        super(fs, log);
+        this.root = tag + 's';
+        this.req = tag + 'DetailsRequest';
+        this.resp = tag + 'DetailsResponse';
+    }
+
+    /**
+     * When the details panel is created set up a listener on
+     * Web Socket for details responses
+     */
+    init() {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.resp, (data) => this.detailsPanelResponseCb(data)]
+        ]));
+        this.handlers.push(this.resp);
+    }
+
+    /**
+     * When the details panel is destroyed this should be called to
+     * de-register from the WebSocket
+     */
+    destroy() {
+        this.wss.unbindHandlers(this.handlers);
+    }
+
+    /**
+     * A callback that executes when the details data that was requested
+     * on WebSocketService arrives.
+     */
+    detailsPanelResponseCb(data: DetailsResponse) {
+        this.detailsData = data['details'];
+    }
+
+    /**
+     * Details Panel Data Request - should be called whenever row id changes
+     */
+    requestDetailsPanelData(query: any) {
+        this.closed = false;
+        // Do not send if the Web Socket hasn't opened
+        if (this.wss.isConnected()) {
+            if (this.fs.debugOn('panel')) {
+                this.log.debug('Details panel data REQUEST:', this.req, query);
+            }
+            this.wss.sendEvent(this.req, query);
+        }
+    }
+
+    /**
+     * this should be called when the details panel close button is clicked
+     */
+    close(): void {
+        this.closed = true;
+        this.id = null;
+        this.closeEvent.emit(this.id);
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/widget/panel-theme.css b/web/gui2-fw-lib/lib/widget/panel-theme.css
new file mode 100644
index 0000000..2291136
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/panel-theme.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.
+ */
+
+/*
+ ONOS GUI -- Panel Service (theme) -- CSS file
+ */
+
+.light .floatpanel {
+    background-color: white;
+    color: #3c3a3a;
+    border: 1px solid #c7c7c0;
+}
+
+.light .floatpanel hr {
+    border: 1px solid #c7c7c0;
+}
+
+.light .floatpanel .bottom tr:nth-child(odd) {
+    background-color: #f4f4f4;
+}
+
+.light .floatpanel .bottom tr:nth-child(even) {
+    background-color: #fbfbfb;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark .floatpanel {
+    background-color: #282528;
+    color: #888c8c;
+    border: 1px solid #364144;
+}
+
+.dark .floatpanel th {
+    background-color: #242424;
+}
+
+.dark .floatpanel h2 {
+    color: #dddddd;
+}
+
+.dark .floatpanel hr {
+    border: 1px solid #30303a;
+}
+
+.dark .floatpanel .bottom tr:nth-child(odd) {
+    background-color: #333333;
+}
+
+.dark .floatpanel .bottom tr:nth-child(even) {
+    background-color: #3a3a3a;
+}
diff --git a/web/gui2-fw-lib/lib/widget/panel.base.ts b/web/gui2-fw-lib/lib/widget/panel.base.ts
new file mode 100644
index 0000000..f568dc8
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/panel.base.ts
@@ -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.
+ */
+import { FnService } from '../util/fn.service';
+import { LogService } from '../log.service';
+
+
+/**
+ * Base model of panel view - implemented by Panel components
+ */
+export interface PanelBase {
+    showPanel(cb: any): void;
+    hidePanel(cb: any): void;
+    togglePanel(cb: any): void;
+    panelIsVisible(): boolean;
+}
+
+/**
+ * ONOS GUI -- Widget -- Panel Base class
+ *
+ * Replacing the panel service in the old implementation
+ */
+export abstract class PanelBaseImpl implements PanelBase {
+
+    on: boolean;
+
+    protected constructor(
+        protected fs: FnService,
+        protected log: LogService
+    ) {
+//        this.log.debug('Panel base class constructed');
+    }
+
+    showPanel(cb) {
+        this.on = true;
+    }
+
+    hidePanel(cb) {
+        this.on = false;
+    }
+
+    togglePanel(cb): boolean {
+        if (this.on) {
+            this.hidePanel(cb);
+        } else {
+            this.showPanel(cb);
+        }
+        return this.on;
+    }
+
+    panelIsVisible(): boolean {
+        return this.on;
+    }
+
+    /**
+     * A dummy implementation of the lionFn until the response is received and the LION
+     * bundle is received from the WebSocket
+     */
+    dummyLion(key: string): string {
+        return '%' + key + '%';
+    }
+}
diff --git a/web/gui2-fw-lib/lib/widget/panel.css b/web/gui2-fw-lib/lib/widget/panel.css
new file mode 100644
index 0000000..77fa9f4
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/panel.css
@@ -0,0 +1,61 @@
+/*
+ * 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 -- Panel Service (layout) -- CSS file
+ */
+
+.floatpanel {
+    position: absolute;
+    z-index: 100;
+    display: block;
+    top: 160px;
+    width: 544px;
+    right: -550px;
+    opacity: 100;
+
+    padding: 2px;
+    font-size: 10pt;
+}
+
+/* The following 4 are copied here from Theme until we sort out the
+ * theme service
+ */
+.floatpanel {
+    background-color: white;
+    color: #3c3a3a;
+    border: 1px solid #c7c7c0;
+}
+
+.floatpanel hr {
+    border: 1px solid #c7c7c0;
+}
+
+.floatpanel .bottom tr:nth-child(odd) {
+    background-color: #f4f4f4;
+}
+
+.floatpanel .bottom tr:nth-child(even) {
+    background-color: #fbfbfb;
+}
+
+.floatpanel.dialog {
+    top: 180px;
+}
+
+html[data-platform='iPad'] .floatpanel {
+    top: 80px;
+}
diff --git a/web/gui2-fw-lib/lib/widget/table.base.ts b/web/gui2-fw-lib/lib/widget/table.base.ts
new file mode 100644
index 0000000..36d7506
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/table.base.ts
@@ -0,0 +1,297 @@
+/*
+ * 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 } from '../util/fn.service';
+import { LogService } from '../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+import { Observable, of } from 'rxjs';
+
+const REFRESH_INTERVAL = 2000;
+const SEARCH_REGEX = '\\W';
+
+/**
+ * Model of table annotations within this table base class
+ */
+export interface TableAnnots {
+    noRowsMsg: string;
+}
+
+/**
+ * A model of data returned from Web Socket in a TableResponse
+ *
+ * There is an interface extending from this one in the parent component
+ */
+export interface TableResponse {
+    annots: any;
+    // There will be other parts to the response depending on table type
+    // Expect one called tag+'s' e.g. devices or apps
+}
+
+/**
+ * A criteria for filtering the tableData
+ */
+export interface TableFilter {
+    queryStr: string;
+    queryBy: string;
+    sortBy: string;
+}
+
+/**
+ * Enumerated values for the sort dir
+ */
+export enum SortDir {
+    asc = 'asc', desc = 'desc'
+}
+
+/**
+ * A structure to format sort params for table
+ * This is sent to WebSocket as part of table request
+ */
+export interface SortParams {
+    firstCol: string;
+    firstDir: SortDir;
+    secondCol: string;
+    secondDir: SortDir;
+}
+
+export interface PayloadParams {
+    devId: string;
+}
+
+
+/**
+ * ONOS GUI -- Widget -- Table Base class
+ */
+export abstract class TableBaseImpl {
+    // attributes from the interface
+    public annots: TableAnnots;
+    protected changedData: string[] = [];
+    protected payloadParams: PayloadParams;
+    protected sortParams: SortParams;
+    public selectCallback; // Function
+    protected parentSelCb = null;
+    protected responseCallback; // Function
+    public loadingIconShown: boolean = false;
+    selId: string = undefined;
+    tableData: any[] = [];
+    tableDataFilter: TableFilter;
+    toggleRefresh; // Function
+    autoRefresh: boolean = true;
+    autoRefreshTip: string = 'Toggle auto refresh'; // TODO: get LION string
+
+    readonly root: string;
+    readonly req: string;
+    readonly resp: string;
+    private refreshPromise: any = null;
+    private handlers: string[] = [];
+
+    protected constructor(
+        protected fs: FnService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+        protected tag: string,
+        protected idKey: string = 'id',
+        protected selCb = () => ({}) // Function
+    ) {
+        this.root = tag + 's';
+        this.req = tag + 'DataRequest';
+        this.resp = tag + 'DataResponse';
+
+        this.selectCallback = this.rowSelectionCb;
+        this.toggleRefresh = () => {
+            this.autoRefresh = !this.autoRefresh;
+            this.autoRefresh ? this.startRefresh() : this.stopRefresh();
+        };
+
+        // Mapped to the search and searchBy inputs in template
+        // Changes are handled through TableFilterPipe
+        this.tableDataFilter = <TableFilter>{
+            queryStr: '',
+            queryBy: '$',
+        };
+    }
+
+    init() {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.resp, (data) => this.tableDataResponseCb(data)]
+        ]));
+        this.handlers.push(this.resp);
+
+        this.annots = <TableAnnots>{
+            noRowsMsg: ''
+        };
+
+        // Now send the WebSocket request and make it repeat every 2 seconds
+        this.requestTableData();
+        this.startRefresh();
+        this.log.debug('TableBase initialized. Calling ', this.req,
+            'every', REFRESH_INTERVAL, 'ms');
+    }
+
+    destroy() {
+        this.wss.unbindHandlers(this.handlers);
+        this.stopRefresh();
+        this.loadingIconShown = false;
+    }
+
+    /**
+     * A callback that executes when the table data that was requested
+     * on WebSocketService arrives.
+     *
+     * Happens every 2 seconds
+     */
+    tableDataResponseCb(data: TableResponse) {
+        this.loadingIconShown = false;
+
+        const newTableData: any[] = Array.from(data[this.root]);
+        this.annots.noRowsMsg = data.annots.no_rows_msg;
+
+        // If the parents onResp() function is set then call it
+        if (this.responseCallback) {
+            this.responseCallback(data);
+        }
+        this.changedData = [];
+
+        // checks if data changed for row flashing
+        if (JSON.stringify(newTableData) !== JSON.stringify(this.tableData)) {
+            this.log.debug('table data has changed');
+            const oldTableData: any[] = this.tableData;
+            this.tableData = [...newTableData]; // ES6 spread syntax
+            // only flash the row if the data already exists
+            if (oldTableData.length > 0) {
+                for (const idx in newTableData) {
+                    if (!this.fs.containsObj(oldTableData, newTableData[idx])) {
+                        this.changedData.push(newTableData[idx][this.idKey]);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Table Data Request
+     * Pass in sort parameters and the set will be returned sorted
+     * In the old GUI there was also a query parameter, but this was not
+     * implemented on the server end
+     */
+    requestTableData() {
+        const p = (<any>Object).assign({}, this.sortParams, this.payloadParams);
+
+        // Allow it to sit in pending events
+        if (this.wss.isConnected()) {
+            if (this.fs.debugOn('table')) {
+                this.log.debug('Table data REQUEST:', this.req, p);
+            }
+            this.wss.sendEvent(this.req, p);
+            this.loadingIconShown = true;
+        }
+    }
+
+    /**
+     * Row Selected
+     */
+    rowSelectionCb(event: any, selRow: any): void {
+        const selId: string = selRow[this.idKey];
+        this.selId = (this.selId === selId) ? undefined : selId;
+        this.log.debug('Row', selId, 'selected');
+        if (this.parentSelCb) {
+            this.parentSelCb(event, selRow);
+        }
+    }
+
+    /**
+     * autoRefresh functions
+     */
+    startRefresh() {
+        this.refreshPromise =
+            setInterval(() => {
+                if (!this.loadingIconShown) {
+                    if (this.fs.debugOn('table')) {
+                        this.log.debug('Refreshing ' + this.root + ' page');
+                    }
+                    this.requestTableData();
+                }
+            }, REFRESH_INTERVAL);
+    }
+
+    stopRefresh() {
+        if (this.refreshPromise) {
+            clearInterval(this.refreshPromise);
+            this.refreshPromise = null;
+        }
+    }
+
+    isChanged(id: string): boolean {
+        return (this.fs.inArray(id, this.changedData) === -1) ? false : true;
+    }
+
+    /**
+     * A dummy implementation of the lionFn until the response is received and the LION
+     * bundle is received from the WebSocket
+     */
+    dummyLion(key: string): string {
+        return '%' + key + '%';
+    }
+
+    /**
+     * Change the sort order of the data returned
+     *
+     * sortParams are passed to the server by WebSocket and the data is
+     * returned sorted
+     *
+     * This is usually assigned to the (click) event on a column, and the column
+     * name passed in e.g. (click)="onSort('origin')
+     * If the column that is passed in is already the firstCol, then reverse its direction
+     * If a new column is passed in, then make the existing col the 2nd sort order
+     */
+    onSort(colName: string) {
+        if (this.sortParams.firstCol === colName) {
+            if (this.sortParams.firstDir === SortDir.desc) {
+                this.sortParams.firstDir = SortDir.asc;
+                return;
+            } else {
+                this.sortParams.firstDir = SortDir.desc;
+                return;
+            }
+        } else {
+            this.sortParams.secondCol = this.sortParams.firstCol;
+            this.sortParams.secondDir = this.sortParams.firstDir;
+            this.sortParams.firstCol = colName;
+            this.sortParams.firstDir = SortDir.desc;
+        }
+        this.log.debug('Sort params', this.sortParams);
+        this.requestTableData();
+    }
+
+    sortIcon(column: string): string {
+        if (this.sortParams.firstCol === column) {
+            if (this.sortParams.firstDir === SortDir.asc) {
+                return 'upArrow';
+            } else {
+                return 'downArrow';
+            }
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * De-selects the row
+     */
+    deselectRow(event) {
+        this.log.debug('Details panel close event');
+        this.selId = event;
+    }
+}
diff --git a/web/gui2-fw-lib/lib/widget/table.css b/web/gui2-fw-lib/lib/widget/table.css
new file mode 100644
index 0000000..82cfb20
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/table.css
@@ -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.
+ */
+
+/* ------ for summary-list tables (layout) ------ */
+
+div.summary-list {
+    margin: 0 20px 16px 10px;
+    font-size: 10pt;
+    border-spacing: 0;
+}
+
+div.summary-list table {
+    border-collapse: collapse;
+    table-layout: fixed;
+    empty-cells: show;
+    margin: 0;
+}
+
+div.summary-list div.table-body {
+    overflow-y: scroll;
+}
+
+div.summary-list div.table-body::-webkit-scrollbar {
+    display: none;
+}
+
+div.summary-list div.table-body tr.no-data td {
+    text-align: center;
+    font-style: italic;
+}
+
+
+/* highlighting */
+div.summary-list tr {
+    transition: background-color 500ms;
+}
+
+div.summary-list td {
+    padding: 4px;
+    text-align: left;
+    word-wrap: break-word;
+    font-size: 10pt;
+}
+
+div.summary-list td.table-icon {
+    width: 42px;
+    padding-top: 4px;
+    padding-bottom: 0px;
+    padding-left: 4px;
+    text-align: center;
+}
+
+div.summary-list .table-header 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;
+}
+
+/* rows are selectable */
+div.summary-list .table-body td {
+    cursor: pointer;
+}
+
+/* Tabular view controls */
+
+div.tabular-header .search {
+    margin: 0 0 10px 10px;
+}
+
+
+div.tabular-header div.ctrl-btns {
+    display: inline-block;
+    float: right;
+    height: 44px;
+    margin-top: 24px;
+    margin-right: 20px;
+    position: absolute;
+    right: 0px;
+}
+
+div.tabular-header div.ctrl-btns div {
+    display: inline-block;
+    cursor: pointer;
+}
+
+div.tabular-header div.ctrl-btns div.separator  {
+    width: 0;
+    height: 40px;
+    padding: 0;
+    border-right: 1px solid #c7c7c0;
+}
diff --git a/web/gui2-fw-lib/lib/widget/table.theme.css b/web/gui2-fw-lib/lib/widget/table.theme.css
new file mode 100644
index 0000000..cc7e7e6
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/table.theme.css
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+/* ------ for summary-list tables (theme) ------ */
+
+.light div.summary-list, .table-header th {
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+.light div.summary-list, td {
+    color: #3c3a3a;
+}
+
+.light div.summary-list, tr:nth-child(even) {
+    background-color: #f4f4f4;
+}
+.light div.summary-list, tr:nth-child(odd) {
+    background-color: #fbfbfb;
+}
+
+.light div.summary-list, tr.selected {
+    background-color: #dbeffc !important;
+}
+
+
+.light div.summary-list, tr.data-change {
+    background-color: #FDFFDC;
+}
+
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+.light .ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #e0dfd6;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+.light .ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.light .ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+.light .ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
+    fill: #518ecc;
+}
+.light .ctrl-btns div.current-view svg.embeddedIcon g.icon use {
+    fill: white;
+}
+
+/* REFRESH */
+.light .ctrl-btns div.refresh svg.embeddedIcon g.icon use {
+    fill: #cdeff2;
+}
+.light .ctrl-btns div.refresh:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+.light .ctrl-btns div.refresh.active svg.embeddedIcon g.icon use {
+    fill: #009fdb;
+}
+.light .ctrl-btns div.refresh.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark div.summary-list .table-header td {
+    background-color: #222222;
+    color: #cccccc;
+}
+
+.dark div.summary-list td {
+    /* note: don't put background-color here */
+    color: #cccccc;
+}
+.dark div.summary-list tr.no-data td {
+    background-color: #333333;
+}
+
+.dark div.summary-list tr:nth-child(even) {
+    background-color: #333333;
+}
+.dark div.summary-list tr:nth-child(odd) {
+    background-color: #3a3a3a;
+}
+
+.dark div.summary-list tr.selected {
+    background-color: #304860 !important;
+}
+
+
+.dark div.summary-list tr.data-change {
+    background-color: #423708;
+}
+
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+.dark .ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #444444;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+.dark .ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.dark .ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+.dark .ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
+    fill: #518ecc;
+}
+.dark .ctrl-btns div.current-view svg.embeddedIcon g.icon use {
+    fill: #dddddd;
+}
+
+/* REFRESH */
+.dark .ctrl-btns div.refresh svg.embeddedIcon g.icon use {
+    fill: #364144;
+}
+.dark .ctrl-btns div.refresh:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+.dark .ctrl-btns div.refresh.active svg.embeddedIcon g.icon use {
+    fill: #0074a6;
+}
+.dark .ctrl-btns div.refresh.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
diff --git a/web/gui2-fw-lib/lib/widget/tablefilter.pipe.spec.ts b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.spec.ts
new file mode 100644
index 0000000..8832feb
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.spec.ts
@@ -0,0 +1,104 @@
+/*
+ * 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 { TableFilterPipe } from './tablefilter.pipe';
+import { TableFilter } from './table.base';
+
+describe('TableFilterPipe', () => {
+
+    const pipe = new TableFilterPipe();
+    const items: any[] = new Array();
+    // Array item 0
+    items.push({
+        id: 'abc',
+        title: 'def',
+        origin: 'ghi'
+    });
+    // Array item 1
+    items.push({
+        id: 'pqr',
+        title: 'stu',
+        origin: 'vwx'
+    });
+    // Array item 2
+    items.push({
+        id: 'dog',
+        title: 'mouse',
+        origin: 'cat'
+    });
+
+
+    it('create an instance', () => {
+        expect(pipe).toBeTruthy();
+    });
+
+    it('expect it to handle empty search', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: '', queryBy: 'title'});
+        expect(filteredItems).toEqual(items);
+    });
+
+    it('expect it to handle empty items', () => {
+        const filteredItems: any[] =
+            pipe.transform(new Array(), <TableFilter>{queryStr: 'de', queryBy: 'title'});
+        expect(filteredItems).toEqual(new Array());
+    });
+
+
+    it('expect it to match 0 by title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'de', queryBy: 'title'});
+        expect(filteredItems).toEqual(items.slice(0, 1));
+    });
+
+    it('expect it to match 1 by title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'st', queryBy: 'title'});
+        expect(filteredItems).toEqual(items.slice(1, 2));
+    });
+
+    it('expect it to match 1 by uppercase title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'sT', queryBy: 'title'});
+        expect(filteredItems).toEqual(items.slice(1, 2));
+    });
+
+    it('expect it to not match by title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'pq', queryBy: 'title'});
+        expect(filteredItems.length).toEqual(0);
+    });
+
+    it('expect it to match 1 by all fields', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'pq', queryBy: '$'});
+        expect(filteredItems).toEqual(items.slice(1, 2));
+    });
+
+    it('expect it to not match by all fields', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'yz', queryBy: '$'});
+        expect(filteredItems.length).toEqual(0);
+    });
+
+    /**
+     * Check that items one and two contain a 't' - title=stu and origin=cat
+     */
+    it('expect it to match 1,2 by all fields', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 't', queryBy: '$'});
+        expect(filteredItems).toEqual(items.slice(1));
+    });
+});
diff --git a/web/gui2-fw-lib/lib/widget/tablefilter.pipe.ts b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.ts
new file mode 100644
index 0000000..8d8f21e
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.ts
@@ -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.
+ */
+import { Pipe, PipeTransform } from '@angular/core';
+import { TableFilter } from './table.base';
+
+/**
+ * Only return the tabledata that matches filtering with some queries
+ *
+ * Note: the pipe is marked pure here as we need to filter on the
+ * content of the filter object (it's not a primitive type)
+ */
+@Pipe({
+  name: 'filter',
+  pure: false
+})
+export class TableFilterPipe implements PipeTransform {
+
+    /**
+     * From an array of table items just return those that match the filter
+     */
+    transform(items: any[], tableDataFilter: TableFilter): any[] {
+        if (!items) {
+            return [];
+        }
+        if (!tableDataFilter.queryStr) {
+            return items;
+        }
+
+        const queryStr = tableDataFilter.queryStr.toLowerCase();
+
+        return items.filter( it => {
+            if (tableDataFilter.queryBy === '$') {
+                const t1 = (<any>Object).values(it);
+                const t2 = (<any>Object).values(it).filter(value => {
+                               return JSON.stringify(value).toLowerCase().indexOf(queryStr) !== -1;
+                           });
+                return (<any>Object).values(it).filter(value => {
+                    return JSON.stringify(value).toLowerCase().indexOf(queryStr) !== -1;
+                }).length > 0;
+            } else {
+                return it[tableDataFilter.queryBy].toLowerCase().includes(queryStr);
+            }
+        });
+    }
+}
diff --git a/web/gui2-fw-lib/lib/widget/tableresize.directive.spec.ts b/web/gui2-fw-lib/lib/widget/tableresize.directive.spec.ts
new file mode 100644
index 0000000..b1e87e6
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tableresize.directive.spec.ts
@@ -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.
+ */
+import { TestBed, inject } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { TableResizeDirective } from './tableresize.directive';
+import { LogService } from '..//log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { MastService } from '../mast/mast.service';
+import { FnService } from '../util/fn.service';
+
+class MockMastService {}
+
+class MockFnService extends FnService {
+    constructor(ar: ActivatedRoute, log: LogService, w: Window) {
+        super(ar, log, w);
+    }
+}
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Widget -- Table Resize Directive - Unit Tests
+ */
+describe('TableResizeDirective', () => {
+    let log: LogService;
+    let mockWindow: Window;
+    let ar: ActivatedRoute;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute(['debug', 'DetectBrowserDirective']);
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            }
+        };
+        TestBed.configureTestingModule({
+            providers: [ TableResizeDirective,
+                { provide: FnService, useValue: new MockFnService(ar, log, mockWindow) },
+                { provide: LogService, useValue: log },
+                { provide: MastService, useClass: MockMastService },
+                { provide: 'Window', useFactory: (() => mockWindow ) },
+            ]
+        });
+    });
+
+    afterEach(() => {
+        log = null;
+    });
+
+    it('should create an instance', inject([TableResizeDirective], (directive: TableResizeDirective) => {
+        expect(directive).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/widget/tableresize.directive.ts b/web/gui2-fw-lib/lib/widget/tableresize.directive.ts
new file mode 100644
index 0000000..5d3a9eb
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tableresize.directive.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { AfterContentChecked, Directive, 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
+ */
+@Directive({
+    selector: '[onosTableResize]',
+})
+export class TableResizeDirective implements AfterContentChecked {
+
+    pdg = 22;
+    tables: any;
+
+    constructor(protected fs: FnService,
+        protected log: LogService,
+        protected mast: MastService,
+        @Inject('Window') private w: any) {
+
+        log.info('TableResizeDirective constructed');
+    }
+
+    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.adjustTable(tables, wsz.width, wsz.height);
+    }
+
+    @HostListener('window:resize', ['$event.target'])
+    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');
+    }
+
+}