Implemented table building functions

Change-Id: Ie4003080b13725561df22de41ec85f8c3f31c794
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps.component.html b/web/gui2/src/main/webapp/app/view/apps/apps.component.html
index 90f9a16..6397f63 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps.component.html
+++ b/web/gui2/src/main/webapp/app/view/apps/apps.component.html
@@ -1,3 +1,100 @@
-<div id="ov-app">
-    <p>apps works!</p>
-</div>
\ No newline at end of file
+<div id="ov-app" filedrop on-file-drop="appDropped()">
+    <div class="tabular-header">
+        <h2>
+            {{lionFn('title_apps')}}
+            ({{ tableData.length }}
+            {{ lionFn('total') }})
+        </h2>
+        <div class="ctrl-btns">
+            <div class="refresh" (click)="toggleRefresh()">
+                <onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}"
+                           iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
+            </div>
+            <div class="separator"></div>
+
+            <!--<form id="inputFileForm">-->
+                <!--<input id="uploadFile"-->
+                       <!--type="file" size="50" accept=".oar,.jar"-->
+                       <!--file-model="appFile">-->
+            <!--</form>-->
+
+            <div class="active" trigger-form>
+                <onos-icon classes="{{ 'active upload' }}"
+                        iconId="upload" iconSize="42" toolTip="{{ uploadTip }}"></onos-icon>
+            </div>
+            <div (click)="appAction('activate')">
+                <onos-icon classes="{{ ctrlBtnState.installed?'active play':'play' }}"
+                           iconId="play" iconSize="42" toolTip="{{ activateTip }}"></onos-icon>
+            </div>
+            <div (click)="appAction('deactivate')">
+                <onos-icon classes="{{ ctrlBtnState.active?'active stop':'stop' }}"
+                        iconId="stop" iconSize="42" toolTip="{{ deactivateTip }}"></onos-icon>
+            </div>
+            <div (click)="appAction('uninstall')">
+                 <!--[ngClass]="{active: ctrlBtnState.selection}">-->
+                <!--tooltip tt-msg="uninstallTip"-->
+                <onos-icon classes="{{ ctrlBtnState.selection?'active garbage':'garbage' }}"
+                        iconId="garbage" iconSize="42" toolTip="{{ uninstallTip }}"></onos-icon>
+            </div>
+            <div (click)="downloadApp()">
+                <onos-icon classes="{{ ctrlBtnState.selection?'active download':'download' }}"
+                        iconId="download" iconSize="42" toolTip="{{ downloadTip }}"></onos-icon>
+            </div>
+        </div>
+
+        <!--<div class="search">-->
+            <!--<input type="text" ng-model="queryTxt" placeholder="Search"/>-->
+            <!--<select ng-model="queryBy">-->
+                <!--<option value="" disabled>Search By</option>-->
+                <!--<option value="$">All Fields</option>-->
+                <!--<option value="title">{{lionFn('title')}}</option>-->
+                <!--<option value="id">{{lionFn('app_id')}}</option>-->
+                <!--<option value="version">{{lionFn('version')}}</option>-->
+                <!--<option value="category">{{lionFn('category')}}</option>-->
+                <!--<option value="apporiginName">{{lionFn('origin')}}</option>-->
+
+            <!--</select>-->
+        <!--</div>-->
+
+
+    </div>
+
+    <div class="summary-list" onos-table-resize>
+        <table onos-flash-changes id-prop="id" width="100%">
+            <tr class="table-header">
+                <th colId="state" class="table-icon" sortable></th>
+                <th colId="icon" class="table-icon"></th>
+                <th colId="title" [ngClass]="{width: '340'}" sortable> {{lionFn('title')}} </th>
+                <th colId="id" [ngClass]="{width: '320px'}"sortable> {{lionFn('app_id')}} </th>
+                <th colId="version" [ngClass]="{width: '140px'}"sortable> {{lionFn('version')}} </th>
+                <th colId="category" [ngClass]="{width: '136px'}"sortable> {{lionFn('category')}} </th>
+                <th colId="origin" sortable> {{lionFn('origin')}} </th>
+            </tr>
+
+            <tr *ngIf="tableData.length === 0" class="no-data">
+                <td colspan="5">
+                    {{annots.no_rows_msg}}
+                </td>
+            </tr>
+            <!--&lt;!&ndash;TODO: Add back in  | filter:queryFilter&ndash;&gt;-->
+            <tr class="table-body" *ngFor="let app of tableData; trackBy $index"
+                (click)="selectCallback($event, app)"
+                [ngClass]="{selected: app.id === selId, 'data-change': isChanged(app.id)}">
+                <td class="table-icon">
+                    <onos-icon iconId="{{app._iconid_state}}"></onos-icon>
+                </td>
+                <td class="table-icon">
+                    <!--<img data-ng-src="./rs/applications/{{app.icon}}/icon"-->
+                                            <!--height="24px" width="24px" />-->
+                </td>
+                <td>{{ app.title }}</td>
+                <td>{{ app.id }}</td>
+                <td>{{ app.version }}</td>
+                <td>{{ app.category }}</td>
+                <td>{{ app.origin }}</td>
+            </tr>
+        </table>
+
+    </div>
+
+</div>
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps.component.ts b/web/gui2/src/main/webapp/app/view/apps/apps.component.ts
index 5eaa38f..b2bb38b 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps.component.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps.component.ts
@@ -13,46 +13,235 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
 import { DialogService } from '../../fw/layer/dialog.service';
 import { FnService } from '../../fw/util/fn.service';
 import { IconService } from '../../fw/svg/icon.service';
 import { KeyService } from '../../fw/util/key.service';
 import { LionService } from '../../fw/util/lion.service';
+import { LoadingService } from '../../fw/layer/loading.service';
 import { LogService } from '../../log.service';
 import { PanelService } from '../../fw/layer/panel.service';
-import { TableBuilderService } from '../../fw/widget/tablebuilder.service';
+import { TableBaseImpl, TableResponse } from '../../fw/widget/tablebase';
 import { UrlFnService } from '../../fw/remote/urlfn.service';
 import { WebSocketService } from '../../fw/remote/websocket.service';
 
+const INSTALLED = 'INSTALLED';
+const ACTIVE = 'ACTIVE';
+const appMgmtReq = 'appManagementRequest';
+const topPdg = 60;
+const panelWidth = 540;
+const pName = 'application-details-panel';
+const detailsReq = 'appDetailsRequest';
+const detailsResp = 'appDetailsResponse';
+const fileUploadUrl = 'applications/upload';
+const activateOption = '?activate=true';
+const appUrlPrefix = 'rs/applications/';
+const iconUrlSuffix = '/icon';
+const downloadSuffix = '/download';
+const dialogId = 'app-dialog';
+const dialogOpts = {
+    edge: 'right',
+    width: 400,
+};
+const strongWarning = {
+    'org.onosproject.drivers': true,
+};
+const propOrder = ['id', 'state', 'category', 'version', 'origin', 'role'];
+
+interface AppTableResponse extends TableResponse {
+    apps: Apps[];
+}
+
+interface Apps {
+    category: string;
+    desc: string;
+    features: string;
+    icon: string;
+    id: string;
+    origin: string;
+    permissions: string;
+    readme: string;
+    required_apps: string;
+    role: string;
+    state: string;
+    title: string;
+    url: string;
+    version: string;
+    _iconid_state: string;
+}
+
+interface CtrlBtnState {
+    installed: boolean;
+    selection: string;
+    active: boolean;
+}
+
 /**
  * ONOS GUI -- Apps View Component
  */
 @Component({
   selector: 'onos-apps',
   templateUrl: './apps.component.html',
-  styleUrls: ['./apps.component.css']
+  styleUrls: [
+    './apps.component.css', './apps.theme.css',
+    '../../fw/widget/table.css', '../../fw/widget/table-theme.css'
+    ]
 })
-export class AppsComponent implements OnInit {
+export class AppsComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    // deferred localization strings
+    lionFn; // Function
+    warnDeactivate: string;
+    warnOwnRisk: string;
+    friendlyProps: string[];
+    ctrlBtnState: CtrlBtnState;
+    detailsPanel: any;
 
     constructor(
-        private fs: FnService,
+        protected fs: FnService,
         private ds: DialogService,
         private is: IconService,
         private ks: KeyService,
-        private ls: LionService,
-        private log: LogService,
+        private lion: LionService,
+        protected ls: LoadingService,
+        protected log: LogService,
         private ps: PanelService,
-        private tbs: TableBuilderService,
         private ufs: UrlFnService,
-        private wss: WebSocketService,
-        private window: Window
+        protected wss: WebSocketService,
+        private window: Window,
     ) {
-        this.log.debug('AppsComponent constructed');
+        super(fs, null, log, wss, 'app');
+        this.responseCallback = this.appResponseCb;
+        this.sortParams = {
+            firstCol: 'state',
+            firstDir: 'desc',
+            secondCol: 'title',
+            secondDir: 'asc',
+        };
+        // We want doLion() to be called only after the Lion service is populated (from the WebSocket)
+        this.lion.loadCb = (() => this.doLion());
+        this.ctrlBtnState = <CtrlBtnState>{
+            installed: false,
+            active: false
+        };
+        if (this.lion.ubercache.length === 0) {
+            this.lionFn = this.dummyLion;
+        } else {
+            this.doLion();
+        }
     }
 
     ngOnInit() {
-        this.log.debug('AppsComponent initialized');
+        this.init();
+        this.log.debug('AppComponent initialized');
     }
 
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('AppComponent destroyed');
+    }
+
+    /**
+     * The callback called when App data returns from WSS
+     */
+    appResponseCb(data: AppTableResponse) {
+        this.log.debug('App response received for ', data.apps.length, 'apps');
+    }
+
+    refreshCtrls() {
+        let row;
+        let rowIdx;
+        if (this.ctrlBtnState.selection) {
+            rowIdx = this.fs.find(this.selId, this.tableData);
+            row = rowIdx >= 0 ? this.tableData[rowIdx] : null;
+
+            this.ctrlBtnState.installed = row && row.state === INSTALLED;
+            this.ctrlBtnState.active = row && row.state === ACTIVE;
+        } else {
+            this.ctrlBtnState.installed = false;
+            this.ctrlBtnState.active = false;
+        }
+    }
+
+    createConfirmationText(action, itemId) {
+//        let content = this.ds.createDiv();
+//        content.append('p').text(this.lionFn(action) + ' ' + itemId);
+//        if (strongWarning[itemId]) {
+//            content.append('p').html(
+//                this.fs.sanitize(this.warnDeactivate) +
+//                '<br>' +
+//                this.fs.sanitize(this.warnOwnRisk)
+//            ).classed('strong', true);
+//        }
+//        return content;
+    }
+
+    confirmAction(action): void {
+        const itemId = this.selId;
+        const spar = this.sortParams;
+
+        function dOk() {
+            this.log.debug('Initiating', action, 'of', itemId);
+            this.wss.sendEvent(appMgmtReq, {
+                action: action,
+                name: itemId,
+                sortCol: spar.sortCol,
+                sortDir: spar.sortDir,
+            });
+            if (action === 'uninstall') {
+                this.detailsPanel.hide();
+            } else {
+                this.wss.sendEvent(detailsReq, { id: itemId });
+            }
+        }
+
+        function dCancel() {
+            this.log.debug('Canceling', action, 'of', itemId);
+        }
+
+//        this.ds.openDialog(dialogId, dialogOpts)
+//            .setTitle(this.lionFn('dlg_confirm_action'))
+//            .addContent(this.createConfirmationText(action, itemId))
+//            .addOk(dOk)
+//            .addCancel(dCancel)
+//            .bindKeys();
+    }
+
+    appAction(action) {
+        if (this.ctrlBtnState.selection) {
+            this.confirmAction(action);
+        }
+    }
+
+    downloadApp() {
+        if (this.ctrlBtnState.selection) {
+            (<any>this.window).location = appUrlPrefix + this.selId + downloadSuffix;
+        }
+    }
+
+    /**
+     * Read the LION bundle for App - this should replace the dummyLion implementation
+     * of lionFn with a function from the LION Service
+     */
+    doLion() {
+        this.lionFn = this.lion.bundle('core.view.App');
+
+        this.warnDeactivate = this.lionFn('dlg_warn_deactivate');
+        this.warnOwnRisk = this.lionFn('dlg_warn_own_risk');
+
+        this.friendlyProps = [
+            this.lionFn('app_id'), this.lionFn('state'),
+            this.lionFn('category'), this.lionFn('version'),
+            this.lionFn('origin'), this.lionFn('role'),
+        ];
+    }
+
+    /**
+     * 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/src/main/webapp/app/view/apps/apps.module.ts b/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
index 3083b55..3e3d5c4 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
@@ -17,6 +17,7 @@
 import { CommonModule } from '@angular/common';
 import { AppsComponent } from './apps.component';
 import { TriggerFormDirective } from './triggerform.directive';
+import { SvgModule } from '../../fw/svg/svg.module';
 
 /**
  * ONOS GUI -- Apps View Module
@@ -30,7 +31,8 @@
         AppsComponent
     ],
     imports: [
-        CommonModule
+        CommonModule,
+        SvgModule
     ],
     declarations: [
         AppsComponent,
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.css b/web/gui2/src/main/webapp/app/view/device/device.component.css
index c578112..4d8454d 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.css
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.css
@@ -17,12 +17,15 @@
 /*
  ONOS GUI -- Device View (layout) -- CSS file
  */
+#ov-device .tabular-header {
+    text-align: left;
+}
 
 #ov-device h2 {
     display: inline-block;
 }
 
-#ov-device div.ctrl-btns {
+#ov-device, div.ctrl-btns {
 }
 
 
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.html b/web/gui2/src/main/webapp/app/view/device/device.component.html
index febc99a..e2d3fda 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.html
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.html
@@ -1,3 +1,99 @@
 <div id="ov-device">
-    <p>device works!</p>
+    <div class="tabular-header">
+        <h2>Devices ({{ tableData.length }} total)</h2>
+        <div class="ctrl-btns">
+            <div class="refresh" (click)="toggleRefresh()">
+                <!-- See icon.theme.css for the defintions of the classes active and refresh-->
+                <onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}"
+                           iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
+            </div>
+            <div class="separator"></div>
+
+            <div>
+                <onos-icon classes="{{ selId ? 'current-view':undefined }}"
+                           iconId="deviceTable" iconSize="42"></onos-icon>
+            </div>
+
+            <div routerLink="/flow" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                           iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/port" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/group" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/meter" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
+            </div>
+
+            <div routerLink="/pipeconf" routerLinkActive="active">
+                <onos-icon classes="{{ selId ? 'active':undefined }}"
+                        iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
+            </div>
+        </div>
+    </div>
+
+    <div class="summary-list" onos-table-resize>
+        <table onos-flash-changes id-prop="id" width="100%">
+            <tr class="table-header">
+                <th colId="available" class="table-icon" sortable></th>
+                <th colId="type" class="table-icon"></th>
+                <th colId="name" sortable>Friendly Name </th>
+                <th colId="id" sortable>Device ID </th>
+                <th colId="masterid" [ngClass]="{width: '130px'}" sortable>Master </th>
+                <th colId="num_ports" [ngClass]="{width: '70px'}" sortable>Ports </th>
+                <th colId="mfr" sortable>Vendor </th>
+                <th colId="hw" sortable>H/W Version </th>
+                <th colId="sw" sortable>S/W Version </th>
+                <th colId="protocol" [ngClass]="{width: '100px'}" sortable>Protocol </th>
+            </tr>
+
+            <tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
+                <td colspan="9">{{ annots.noRowsMsg }}</td>
+            </tr>
+
+
+            <tr class="table-body" *ngFor="let dev of tableData; trackBy $index"
+                (click)="selectCallback($event, dev)"
+                [ngClass]="{selected: dev.id === selId, 'data-change': isChanged(dev.id)}">
+                <td class="table-icon">
+                    <!--[ngClass]="{width: devAvail.getBBox().width}"-->
+                    <onos-icon iconId="{{dev._iconid_available}}"></onos-icon>
+                </td>
+                <td class="table-icon">
+                    <onos-icon iconId="{{dev._iconid_type}}"></onos-icon>
+                </td>
+                <td>{{ dev.name }}</td>
+                <td>{{ dev.id }}</td>
+                <td>{{ dev.masterid }}</td>
+                <td>{{ dev.num_ports }}</td>
+                <td>{{ dev.mfr }}</td>
+                <td>{{ dev.hw }}</td>
+                <td>{{ dev.sw }}</td>
+                <td>{{ dev.protocol }}</td>
+            </tr>
+        </table>
+    </div>
+    <small>
+    <p>TODO (21 Jun 18): Add in:</p>
+    <ul>
+        <li>Scrolling for long lists of devices</li>
+        <li>Sorting by column</li>
+        <li>Left align header columns</li>
+        <li>Move tooltip to underneath icon</li>
+        <li>Correct width and icon colour of active and device icon columns</li>
+        <li>Add device details panel</li>
+        <li>Add more unit tests</li>
+        <li>Make icon for #undefined work (e.g. for device type olt or unknown)</li>
+        <li>Change loading service to fade in and out and have a threshold of </li>
+    </ul>
+    </small>
 </div>
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.ts b/web/gui2/src/main/webapp/app/view/device/device.component.ts
index 6929f9b..f4a6fbe 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
 import { DetailsPanelService } from '../../fw/layer/detailspanel.service';
 import { FnService } from '../../fw/util/fn.service';
 import { IconService } from '../../fw/svg/icon.service';
@@ -23,42 +23,78 @@
 import { MastService } from '../../fw/mast/mast.service';
 import { NavService } from '../../fw/nav/nav.service';
 import { PanelService } from '../../fw/layer/panel.service';
-import { TableBuilderService } from '../../fw/widget/tablebuilder.service';
+import { TableBaseImpl, TableResponse } from '../../fw/widget/tablebase';
 import { TableDetailService } from '../../fw/widget/tabledetail.service';
 import { WebSocketService } from '../../fw/remote/websocket.service';
 
+interface DeviceTableResponse extends TableResponse {
+    devices: Device[];
+}
+
+interface Device {
+    available: boolean;
+    chassisid: string;
+    hw: string;
+    id: string;
+    masterid: string;
+    mfr: string;
+    name: string;
+    num_ports: number;
+    protocol: string;
+    serial: string;
+    sw: string;
+    _iconid_available: string;
+    _iconid_type: string;
+}
+
+
 /**
  * ONOS GUI -- Device View Component
  */
 @Component({
   selector: 'onos-device',
   templateUrl: './device.component.html',
-  styleUrls: ['./device.component.css']
+  styleUrls: ['./device.component.css', './device.theme.css', '../../fw/widget/table.css', '../../fw/widget/table-theme.css']
 })
-export class DeviceComponent implements OnInit {
+export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+    // TODO: Update for LION
+    flowTip = 'Show flow view for selected device';
+    portTip = 'Show port view for selected device';
+    groupTip = 'Show group view for selected device';
+    meterTip = 'Show meter view for selected device';
+    pipeconfTip = 'Show pipeconf view for selected device';
 
     constructor(
         private dps: DetailsPanelService,
-        private fs: FnService,
+        protected fs: FnService,
+        protected ls: LoadingService,
         private is: IconService,
         private ks: KeyService,
-        private log: LogService,
+        protected log: LogService,
         private mast: MastService,
         private nav: NavService,
         private ps: PanelService,
-        private tbs: TableBuilderService,
         private tds: TableDetailService,
-        private wss: WebSocketService,
-        private ls: LoadingService, // TODO: Remove - already added through tbs
-        private window: Window
+        protected wss: WebSocketService,
+        private window: Window,
     ) {
-        this.log.debug('DeviceComponent constructed');
+        super(fs, ls, log, wss, 'device');
+        this.responseCallback = this.deviceResponseCb;
     }
 
     ngOnInit() {
+        this.init();
         this.log.debug('DeviceComponent initialized');
-        // TODO: Remove this - it's only for demo purposes
-//        this.ls.startAnim();
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+        this.log.debug('DeviceComponent destroyed');
+    }
+
+    deviceResponseCb(data: DeviceTableResponse) {
+        this.log.debug('Device response received for ', data.devices.length, 'devices');
     }
 
 }
diff --git a/web/gui2/src/main/webapp/app/view/device/device.module.ts b/web/gui2/src/main/webapp/app/view/device/device.module.ts
index 8f0f351..99b15bd 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.module.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.module.ts
@@ -15,9 +15,11 @@
  */
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
+import { RouterModule, Routes } from '@angular/router';
 import { DeviceComponent } from './device.component';
 import { DeviceDetailsPanelDirective } from './devicedetailspanel.directive';
-import { RemoteModule } from '../../fw/remote/remote.module';
+import { SvgModule } from '../../fw/svg/svg.module';
+
 /**
  * ONOS GUI -- Device View Module
  */
@@ -27,7 +29,8 @@
   ],
   imports: [
     CommonModule,
-    RemoteModule
+    RouterModule,
+    SvgModule
   ],
   declarations: [
     DeviceComponent,