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