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">\</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. '&' -> '&'
+
+ 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');
+ }
+
+}