Added in panel support - details panels

Change-Id: I2803edd6fe12cb0d97a2d3c45a692ea701786dd2
diff --git a/web/gui2/src/main/webapp/app/fw/widget/table.base.ts b/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
new file mode 100644
index 0000000..0093f72
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
@@ -0,0 +1,285 @@
+/*
+ * 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 { LoadingService } from '../layer/loading.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
+ */
+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;
+}
+
+/**
+ * ONOS GUI -- Widget -- Table Base class
+ */
+export abstract class TableBaseImpl {
+    // attributes from the interface
+    protected annots: TableAnnots;
+    protected changedData: string[] = [];
+    protected payloadParams: any;
+    protected sortParams: SortParams;
+    protected selectCallback; // Function
+    protected parentSelCb = null;
+    protected responseCallback; // Function
+    selId: string = undefined;
+    tableData: any[] = [];
+    tableDataFilter: TableFilter;
+    toggleRefresh; // Function
+    autoRefresh: boolean = true;
+    autoRefreshTip: string = 'Toggle auto refresh'; // TODO: get LION string
+
+    private root: string;
+    private req: string;
+    private resp: string;
+    private refreshPromise: any = null;
+    private handlers: string[] = [];
+
+    constructor(
+        protected fs: FnService,
+        protected ls: LoadingService,
+        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.ls.stop();
+    }
+
+    /**
+     * A callback that executes when the table data that was requested
+     * on WebSocketService arrives.
+     *
+     * Happens every 2 seconds
+     */
+    tableDataResponseCb(data: TableResponse) {
+        this.ls.stop();
+
+        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 = 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.ls.start();
+        }
+    }
+
+    /**
+     * 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.ls.waiting()) {
+                    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 '';
+        }
+    }
+}