Added actions to the Apps view of web/gui2

Change-Id: I3d96a324590bee4de0875d4f533cc723c7f6ba52
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 0f38dbc..738ee18 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
@@ -19,7 +19,7 @@
 import { AppsRoutingModule } from './apps-routing.module';
 import { AppsComponent } from './apps/apps.component';
 import { AppsDetailsComponent } from './appsdetails/appsdetails.component';
-import { TriggerFormDirective } from './triggerform.directive';
+import { LayerModule } from '../../fw/layer/layer.module';
 import { SvgModule } from '../../fw/svg/svg.module';
 import { WidgetModule } from '../../fw/widget/widget.module';
 
@@ -36,12 +36,12 @@
         AppsRoutingModule,
         SvgModule,
         WidgetModule,
-        FormsModule
+        FormsModule,
+        LayerModule
     ],
     declarations: [
         AppsComponent,
-        AppsDetailsComponent,
-        TriggerFormDirective
+        AppsDetailsComponent
     ]
 })
 export class AppsModule { }
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
index 5813f30..3096bae 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
@@ -17,13 +17,15 @@
 /*
  ONOS GUI -- Applications View (layout) -- CSS file
  */
+#ov-app .tabular-header {
+    text-align: left;
+}
 
 #ov-app h2 {
     display: inline-block;
 }
 
 #ov-app div.ctrl-btns {
-    width: 290px;
 }
 
 /* -- Drag-n-Drop oar file upload -- */
@@ -35,19 +37,3 @@
 .dropping {
 
 }
-
-/* -- Confirmation Dialog -- */
-#app-dialog {
-    top: 140px;
-    padding: 12px;
-}
-
-#app-dialog p {
-    font-size: 12pt;
-}
-
-#app-dialog p.strong {
-    font-weight: bold;
-    padding: 8px;
-    text-align: center;
-}
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
index 1c4a22f..943e26b 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
@@ -1,5 +1,5 @@
 <!--
-~ Copyright 2014-present Open Networking Foundation
+~ 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.
@@ -13,41 +13,42 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
-<div id="ov-app" filedrop on-file-drop="appDropped()">
+<div id="ov-app" (dragover)="onDragOver($event)" (dragleave)="onDragLeave($event)" (drop)="onDrop($event)">
     <div class="tabular-header">
+        <onos-flash id="appMsgFlash" message="{{ alertMsg }}" dwell="5000" warning="true" (closed)="alertMsg = ''"></onos-flash>
+        <onos-confirm message="{{ confirmMsg }}" warning="{{ strongWarning }}" (chosen)="dOk($event)"></onos-confirm>
         <h2>
             {{lionFn('title_apps')}}
             ({{ tableData.length }}
             {{ lionFn('total') }})
         </h2>
         <div class="ctrl-btns">
+            <form #inputFileForm="ngForm">
+                <input id="uploadFile" hidden
+                       type="file" size="50" accept=".oar,.jar"
+                       name="appFile" (change)="fileEvent($event)">
+            </form>
+
             <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>
+            <div class="active" (click)="triggerForm()">
                 <onos-icon classes="{{ 'active upload' }}"
-                        iconId="upload" iconSize="42" toolTip="{{ uploadTip }}"></onos-icon>
+                           iconId="upload" iconSize="42" toolTip="{{ uploadTip }}"></onos-icon>
             </div>
-            <div (click)="appAction('activate')">
+
+            <div (click)="confirmAction(AppActionEnum.ACTIVATE)">
                 <onos-icon classes="{{ ctrlBtnState.installed?'active play':'play' }}"
                            iconId="play" iconSize="42" toolTip="{{ activateTip }}"></onos-icon>
             </div>
-            <div (click)="appAction('deactivate')">
+            <div (click)="confirmAction(AppActionEnum.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"-->
+            <div (click)="confirmAction(AppActionEnum.UNINSTALL)">
                 <onos-icon classes="{{ ctrlBtnState.selection?'active garbage':'garbage' }}"
                         iconId="garbage" iconSize="42" toolTip="{{ uninstallTip }}"></onos-icon>
             </div>
@@ -73,57 +74,61 @@
 
     </div>
 
-    <div class="summary-list" onos-table-resize>
-        <table onos-flash-changes id-prop="id" width="100%">
-            <tr class="table-header">
-                <th colId="state" [ngStyle]="{width: '32px'}" class="table-icon" (click)="onSort('state')">
-                    <onos-icon classes="active" [iconId]="sortIcon('state')"></onos-icon>
-                </th>
-                <th colId="icon" [ngStyle]="{width: '32px'}" class="table-icon"></th>
-                <th colId="title"  (click)="onSort('title')">{{lionFn('title')}}
-                    <onos-icon classes="active" [iconId]="sortIcon('title')"></onos-icon>
-                </th>
-                <th colId="id" (click)="onSort('id')">{{lionFn('app_id')}}
-                    <onos-icon classes="active" [iconId]="sortIcon('id')"></onos-icon>
-                </th>
-                <th colId="version" (click)="onSort('version')"> {{lionFn('version')}}
-                    <onos-icon classes="active" [iconId]="sortIcon('version')"></onos-icon>
-                </th>
-                <th colId="category" (click)="onSort('category')"> {{lionFn('category')}}
-                    <onos-icon classes="active" [iconId]="sortIcon('category')"></onos-icon>
-                </th>
-                <th colId="origin" (click)="onSort('origin')"> {{lionFn('origin')}}
-                    <onos-icon classes="active" [iconId]="sortIcon('origin')"></onos-icon>
-                </th>
-            </tr>
-
-            <tr *ngIf="tableData.length === 0" class="no-data">
-                <td colspan="5">
-                    {{annots.no_rows_msg}}
-                </td>
-            </tr>
-            <!-- See https://angular.io/guide/pipes#appendix-no-filterpipe-or-orderbypipe
-                Angular has dropped the filter and order by pipe that were present in
-                AngularJS - filter and sort the data at source instead -->
-            <tr class="table-body" *ngFor="let app of tableData | filter: tableDataFilter"
-                (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">
-                    <!-- The path below gets the app icon from the old GUI path -->
-                    <img src="../../ui/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 id="summary-list" class="summary-list">
+        <div class="table-header">
+            <table onosTableResize>
+                <tr>
+                    <th colId="state" [ngStyle]="{width: '32px'}" class="table-icon" (click)="onSort('state')">
+                        <onos-icon classes="active" [iconId]="sortIcon('state')"></onos-icon>
+                    </th>
+                    <th colId="icon" [ngStyle]="{width: '32px'}" class="table-icon"></th>
+                    <th colId="title"  (click)="onSort('title')">{{lionFn('title')}}
+                        <onos-icon classes="active" [iconId]="sortIcon('title')"></onos-icon>
+                    </th>
+                    <th colId="id" (click)="onSort('id')">{{lionFn('app_id')}}
+                        <onos-icon classes="active" [iconId]="sortIcon('id')"></onos-icon>
+                    </th>
+                    <th colId="version" (click)="onSort('version')"> {{lionFn('version')}}
+                        <onos-icon classes="active" [iconId]="sortIcon('version')"></onos-icon>
+                    </th>
+                    <th colId="category" (click)="onSort('category')"> {{lionFn('category')}}
+                        <onos-icon classes="active" [iconId]="sortIcon('category')"></onos-icon>
+                    </th>
+                    <th colId="origin" (click)="onSort('origin')"> {{lionFn('origin')}}
+                        <onos-icon classes="active" [iconId]="sortIcon('origin')"></onos-icon>
+                    </th>
+                </tr>
+            </table>
+        </div>
+        <div class="table-body">
+            <table onosTableResize>
+                <tr *ngIf="tableData.length === 0" class="no-data">
+                    <td colspan="5">
+                        {{annots.no_rows_msg}}
+                    </td>
+                </tr>
+                <!-- See https://angular.io/guide/pipes#appendix-no-filterpipe-or-orderbypipe
+                    Angular has dropped the filter and order by pipe that were present in
+                    AngularJS - filter and sort the data at source instead -->
+                <tr *ngFor="let app of tableData | filter: tableDataFilter"
+                    (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">
+                        <!-- The path below gets the app icon from the old GUI path -->
+                        <img src="../../ui/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>
     <!-- There are 2 ways this component can be included
      1) Insert it in to the ngFor above and have it created as the row is rendered
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
index 1889a44..cddf9ae 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
@@ -19,11 +19,14 @@
 import { FormsModule } from '@angular/forms';
 import { DebugElement } from '@angular/core';
 import { By } from '@angular/platform-browser';
+import { HttpClient, HttpErrorResponse } from '@angular/common/http';
 
 import { LogService } from '../../../log.service';
 import { AppsComponent } from './apps.component';
 import { AppsDetailsComponent } from '../appsdetails/appsdetails.component';
+import { ConfirmComponent } from '../../../fw/layer/confirm/confirm.component';
 import { DialogService } from '../../../fw/layer/dialog.service';
+import { FlashComponent } from '../../../fw/layer/flash/flash.component';
 import { FnService } from '../../../fw/util/fn.service';
 import { IconComponent } from '../../../fw/svg/icon/icon.component';
 import { IconService } from '../../../fw/svg/icon.service';
@@ -47,6 +50,8 @@
 
 class MockFnService {}
 
+class MockHttpClient {}
+
 class MockIconService {
     loadIconDef() {}
 }
@@ -108,10 +113,18 @@
 
         TestBed.configureTestingModule({
             imports: [ BrowserAnimationsModule, FormsModule ],
-            declarations: [ AppsComponent, IconComponent, AppsDetailsComponent, TableFilterPipe ],
+            declarations: [
+                AppsComponent,
+                ConfirmComponent,
+                IconComponent,
+                AppsDetailsComponent,
+                TableFilterPipe,
+                FlashComponent
+            ],
             providers: [
                 { provide: DialogService, useClass: MockDialogService },
                 { provide: FnService, useValue: fs },
+                { provide: HttpClient, useClass: MockHttpClient },
                 { provide: IconService, useClass: MockIconService },
                 { provide: KeyService, useClass: MockKeyService },
                 { provide: LionService, useFactory: (() => {
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
index 79fa310..ffd7b37 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * 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.
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
+import { HttpClient, HttpErrorResponse } from '@angular/common/http';
+
 import { DialogService } from '../../../fw/layer/dialog.service';
 import { FnService } from '../../../fw/util/fn.service';
 import { IconService } from '../../../fw/svg/icon.service';
@@ -28,14 +30,13 @@
 
 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 APPMGMTREQ = 'appManagementRequest';
+const DETAILSREQ = 'appDetailsRequest';
+const FILEUPLOADURL = 'upload';
+const FILEDOWNLOADURL = 'download';
+const ACTIVATEOPTION = '?activate=true';
+const DRAGDROPMSG1 = 'Drag and drop one file at a time';
+const DRAGDROPMSGEXT = 'Only files ending in .oar can be dropped';
 
 /** Prefix to access the REST service for applications */
 export const APPURLPREFIX = '../../ui/rs/applications/'; // TODO: This is a hack to work off GUIv1 URL
@@ -81,6 +82,13 @@
     _iconid_state: string;
 }
 
+export enum AppAction {
+    NONE = 0,
+    ACTIVATE = 1,
+    DEACTIVATE = 2,
+    UNINSTALL = 3,
+}
+
 /**
  * Model of the Control Button
  */
@@ -91,7 +99,7 @@
 }
 
 /**
- * ONOS GUI -- Apps View Component
+ * ONOS GUI -- Apps View Component extends TableBaseImpl
  */
 @Component({
   selector: 'onos-apps',
@@ -108,15 +116,18 @@
     warnDeactivate: string;
     warnOwnRisk: string;
     ctrlBtnState: CtrlBtnState;
-    detailsPanel: any;
     appFile: any;
-    activateImmediately = '';
 
     uploadTip: string;
     activateTip: string;
     deactivateTip: string;
     uninstallTip: string;
     downloadTip: string;
+    alertMsg: string;
+    AppActionEnum: any = AppAction;
+    appAction: AppAction = AppAction.NONE;
+    confirmMsg: string = '';
+    strongWarning: string = '';
 
     constructor(
         protected fs: FnService,
@@ -129,9 +140,11 @@
         private ufs: UrlFnService,
         protected wss: WebSocketService,
         @Inject('Window') private window: Window,
+        private httpClient: HttpClient
     ) {
         super(fs, null, log, wss, 'app');
         this.responseCallback = this.appResponseCb;
+        this.parentSelCb =  this.rowSelection;
         // pre-populate sort so active apps are at the top of the list
         this.sortParams = {
             firstCol: 'state',
@@ -180,78 +193,100 @@
         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;
+    /**
+     * called when a row is selected - sets the state of control icons
+     */
+    rowSelection(event: any, selRow: any) {
+        this.ctrlBtnState.installed = this.selId && selRow && selRow.state === INSTALLED;
+        this.ctrlBtnState.active = this.selId && selRow && selRow.state === ACTIVE;
+        this.ctrlBtnState.selection = this.selId;
+        this.log.debug('Row ', this.selId, 'selected', this.ctrlBtnState);
+    }
 
-            this.ctrlBtnState.installed = row && row.state === INSTALLED;
-            this.ctrlBtnState.active = row && row.state === ACTIVE;
-        } else {
-            this.ctrlBtnState.installed = false;
-            this.ctrlBtnState.active = false;
+
+    /**
+     * Perform one of the app actions - activate, deactivate or uninstall
+     * Raises a dialog which calls back the dOk() below
+     */
+    confirmAction(action: AppAction): void {
+        this.appAction = action;
+        const appActionLc = (<string>AppAction[this.appAction]).toLowerCase();
+
+        this.confirmMsg = this.lionFn(appActionLc) + ' ' + this.selId;
+        if (strongWarning[this.selId]) {
+            this.strongWarning = this.warnDeactivate + '\n' + this.warnOwnRisk;
         }
+
+        this.log.debug('Initiating', this.appAction, 'of', this.selId);
     }
 
-    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;
-    }
+    /**
+     * Callback when the Confirm dialog is shown and a choice is made
+     */
+    dOk(choice: boolean) {
+        const appActionLc = (<string>AppAction[this.appAction]).toLowerCase();
+        if (choice) {
+            this.log.debug('Confirmed', appActionLc, 'on', this.selId);
 
-    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.firstCol,
-                sortDir: spar.firstDir,
+            this.wss.sendEvent(APPMGMTREQ, {
+                action: appActionLc,
+                name: this.selId,
+                sortCol: this.sortParams.firstCol,
+                sortDir: SortDir[this.sortParams.firstDir],
             });
-            if (action === 'uninstall') {
-                this.detailsPanel.hide();
+            if (this.appAction === AppAction.UNINSTALL) {
+                this.selId = '';
             } else {
-                this.wss.sendEvent(detailsReq, { id: itemId });
+                this.wss.sendEvent(DETAILSREQ, { id: this.selId });
             }
-        }
 
-        function dCancel() {
-            this.log.debug('Canceling', action, 'of', itemId);
+        } else {
+            this.log.debug('Cancelled', appActionLc, 'on', this.selId);
         }
-
-//        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);
-        }
+        this.confirmMsg = '';
+        this.strongWarning = '';
     }
 
     downloadApp() {
         if (this.ctrlBtnState.selection) {
-            (<any>this.window).location = APPURLPREFIX + this.selId + ICONURLSUFFIX;
+            (<any>this.window).location = APPURLPREFIX + this.selId + '/' + FILEDOWNLOADURL;
         }
     }
 
     /**
+     * When the file is selected this fires
+     * It passes the file on to the server through a POST request
+     * If there is an error its logged and raised to the user through Flash Component
+     */
+    fileEvent(event: any, activateImmediately?: boolean) {
+        this.log.debug('File event for', event.target.files[0]);
+        const formData = new FormData();
+        formData.append('file', event.target.files[0]);
+        let url = APPURLPREFIX + FILEUPLOADURL;
+        if (activateImmediately) {
+            url += ACTIVATEOPTION;
+        }
+        this.httpClient
+            .post<any>(APPURLPREFIX + FILEUPLOADURL, formData)
+            .subscribe(
+                data => this.log.debug(data),
+                err => {
+                    this.log.warn(err.message);
+                    this.alertMsg = err.message; // This will activate flash msg
+                }
+            );
+
+    }
+
+    /**
+     * When the upload button is clicked pass this on to the file input (hidden)
+     */
+    triggerForm() {
+        document.getElementById('uploadFile')
+                .dispatchEvent(new MouseEvent('click'));
+    }
+
+    /**
      * Read the LION bundle for App and set up the lionFn
      */
     doLion() {
@@ -267,9 +302,39 @@
         this.downloadTip = this.lionFn('tt_ctl_download');
     }
 
-    appDropped() {
-        this.activateImmediately = activateOption;
-//        $scope.$emit('FileChanged'); // TODO: Implement this
-        this.appFile = null;
+    onDrop(event: DragEvent) {
+        event.preventDefault();
+        event.stopPropagation();
+
+        const dt = event.dataTransfer;
+        const droppedFiles = dt.files;
+
+        this.log.debug(droppedFiles.length, 'File(s) dropped');
+        if (droppedFiles.length !== 1) {
+            this.log.error(DRAGDROPMSG1, droppedFiles.length, 'were dropped');
+            this.alertMsg = DRAGDROPMSG1;
+            return;
+        } else if (droppedFiles[0].name.slice(droppedFiles[0].name.length - 4) !== '.oar') {
+            this.log.error(DRAGDROPMSGEXT, droppedFiles[0].name, 'rejected');
+            this.alertMsg = DRAGDROPMSGEXT;
+            return;
+        }
+
+        const fileEvent = {
+            target: {
+                files: droppedFiles
+            }
+        };
+        this.fileEvent(fileEvent, true);
+    }
+
+    onDragOver(evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
+    }
+
+    onDragLeave(evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
     }
 }
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css b/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
index 3de7ad8..13f1847 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
@@ -23,16 +23,38 @@
     border: solid 3px #0095d6;
 }
 
-
-/* -- confirmation dialog -- */
-.light #app-dialog p.strong {
-    color: white;
-    background-color: #ce5b58;
+#ov-app .tabular-header {
+    text-align: left;
+}
+#ov-app 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;
+    background-color: #e5e5e6;
+    color: #3c3a3a;
 }
 
-.light #app-dialog.floatpanel.dialog {
-    background-color: #ffffff;
+#ov-app div.summary-list .table-body {
+    overflow:scroll;
+    max-height:70vh;
 }
+#ov-app h2 {
+    display: inline-block;
+}
+
+#ov-app, div.ctrl-btns {
+}
+
+#ov-app th, td {
+    text-align: left;
+    padding:  8px;
+}
+
 
 /* ========== DARK Theme ========== */
 
@@ -40,13 +62,3 @@
 .dark .app-title {
     color: #dddddd;
 }
-
-/* -- confirmation dialog -- */
-.dark #app-dialog p.strong {
-    color: red;
-    background-color: #ecd98e;
-}
-.dark #app-dialog.floatpanel.dialog {
-    background-color: #282528;
-    color:#ddddee;
-}
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
index bfa8d08..ce9af3a 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
@@ -103,6 +103,8 @@
 
 #application-details-panel .bottom {
     padding: 0px;
+    overflow: auto;
+    height: 200px;
 }
 
 #application-details-panel .bottom table {
diff --git a/web/gui2/src/main/webapp/app/view/apps/triggerform.directive.ts b/web/gui2/src/main/webapp/app/view/apps/triggerform.directive.ts
deleted file mode 100644
index 7134c3a..0000000
--- a/web/gui2/src/main/webapp/app/view/apps/triggerform.directive.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 2015-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the 'License');
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an 'AS IS' BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { Directive } from '@angular/core';
-import { LogService } from '../../log.service';
-
-/**
- * ONOS GUI -- Apps -- Trigger Form Directive
- */
-@Directive({
-  selector: '[onosTriggerForm]'
-})
-export class TriggerFormDirective {
-
-    constructor(
-        private log: LogService,
-    ) {
-        this.log.debug('TriggerFormDirective constructed');
-    }
-
-}