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/widget/detailspanel.base.ts b/web/gui2-fw-lib/lib/widget/detailspanel.base.ts
new file mode 100644
index 0000000..97efee6
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/detailspanel.base.ts
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { FnService } from '../util/fn.service';
+import { LogService } from '../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+import { PanelBaseImpl } from './panel.base';
+import { Output, EventEmitter, Input } from '@angular/core';
+
+/**
+ * A generic model of the data returned from the *DetailsResponse
+ */
+export interface DetailsResponse {
+    details: any;
+}
+
+/**
+ * Extends the PanelBase abstract class specifically for showing details
+ *
+ * This makes another call through WSS to the server for specific
+ * details to fill the panel with
+ *
+ * This replaces the detailspanel service in the old gui
+ */
+export abstract class DetailsPanelBaseImpl extends PanelBaseImpl {
+
+    @Input() id: string;
+    @Output() closeEvent = new EventEmitter<string>();
+
+    private root: string;
+    private req: string;
+    private resp: string;
+    private handlers: string[] = [];
+    public detailsData: any = {};
+    public closed: boolean = false;
+
+    constructor(
+        protected fs: FnService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+        protected tag: string,
+    ) {
+        super(fs, log);
+        this.root = tag + 's';
+        this.req = tag + 'DetailsRequest';
+        this.resp = tag + 'DetailsResponse';
+    }
+
+    /**
+     * When the details panel is created set up a listener on
+     * Web Socket for details responses
+     */
+    init() {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.resp, (data) => this.detailsPanelResponseCb(data)]
+        ]));
+        this.handlers.push(this.resp);
+    }
+
+    /**
+     * When the details panel is destroyed this should be called to
+     * de-register from the WebSocket
+     */
+    destroy() {
+        this.wss.unbindHandlers(this.handlers);
+    }
+
+    /**
+     * A callback that executes when the details data that was requested
+     * on WebSocketService arrives.
+     */
+    detailsPanelResponseCb(data: DetailsResponse) {
+        this.detailsData = data['details'];
+    }
+
+    /**
+     * Details Panel Data Request - should be called whenever row id changes
+     */
+    requestDetailsPanelData(query: any) {
+        this.closed = false;
+        // Do not send if the Web Socket hasn't opened
+        if (this.wss.isConnected()) {
+            if (this.fs.debugOn('panel')) {
+                this.log.debug('Details panel data REQUEST:', this.req, query);
+            }
+            this.wss.sendEvent(this.req, query);
+        }
+    }
+
+    /**
+     * this should be called when the details panel close button is clicked
+     */
+    close(): void {
+        this.closed = true;
+        this.id = null;
+        this.closeEvent.emit(this.id);
+    }
+
+}
diff --git a/web/gui2-fw-lib/lib/widget/panel-theme.css b/web/gui2-fw-lib/lib/widget/panel-theme.css
new file mode 100644
index 0000000..2291136
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/panel-theme.css
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Panel Service (theme) -- CSS file
+ */
+
+.light .floatpanel {
+    background-color: white;
+    color: #3c3a3a;
+    border: 1px solid #c7c7c0;
+}
+
+.light .floatpanel hr {
+    border: 1px solid #c7c7c0;
+}
+
+.light .floatpanel .bottom tr:nth-child(odd) {
+    background-color: #f4f4f4;
+}
+
+.light .floatpanel .bottom tr:nth-child(even) {
+    background-color: #fbfbfb;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark .floatpanel {
+    background-color: #282528;
+    color: #888c8c;
+    border: 1px solid #364144;
+}
+
+.dark .floatpanel th {
+    background-color: #242424;
+}
+
+.dark .floatpanel h2 {
+    color: #dddddd;
+}
+
+.dark .floatpanel hr {
+    border: 1px solid #30303a;
+}
+
+.dark .floatpanel .bottom tr:nth-child(odd) {
+    background-color: #333333;
+}
+
+.dark .floatpanel .bottom tr:nth-child(even) {
+    background-color: #3a3a3a;
+}
diff --git a/web/gui2-fw-lib/lib/widget/panel.base.ts b/web/gui2-fw-lib/lib/widget/panel.base.ts
new file mode 100644
index 0000000..f568dc8
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/panel.base.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { FnService } from '../util/fn.service';
+import { LogService } from '../log.service';
+
+
+/**
+ * Base model of panel view - implemented by Panel components
+ */
+export interface PanelBase {
+    showPanel(cb: any): void;
+    hidePanel(cb: any): void;
+    togglePanel(cb: any): void;
+    panelIsVisible(): boolean;
+}
+
+/**
+ * ONOS GUI -- Widget -- Panel Base class
+ *
+ * Replacing the panel service in the old implementation
+ */
+export abstract class PanelBaseImpl implements PanelBase {
+
+    on: boolean;
+
+    protected constructor(
+        protected fs: FnService,
+        protected log: LogService
+    ) {
+//        this.log.debug('Panel base class constructed');
+    }
+
+    showPanel(cb) {
+        this.on = true;
+    }
+
+    hidePanel(cb) {
+        this.on = false;
+    }
+
+    togglePanel(cb): boolean {
+        if (this.on) {
+            this.hidePanel(cb);
+        } else {
+            this.showPanel(cb);
+        }
+        return this.on;
+    }
+
+    panelIsVisible(): boolean {
+        return this.on;
+    }
+
+    /**
+     * A dummy implementation of the lionFn until the response is received and the LION
+     * bundle is received from the WebSocket
+     */
+    dummyLion(key: string): string {
+        return '%' + key + '%';
+    }
+}
diff --git a/web/gui2-fw-lib/lib/widget/panel.css b/web/gui2-fw-lib/lib/widget/panel.css
new file mode 100644
index 0000000..77fa9f4
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/panel.css
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Panel Service (layout) -- CSS file
+ */
+
+.floatpanel {
+    position: absolute;
+    z-index: 100;
+    display: block;
+    top: 160px;
+    width: 544px;
+    right: -550px;
+    opacity: 100;
+
+    padding: 2px;
+    font-size: 10pt;
+}
+
+/* The following 4 are copied here from Theme until we sort out the
+ * theme service
+ */
+.floatpanel {
+    background-color: white;
+    color: #3c3a3a;
+    border: 1px solid #c7c7c0;
+}
+
+.floatpanel hr {
+    border: 1px solid #c7c7c0;
+}
+
+.floatpanel .bottom tr:nth-child(odd) {
+    background-color: #f4f4f4;
+}
+
+.floatpanel .bottom tr:nth-child(even) {
+    background-color: #fbfbfb;
+}
+
+.floatpanel.dialog {
+    top: 180px;
+}
+
+html[data-platform='iPad'] .floatpanel {
+    top: 80px;
+}
diff --git a/web/gui2-fw-lib/lib/widget/table.base.ts b/web/gui2-fw-lib/lib/widget/table.base.ts
new file mode 100644
index 0000000..36d7506
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/table.base.ts
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { FnService } from '../util/fn.service';
+import { LogService } from '../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+import { Observable, of } from 'rxjs';
+
+const REFRESH_INTERVAL = 2000;
+const SEARCH_REGEX = '\\W';
+
+/**
+ * Model of table annotations within this table base class
+ */
+export interface TableAnnots {
+    noRowsMsg: string;
+}
+
+/**
+ * A model of data returned from Web Socket in a TableResponse
+ *
+ * There is an interface extending from this one in the parent component
+ */
+export interface TableResponse {
+    annots: any;
+    // There will be other parts to the response depending on table type
+    // Expect one called tag+'s' e.g. devices or apps
+}
+
+/**
+ * A criteria for filtering the tableData
+ */
+export interface TableFilter {
+    queryStr: string;
+    queryBy: string;
+    sortBy: string;
+}
+
+/**
+ * Enumerated values for the sort dir
+ */
+export enum SortDir {
+    asc = 'asc', desc = 'desc'
+}
+
+/**
+ * A structure to format sort params for table
+ * This is sent to WebSocket as part of table request
+ */
+export interface SortParams {
+    firstCol: string;
+    firstDir: SortDir;
+    secondCol: string;
+    secondDir: SortDir;
+}
+
+export interface PayloadParams {
+    devId: string;
+}
+
+
+/**
+ * ONOS GUI -- Widget -- Table Base class
+ */
+export abstract class TableBaseImpl {
+    // attributes from the interface
+    public annots: TableAnnots;
+    protected changedData: string[] = [];
+    protected payloadParams: PayloadParams;
+    protected sortParams: SortParams;
+    public selectCallback; // Function
+    protected parentSelCb = null;
+    protected responseCallback; // Function
+    public loadingIconShown: boolean = false;
+    selId: string = undefined;
+    tableData: any[] = [];
+    tableDataFilter: TableFilter;
+    toggleRefresh; // Function
+    autoRefresh: boolean = true;
+    autoRefreshTip: string = 'Toggle auto refresh'; // TODO: get LION string
+
+    readonly root: string;
+    readonly req: string;
+    readonly resp: string;
+    private refreshPromise: any = null;
+    private handlers: string[] = [];
+
+    protected constructor(
+        protected fs: FnService,
+        protected log: LogService,
+        protected wss: WebSocketService,
+        protected tag: string,
+        protected idKey: string = 'id',
+        protected selCb = () => ({}) // Function
+    ) {
+        this.root = tag + 's';
+        this.req = tag + 'DataRequest';
+        this.resp = tag + 'DataResponse';
+
+        this.selectCallback = this.rowSelectionCb;
+        this.toggleRefresh = () => {
+            this.autoRefresh = !this.autoRefresh;
+            this.autoRefresh ? this.startRefresh() : this.stopRefresh();
+        };
+
+        // Mapped to the search and searchBy inputs in template
+        // Changes are handled through TableFilterPipe
+        this.tableDataFilter = <TableFilter>{
+            queryStr: '',
+            queryBy: '$',
+        };
+    }
+
+    init() {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.resp, (data) => this.tableDataResponseCb(data)]
+        ]));
+        this.handlers.push(this.resp);
+
+        this.annots = <TableAnnots>{
+            noRowsMsg: ''
+        };
+
+        // Now send the WebSocket request and make it repeat every 2 seconds
+        this.requestTableData();
+        this.startRefresh();
+        this.log.debug('TableBase initialized. Calling ', this.req,
+            'every', REFRESH_INTERVAL, 'ms');
+    }
+
+    destroy() {
+        this.wss.unbindHandlers(this.handlers);
+        this.stopRefresh();
+        this.loadingIconShown = false;
+    }
+
+    /**
+     * A callback that executes when the table data that was requested
+     * on WebSocketService arrives.
+     *
+     * Happens every 2 seconds
+     */
+    tableDataResponseCb(data: TableResponse) {
+        this.loadingIconShown = false;
+
+        const newTableData: any[] = Array.from(data[this.root]);
+        this.annots.noRowsMsg = data.annots.no_rows_msg;
+
+        // If the parents onResp() function is set then call it
+        if (this.responseCallback) {
+            this.responseCallback(data);
+        }
+        this.changedData = [];
+
+        // checks if data changed for row flashing
+        if (JSON.stringify(newTableData) !== JSON.stringify(this.tableData)) {
+            this.log.debug('table data has changed');
+            const oldTableData: any[] = this.tableData;
+            this.tableData = [...newTableData]; // ES6 spread syntax
+            // only flash the row if the data already exists
+            if (oldTableData.length > 0) {
+                for (const idx in newTableData) {
+                    if (!this.fs.containsObj(oldTableData, newTableData[idx])) {
+                        this.changedData.push(newTableData[idx][this.idKey]);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Table Data Request
+     * Pass in sort parameters and the set will be returned sorted
+     * In the old GUI there was also a query parameter, but this was not
+     * implemented on the server end
+     */
+    requestTableData() {
+        const p = (<any>Object).assign({}, this.sortParams, this.payloadParams);
+
+        // Allow it to sit in pending events
+        if (this.wss.isConnected()) {
+            if (this.fs.debugOn('table')) {
+                this.log.debug('Table data REQUEST:', this.req, p);
+            }
+            this.wss.sendEvent(this.req, p);
+            this.loadingIconShown = true;
+        }
+    }
+
+    /**
+     * Row Selected
+     */
+    rowSelectionCb(event: any, selRow: any): void {
+        const selId: string = selRow[this.idKey];
+        this.selId = (this.selId === selId) ? undefined : selId;
+        this.log.debug('Row', selId, 'selected');
+        if (this.parentSelCb) {
+            this.parentSelCb(event, selRow);
+        }
+    }
+
+    /**
+     * autoRefresh functions
+     */
+    startRefresh() {
+        this.refreshPromise =
+            setInterval(() => {
+                if (!this.loadingIconShown) {
+                    if (this.fs.debugOn('table')) {
+                        this.log.debug('Refreshing ' + this.root + ' page');
+                    }
+                    this.requestTableData();
+                }
+            }, REFRESH_INTERVAL);
+    }
+
+    stopRefresh() {
+        if (this.refreshPromise) {
+            clearInterval(this.refreshPromise);
+            this.refreshPromise = null;
+        }
+    }
+
+    isChanged(id: string): boolean {
+        return (this.fs.inArray(id, this.changedData) === -1) ? false : true;
+    }
+
+    /**
+     * A dummy implementation of the lionFn until the response is received and the LION
+     * bundle is received from the WebSocket
+     */
+    dummyLion(key: string): string {
+        return '%' + key + '%';
+    }
+
+    /**
+     * Change the sort order of the data returned
+     *
+     * sortParams are passed to the server by WebSocket and the data is
+     * returned sorted
+     *
+     * This is usually assigned to the (click) event on a column, and the column
+     * name passed in e.g. (click)="onSort('origin')
+     * If the column that is passed in is already the firstCol, then reverse its direction
+     * If a new column is passed in, then make the existing col the 2nd sort order
+     */
+    onSort(colName: string) {
+        if (this.sortParams.firstCol === colName) {
+            if (this.sortParams.firstDir === SortDir.desc) {
+                this.sortParams.firstDir = SortDir.asc;
+                return;
+            } else {
+                this.sortParams.firstDir = SortDir.desc;
+                return;
+            }
+        } else {
+            this.sortParams.secondCol = this.sortParams.firstCol;
+            this.sortParams.secondDir = this.sortParams.firstDir;
+            this.sortParams.firstCol = colName;
+            this.sortParams.firstDir = SortDir.desc;
+        }
+        this.log.debug('Sort params', this.sortParams);
+        this.requestTableData();
+    }
+
+    sortIcon(column: string): string {
+        if (this.sortParams.firstCol === column) {
+            if (this.sortParams.firstDir === SortDir.asc) {
+                return 'upArrow';
+            } else {
+                return 'downArrow';
+            }
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * De-selects the row
+     */
+    deselectRow(event) {
+        this.log.debug('Details panel close event');
+        this.selId = event;
+    }
+}
diff --git a/web/gui2-fw-lib/lib/widget/table.css b/web/gui2-fw-lib/lib/widget/table.css
new file mode 100644
index 0000000..82cfb20
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/table.css
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* ------ for summary-list tables (layout) ------ */
+
+div.summary-list {
+    margin: 0 20px 16px 10px;
+    font-size: 10pt;
+    border-spacing: 0;
+}
+
+div.summary-list table {
+    border-collapse: collapse;
+    table-layout: fixed;
+    empty-cells: show;
+    margin: 0;
+}
+
+div.summary-list div.table-body {
+    overflow-y: scroll;
+}
+
+div.summary-list div.table-body::-webkit-scrollbar {
+    display: none;
+}
+
+div.summary-list div.table-body tr.no-data td {
+    text-align: center;
+    font-style: italic;
+}
+
+
+/* highlighting */
+div.summary-list tr {
+    transition: background-color 500ms;
+}
+
+div.summary-list td {
+    padding: 4px;
+    text-align: left;
+    word-wrap: break-word;
+    font-size: 10pt;
+}
+
+div.summary-list td.table-icon {
+    width: 42px;
+    padding-top: 4px;
+    padding-bottom: 0px;
+    padding-left: 4px;
+    text-align: center;
+}
+
+div.summary-list .table-header td {
+    font-weight: bold;
+    font-variant: small-caps;
+    text-transform: uppercase;
+    font-size: 10pt;
+    padding-top: 8px;
+    padding-bottom: 8px;
+    letter-spacing: 0.02em;
+    cursor: pointer;
+}
+
+/* rows are selectable */
+div.summary-list .table-body td {
+    cursor: pointer;
+}
+
+/* Tabular view controls */
+
+div.tabular-header .search {
+    margin: 0 0 10px 10px;
+}
+
+
+div.tabular-header div.ctrl-btns {
+    display: inline-block;
+    float: right;
+    height: 44px;
+    margin-top: 24px;
+    margin-right: 20px;
+    position: absolute;
+    right: 0px;
+}
+
+div.tabular-header div.ctrl-btns div {
+    display: inline-block;
+    cursor: pointer;
+}
+
+div.tabular-header div.ctrl-btns div.separator  {
+    width: 0;
+    height: 40px;
+    padding: 0;
+    border-right: 1px solid #c7c7c0;
+}
diff --git a/web/gui2-fw-lib/lib/widget/table.theme.css b/web/gui2-fw-lib/lib/widget/table.theme.css
new file mode 100644
index 0000000..cc7e7e6
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/table.theme.css
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* ------ for summary-list tables (theme) ------ */
+
+.light div.summary-list, .table-header th {
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+}
+
+.light div.summary-list, td {
+    color: #3c3a3a;
+}
+
+.light div.summary-list, tr:nth-child(even) {
+    background-color: #f4f4f4;
+}
+.light div.summary-list, tr:nth-child(odd) {
+    background-color: #fbfbfb;
+}
+
+.light div.summary-list, tr.selected {
+    background-color: #dbeffc !important;
+}
+
+
+.light div.summary-list, tr.data-change {
+    background-color: #FDFFDC;
+}
+
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+.light .ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #e0dfd6;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+.light .ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.light .ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+.light .ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
+    fill: #518ecc;
+}
+.light .ctrl-btns div.current-view svg.embeddedIcon g.icon use {
+    fill: white;
+}
+
+/* REFRESH */
+.light .ctrl-btns div.refresh svg.embeddedIcon g.icon use {
+    fill: #cdeff2;
+}
+.light .ctrl-btns div.refresh:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+.light .ctrl-btns div.refresh.active svg.embeddedIcon g.icon use {
+    fill: #009fdb;
+}
+.light .ctrl-btns div.refresh.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark div.summary-list .table-header td {
+    background-color: #222222;
+    color: #cccccc;
+}
+
+.dark div.summary-list td {
+    /* note: don't put background-color here */
+    color: #cccccc;
+}
+.dark div.summary-list tr.no-data td {
+    background-color: #333333;
+}
+
+.dark div.summary-list tr:nth-child(even) {
+    background-color: #333333;
+}
+.dark div.summary-list tr:nth-child(odd) {
+    background-color: #3a3a3a;
+}
+
+.dark div.summary-list tr.selected {
+    background-color: #304860 !important;
+}
+
+
+.dark div.summary-list tr.data-change {
+    background-color: #423708;
+}
+
+/* --- Control Buttons --- */
+
+/* INACTIVE */
+.dark .ctrl-btns div svg.embeddedIcon g.icon use {
+    fill: #444444;
+}
+/* note: no change for inactive buttons when hovered */
+
+
+/* ACTIVE */
+.dark .ctrl-btns div.active svg.embeddedIcon g.icon use {
+    fill: #939598;
+}
+.dark .ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+
+/* CURRENT-VIEW */
+.dark .ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
+    fill: #518ecc;
+}
+.dark .ctrl-btns div.current-view svg.embeddedIcon g.icon use {
+    fill: #dddddd;
+}
+
+/* REFRESH */
+.dark .ctrl-btns div.refresh svg.embeddedIcon g.icon use {
+    fill: #364144;
+}
+.dark .ctrl-btns div.refresh:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
+.dark .ctrl-btns div.refresh.active svg.embeddedIcon g.icon use {
+    fill: #0074a6;
+}
+.dark .ctrl-btns div.refresh.active:hover svg.embeddedIcon g.icon use {
+    fill: #ce5b58;
+}
diff --git a/web/gui2-fw-lib/lib/widget/tablefilter.pipe.spec.ts b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.spec.ts
new file mode 100644
index 0000000..8832feb
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.spec.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TableFilterPipe } from './tablefilter.pipe';
+import { TableFilter } from './table.base';
+
+describe('TableFilterPipe', () => {
+
+    const pipe = new TableFilterPipe();
+    const items: any[] = new Array();
+    // Array item 0
+    items.push({
+        id: 'abc',
+        title: 'def',
+        origin: 'ghi'
+    });
+    // Array item 1
+    items.push({
+        id: 'pqr',
+        title: 'stu',
+        origin: 'vwx'
+    });
+    // Array item 2
+    items.push({
+        id: 'dog',
+        title: 'mouse',
+        origin: 'cat'
+    });
+
+
+    it('create an instance', () => {
+        expect(pipe).toBeTruthy();
+    });
+
+    it('expect it to handle empty search', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: '', queryBy: 'title'});
+        expect(filteredItems).toEqual(items);
+    });
+
+    it('expect it to handle empty items', () => {
+        const filteredItems: any[] =
+            pipe.transform(new Array(), <TableFilter>{queryStr: 'de', queryBy: 'title'});
+        expect(filteredItems).toEqual(new Array());
+    });
+
+
+    it('expect it to match 0 by title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'de', queryBy: 'title'});
+        expect(filteredItems).toEqual(items.slice(0, 1));
+    });
+
+    it('expect it to match 1 by title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'st', queryBy: 'title'});
+        expect(filteredItems).toEqual(items.slice(1, 2));
+    });
+
+    it('expect it to match 1 by uppercase title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'sT', queryBy: 'title'});
+        expect(filteredItems).toEqual(items.slice(1, 2));
+    });
+
+    it('expect it to not match by title', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'pq', queryBy: 'title'});
+        expect(filteredItems.length).toEqual(0);
+    });
+
+    it('expect it to match 1 by all fields', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'pq', queryBy: '$'});
+        expect(filteredItems).toEqual(items.slice(1, 2));
+    });
+
+    it('expect it to not match by all fields', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 'yz', queryBy: '$'});
+        expect(filteredItems.length).toEqual(0);
+    });
+
+    /**
+     * Check that items one and two contain a 't' - title=stu and origin=cat
+     */
+    it('expect it to match 1,2 by all fields', () => {
+        const filteredItems: any[] =
+            pipe.transform(items, <TableFilter>{queryStr: 't', queryBy: '$'});
+        expect(filteredItems).toEqual(items.slice(1));
+    });
+});
diff --git a/web/gui2-fw-lib/lib/widget/tablefilter.pipe.ts b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.ts
new file mode 100644
index 0000000..8d8f21e
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tablefilter.pipe.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Pipe, PipeTransform } from '@angular/core';
+import { TableFilter } from './table.base';
+
+/**
+ * Only return the tabledata that matches filtering with some queries
+ *
+ * Note: the pipe is marked pure here as we need to filter on the
+ * content of the filter object (it's not a primitive type)
+ */
+@Pipe({
+  name: 'filter',
+  pure: false
+})
+export class TableFilterPipe implements PipeTransform {
+
+    /**
+     * From an array of table items just return those that match the filter
+     */
+    transform(items: any[], tableDataFilter: TableFilter): any[] {
+        if (!items) {
+            return [];
+        }
+        if (!tableDataFilter.queryStr) {
+            return items;
+        }
+
+        const queryStr = tableDataFilter.queryStr.toLowerCase();
+
+        return items.filter( it => {
+            if (tableDataFilter.queryBy === '$') {
+                const t1 = (<any>Object).values(it);
+                const t2 = (<any>Object).values(it).filter(value => {
+                               return JSON.stringify(value).toLowerCase().indexOf(queryStr) !== -1;
+                           });
+                return (<any>Object).values(it).filter(value => {
+                    return JSON.stringify(value).toLowerCase().indexOf(queryStr) !== -1;
+                }).length > 0;
+            } else {
+                return it[tableDataFilter.queryBy].toLowerCase().includes(queryStr);
+            }
+        });
+    }
+}
diff --git a/web/gui2-fw-lib/lib/widget/tableresize.directive.spec.ts b/web/gui2-fw-lib/lib/widget/tableresize.directive.spec.ts
new file mode 100644
index 0000000..b1e87e6
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tableresize.directive.spec.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TestBed, inject } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { TableResizeDirective } from './tableresize.directive';
+import { LogService } from '..//log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { MastService } from '../mast/mast.service';
+import { FnService } from '../util/fn.service';
+
+class MockMastService {}
+
+class MockFnService extends FnService {
+    constructor(ar: ActivatedRoute, log: LogService, w: Window) {
+        super(ar, log, w);
+    }
+}
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Widget -- Table Resize Directive - Unit Tests
+ */
+describe('TableResizeDirective', () => {
+    let log: LogService;
+    let mockWindow: Window;
+    let ar: ActivatedRoute;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute(['debug', 'DetectBrowserDirective']);
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            }
+        };
+        TestBed.configureTestingModule({
+            providers: [ TableResizeDirective,
+                { provide: FnService, useValue: new MockFnService(ar, log, mockWindow) },
+                { provide: LogService, useValue: log },
+                { provide: MastService, useClass: MockMastService },
+                { provide: 'Window', useFactory: (() => mockWindow ) },
+            ]
+        });
+    });
+
+    afterEach(() => {
+        log = null;
+    });
+
+    it('should create an instance', inject([TableResizeDirective], (directive: TableResizeDirective) => {
+        expect(directive).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/lib/widget/tableresize.directive.ts b/web/gui2-fw-lib/lib/widget/tableresize.directive.ts
new file mode 100644
index 0000000..5d3a9eb
--- /dev/null
+++ b/web/gui2-fw-lib/lib/widget/tableresize.directive.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AfterContentChecked, Directive, Inject } from '@angular/core';
+import { FnService } from '../util/fn.service';
+import { LogService } from '../log.service';
+import { MastService } from '../mast/mast.service';
+import { HostListener } from '@angular/core';
+import * as d3 from 'd3';
+
+/**
+ * ONOS GUI -- Widget -- Table Resize Directive
+ */
+@Directive({
+    selector: '[onosTableResize]',
+})
+export class TableResizeDirective implements AfterContentChecked {
+
+    pdg = 22;
+    tables: any;
+
+    constructor(protected fs: FnService,
+        protected log: LogService,
+        protected mast: MastService,
+        @Inject('Window') private w: any) {
+
+        log.info('TableResizeDirective constructed');
+    }
+
+    ngAfterContentChecked() {
+        this.tables = {
+            thead: d3.select('div.table-header').select('table'),
+            tbody: d3.select('div.table-body').select('table')
+        };
+        this.windowSize(this.tables);
+    }
+
+    windowSize(tables: any) {
+        const wsz = this.fs.windowSize(0, 30);
+        this.adjustTable(tables, wsz.width, wsz.height);
+    }
+
+    @HostListener('window:resize', ['$event.target'])
+    onResize(event: any) {
+        this.windowSize(this.tables);
+        return {
+            h: this.w.innerHeight,
+            w: this.w.innerWidth
+        };
+    }
+
+    adjustTable(tables: any, width: number, height: number) {
+        this._width(tables.thead, width + 'px');
+        this._width(tables.tbody, width + 'px');
+
+        this.setHeight(tables.thead, d3.select('div.table-body'), height);
+    }
+
+    _width(elem, width) {
+        elem.style('width', width);
+    }
+
+    setHeight(thead: any, body: any, height: number) {
+        const h = height - (this.mast.mastHeight +
+            this.fs.noPxStyle(d3.select('.tabular-header'), 'height') +
+            this.fs.noPxStyle(thead, 'height') + this.pdg);
+        body.style('height', h + 'px');
+    }
+
+}