Added in panel support - details panels

Change-Id: I2803edd6fe12cb0d97a2d3c45a692ea701786dd2
diff --git a/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.spec.ts b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.spec.ts
new file mode 100644
index 0000000..2b07f1c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.spec.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2015-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/src/main/webapp/app/fw/remote/websocket.service.spec.ts b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.spec.ts
new file mode 100644
index 0000000..57d8d34
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.spec.ts
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2015-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);
+    });
+
+    it('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 === 1);
+        expect(o.cb === noop);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+        o = wss.addOpenListener(noop);
+        expect(o.id === 2);
+        expect(o.cb === noop);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+    });
+
+    it('should log error if callback not a function, addOpenListener',
+        () => {
+            const o = wss.addOpenListener(null);
+            expect(o.id === 1);
+            expect(o.cb === null);
+            expect(o.error === '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/src/main/webapp/app/fw/remote/websocket.service.ts b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
index 18dd734..b5eeaa6 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
@@ -385,6 +385,10 @@
         }
     }
 
+    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