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');
+ }
+
+}