This is an automated email from the ASF dual-hosted git repository.

mmiklavcic pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/metron.git


The following commit(s) were added to refs/heads/master by this push:
     new 46e8625  METRON-2060 Improving Alerts table config pane (tiborm via 
mmiklavc) closes apache/metron#1375
46e8625 is described below

commit 46e8625865100b35ee69fe6499e8bda56197fbcd
Author: tiborm <tibor.mel...@gmail.com>
AuthorDate: Fri Apr 12 11:43:16 2019 -0600

    METRON-2060 Improving Alerts table config pane (tiborm via mmiklavc) closes 
apache/metron#1375
---
 .../alert-details/alert-details.component.html     |  59 ++++----
 .../alert-details/alert-details.component.scss     |  15 +++
 .../alerts-list/alerts-list.component.spec.ts      |   2 +-
 .../configure-table/configure-table.component.html | 148 +++++++++++++--------
 .../configure-table/configure-table.component.scss |  39 ++++++
 .../configure-table.component.spec.ts              | 147 +++++++++++++++++---
 .../configure-table/configure-table.component.ts   | 110 +++++++++------
 metron-interface/metron-alerts/src/slider.scss     |  28 ++--
 metron-interface/metron-alerts/src/styles.scss     |  17 +++
 metron-interface/metron-alerts/src/vendor.scss     |   1 +
 10 files changed, 399 insertions(+), 167 deletions(-)

diff --git 
a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html
 
b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html
index abc01ca..c4bcc88 100644
--- 
a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html
+++ 
b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html
@@ -12,7 +12,7 @@
   the specific language governing permissions and limitations under the 
License.
   -->
 <div class="metron-slider-pane-details load-right-to-left dialog1x" 
[ngClass]="{'is-meta-alert': isMetaAlert}">
-    <div class="container-fluid pl-0 h-100" [ngClass]="{'pr-0': isMetaAlert}">
+    <div class="container-fluid pl-0 pr-0 h-100">
         <div class="h-100 d-flex">
             <div class="nav-container" *ngIf="!isMetaAlert">
                 <ul class="nav flex-column">
@@ -81,33 +81,38 @@
                         </table>
                     </div>
 
-                    <div class="ml-1 my-3 form" *ngIf="activeTab === 
tabs.DETAILS">
-                        <ng-container *ngFor="let alert of alertSources; let i 
= index;" >
-                            <div class="pb-2 alert-details-title"> Alert {{ i 
+ 1 }} of {{ alertSources.length }}</div>
-                            <div *ngFor="let field of alert | 
alertDetailsKeys" class="row ml-1">
-                                <div class="col-6 mb-1 key">{{ field }}</div>  
 <div class="col-6"> {{ alert[field] }} </div>
-                            </div>
-                        </ng-container>
-                    </div>
+                    <div class="tabContainer">
+                        <div *ngIf="activeTab === tabs.DETAILS" class="ml-1 
my-3 form" >
+                            <ng-container *ngFor="let alert of alertSources; 
let i = index;" >
+                                <div class="pb-2 alert-details-title"> Alert 
{{ i + 1 }} of {{ alertSources.length }}</div>
+                                <ul>
+                                    <li *ngFor="let field of alert | 
alertDetailsKeys">
+                                        <div class="key">{{ field }}</div>
+                                        <div> {{ alert[field] }} </div>
+                                    </li>
+                                </ul>
+                            </ng-container>
+                        </div>
 
-                    <div *ngIf="activeTab === tabs.COMMENTS" class="my-4">
-                        <div> Comments <span 
*ngIf="alertCommentsWrapper.length > 0"> ({{alertCommentsWrapper.length}}) 
</span></div>
-                        <textarea class="form-control" 
[(ngModel)]="alertCommentStr"> </textarea>
-                        <button class="btn btn-mine_shaft_2" 
[disabled]="alertCommentStr.trim().length === 0" (click)="onAddComment()">ADD 
COMMENT</button>
-                        <ng-container *ngFor="let alertCommentWrapper of 
alertCommentsWrapper; let i = index">
-                            <hr>
-                            <div class="comment-container" 
data-qe-id="comment">
-                                <i
-                                    class="fa fa-trash-o"
-                                    aria-hidden="true"
-                                    (click)="onDeleteComment(i)"
-                                    data-qe-id="delete-comment"
-                                >
-                                </i>
-                                <div class="comment"> {{ 
alertCommentWrapper.alertComment.comment }} </div>
-                                <div class="font-italic username-timestamp"> - 
{{ alertCommentWrapper.alertComment.username }} - 
{{alertCommentWrapper.displayTime}}</div>
-                            </div>
-                        </ng-container>
+                        <div *ngIf="activeTab === tabs.COMMENTS" class="my-4">
+                            <div> Comments <span 
*ngIf="alertCommentsWrapper.length > 0"> ({{alertCommentsWrapper.length}}) 
</span></div>
+                            <textarea class="form-control" 
[(ngModel)]="alertCommentStr"> </textarea>
+                            <button class="btn btn-mine_shaft_2" 
[disabled]="alertCommentStr.trim().length === 0" (click)="onAddComment()">ADD 
COMMENT</button>
+                            <ng-container *ngFor="let alertCommentWrapper of 
alertCommentsWrapper; let i = index">
+                                <hr>
+                                <div class="comment-container" 
data-qe-id="comment">
+                                    <i
+                                        class="fa fa-trash-o"
+                                        aria-hidden="true"
+                                        (click)="onDeleteComment(i)"
+                                        data-qe-id="delete-comment"
+                                    >
+                                    </i>
+                                    <div class="comment"> {{ 
alertCommentWrapper.alertComment.comment }} </div>
+                                    <div class="font-italic 
username-timestamp"> - {{ alertCommentWrapper.alertComment.username }} - 
{{alertCommentWrapper.displayTime}}</div>
+                                </div>
+                            </ng-container>
+                        </div>
                     </div>
                 </div>
             </div>
diff --git 
a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss
 
b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss
index 3b10c8f..3373292 100644
--- 
a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss
+++ 
b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss
@@ -176,3 +176,18 @@ textarea {
   opacity: 0.5;
   cursor: not-allowed;
 }
+
+.tabContainer {
+  max-height: 100%;
+  height: 100%;
+  overflow: scroll;
+
+  ul {
+    padding-inline-start: 20px;
+    padding-bottom: 1rem;
+
+    li {
+      margin-bottom: 1rem;
+    }
+  }
+}
diff --git 
a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.spec.ts
 
b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.spec.ts
index 7adbbe9..fe838b3 100644
--- 
a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.spec.ts
+++ 
b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.spec.ts
@@ -31,7 +31,7 @@ import { DialogService } from 'app/service/dialog.service';
 import { Observable } from 'rxjs';
 import { Filter } from 'app/model/filter';
 
-fdescribe('AlertsListComponent', () => {
+describe('AlertsListComponent', () => {
 
   let component: AlertsListComponent;
   let fixture: ComponentFixture<AlertsListComponent>;
diff --git 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.html
 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.html
index 9cedca2..0ddf729 100644
--- 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.html
+++ 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.html
@@ -11,70 +11,100 @@
        OR CONDITIONS OF ANY KIND, either express or implied. See the License 
for
   the specific language governing permissions and limitations under the 
License.
   -->
-<div class="metron-slider-pane-editable load-right-to-left dialog2x">
-  <div class="container-fluid">
-    <div class="row mb-3">
-      <div class="col-md-12">
-        <div class="d-flex pb-3">
-          <div class="form-title font-weight-bold">Configure Table</div>
-          <i class="fa fa-times ml-auto close-button" aria-hidden="true" 
(click)="goBack()"></i>
-        </div>
-        <div class="input-group">
-          <input class="input flex-fill" data-qe-id="filter-input" type="text" 
placeholder="Filter list" #filterColResults >
-          <div class="input-group-append">
-            <button class="btn btn-secondary btn-search-clear" 
data-qe-id="filter-reset" (click)="clearFilter()"></button>
-          </div>
-        </div>
+<div class="metron-slider-pane-editable pb-0 d-flex flex-column 
load-right-to-left custom-dialog2x">
+
+  <div class="container-fluid mb-3">
+    <div class="d-flex pb-3">
+      <div class="form-title font-weight-bold">Configure Table Columns</div>
+      <i class="fa fa-times ml-auto close-button" aria-hidden="true" 
(click)="goBack()"></i>
+    </div>
+    <div class="input-group">
+      <input class="input flex-fill" data-qe-id="filter-input" type="text" 
placeholder="Filter list of available fields" #columnFilterInput >
+      <div class="input-group-append">
+        <button class="btn btn-secondary btn-search-clear" 
data-qe-id="filter-reset" (click)="clearFilter()"></button>
       </div>
     </div>
-    <div class="row mx-0">
-      <table class="table">
-        <thead>
-          <tr>
-            <th style="width: 10%"> <input id="select-deselect-all-col" 
class="fontawesome-checkbox" type="checkbox" 
(click)="onSelectDeselectAll($event)"><label 
for="select-deselect-all-col"></label> </th>
-            <th style="width: 30%"> Field </th>
-            <th style="width: 10%"> Short Name </th>
-            <th style="width: 30%"> Type  </th>
-            <th style="width: 10%"> </th>
-            <th style="width: 10%"> </th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr>
-            <td><input #selectColName id="select-deselect-score" 
class="fontawesome-checkbox" type="checkbox" [checked]="true" disabled><label 
for="select-deselect-score"></label></td>
-            <td> <span> Score </span></td>
-            <td> </td>
-            <td> <span> STRING </span></td>
-            <td> - </td>
-            <td> - </td>
-          </tr>
-          <tr *ngFor="let columns of filteredColumns; let i = index" 
[ngClass]="{'background-tiber': columns.selected}">
-            <td>
-              <input #selectColName id="select-deselect-{{ 
columns.columnMetadata.name }}" class="fontawesome-checkbox" type="checkbox" 
[checked]="columns.selected" (click)="selectColumn(columns)">
-              <label for="select-deselect-{{ columns.columnMetadata.name 
}}"></label>
-            </td>
-            <td #element>
-              <span [attr.title]="columns.key"> {{ columns.columnMetadata.name 
| centerEllipses }} </span>
-            </td>
-            <td>
-              <input class="input" attr.data-qe-id="display-name-{{ 
columns.columnMetadata.name }}" placeholder="rename" 
[(ngModel)]="columns.displayName">
-            </td>
-            <td>
-              <span class="text-uppercase"> {{ columns.columnMetadata.type }} 
</span>
-            </td>
-            <td>
-              <span id="up-{{ columns.columnMetadata.name }}" class="up" 
(click)="swapUp(i)" [ngClass]="{'disabled': i === 0}"></span>
-            </td>
-            <td>
-              <span id="down-{{ columns.columnMetadata.name }}" class="down" 
(click)="swapDown(i)" [ngClass]="{'disabled': i + 1 === 
allColumns.length}"></span>
-            </td>
-          </tr>
-        </tbody>
-      </table>
+  </div>
+
+  <div class="container-fluid pt-0 table-config d-flex flex-wrap 
overflow-auto">
+
+    <div class="d-flex w-100 justify-content-center align-items-center" 
*ngIf="!visibleColumns.length">
+        <div class="spinner-border text-info" role="status"></div>
+        <div class="pt-2"><small>Loading...</small></div>
     </div>
+
+    <table data-qe-id="table-visible" class="table" 
*ngIf="visibleColumns.length">
+      <thead>
+        <tr>
+          <th class="main-column" colspan=2> Visible </th>
+          <th> Short Name </th>
+          <th> Type  </th>
+          <th> </th>
+          <th> </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr class="background-tiber">
+          <td>
+            <button class="btn btn-secondary btn-sm" disabled>remove</button>
+          </td>
+          <td>
+            <span> Score </span>
+          </td>
+          <td> </td>
+          <td> <span> STRING </span></td>
+          <td> - </td>
+          <td> - </td>
+        </tr>
+        <tr attr.data-qe-id="row-{{ i }}" *ngFor="let column of 
visibleColumns; let i = index" [ngClass]="{'background-tiber': 
column.selected}">
+          <td>
+            <button attr.data-qe-id="remove-btn-{{ i }}" 
(click)="onColumnRemoved(column)" class="btn btn-secondary 
btn-sm">remove</button>
+          </td>
+          <td #element>
+            <span attr.data-qe-id="field-label-{{ i }}" 
[attr.title]="column.columnMetadata.name"> {{ column.columnMetadata.name | 
centerEllipses }} </span>
+          </td>
+          <td>
+            <input class="input" placeholder="rename" 
[(ngModel)]="column.displayName">
+          </td>
+          <td>
+            <span class="text-uppercase"> {{ column.columnMetadata.type }} 
</span>
+          </td>
+          <td>
+            <span id="up-{{ column.columnMetadata.name }}" class="up" 
(click)="swapUp(i)" [ngClass]="{'disabled': i === 0}"></span>
+          </td>
+          <td>
+            <span id="down-{{ column.columnMetadata.name }}" class="down" 
(click)="swapDown(i)" [ngClass]="{'disabled': i + 1 === 
visibleColumns.length}"></span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <table data-qe-id="table-available" class="table" 
*ngIf="availableColumns.length">
+      <thead>
+        <tr>
+          <th class="main-column" colspan=2> Available </th>
+          <th> Type  </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr attr.data-qe-id="row-{{ i }}" *ngFor="let column of 
filteredColumns; let i = index" [ngClass]="{'background-tiber': 
column.selected}">
+          <td>
+            <button attr.data-qe-id="add-btn-{{ i }}" 
(click)="onColumnAdded(column)" class="btn btn-primary btn-sm">add</button>
+          </td>
+          <td #element>
+            <span attr.data-qe-id="field-label-{{ i }}" 
[attr.title]="column.columnMetadata.name"> {{ column.columnMetadata.name | 
centerEllipses }} </span>
+          </td>
+          <td>
+            <span class="text-uppercase"> {{ column.columnMetadata.type }} 
</span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
   </div>
-  <div class="container-fluid metron-floating-button-bar-2x">
+
+  <div class="container-fluid custom-metron-button-bar-2x">
     <button type="submit" data-qe-id="save-table-config" class="btn 
btn-all_ports" (click)="save()">SAVE</button>
     <button class="btn btn-mine_shaft_2" (click)="goBack()">CANCEL</button>
   </div>
 </div>
+
diff --git 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.scss
 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.scss
index d17f3df..d590f0c 100644
--- 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.scss
+++ 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.scss
@@ -17,6 +17,18 @@
  */
 @import "../../../slider";
 @import "../../../variables";
+@import "../../../styles";
+
+@include keyframes("keyframe-dialog-rtl", $dialog-4x-width, "0px")
+
+.load-right-to-left {
+  @include animation("keyframe-dialog-rtl", "0.3s", "ease");
+}
+
+.custom-metron-button-bar-2x {
+  @extend .metron-button-bar-2x;
+  width: 100%;
+}
 
 .container-fluid {
   padding-top: 15px;
@@ -29,8 +41,29 @@
   text-overflow: ellipsis;
 }
 
+.custom-dialog2x {
+  width: 75%;
+
+  .table-config {
+
+    flex-grow: 1;
+    flex-wrap: wrap;
+
+    table {
+      flex: 1;
+      margin: 0 1rem 0 0;
+    }
+
+    .main-column {
+      text-transform: uppercase;
+      font-size: 1.1rem;
+    }
+  }
+}
+
 .table th, .table td {
   padding: 0.6rem 0.25rem;
+  text-overflow: ellipsis;
 }
 
 .up:before {
@@ -75,3 +108,9 @@
     padding-left: 5px;
   }
 }
+
+.btn-sm {
+  padding: 0.1rem 0.25rem;
+  font-size: 0.75rem;
+  line-height: 1.1rem;
+}
diff --git 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.spec.ts
 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.spec.ts
index bc3f21e..1dc1e3e 100644
--- 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.spec.ts
+++ 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.spec.ts
@@ -20,13 +20,14 @@ import { FormsModule } from '@angular/forms';
 import { RouterTestingModule } from '@angular/router/testing';
 import { of } from 'rxjs';
 
-import { ConfigureTableComponent } from './configure-table.component';
+import { ConfigureTableComponent, ColumnMetadataWrapper } from 
'./configure-table.component';
 import { ConfigureTableService } from '../../service/configure-table.service';
 import { SwitchComponent } from '../../shared/switch/switch.component';
 import { CenterEllipsesPipe } from '../../shared/pipes/center-ellipses.pipe';
 import { ClusterMetaDataService } from 'app/service/cluster-metadata.service';
 import { SearchService } from 'app/service/search.service';
 import { ColumnNamesService } from 'app/service/column-names.service';
+import { By } from '@angular/platform-browser';
 
 class FakeClusterMetaDataService {
   getDefaultColumns() {
@@ -113,20 +114,20 @@ describe('ConfigureTableComponent', () => {
 
     component.ngOnInit();
     component.ngAfterViewInit();
-    expect(component.filteredColumns.length).toBe(18);
+    expect(component.filteredColumns.length).toBe(10);
 
-    filter.value = 'guid';
+    filter.value = 'timestamp';
     filter.dispatchEvent(new Event('keyup'));
     tick(300);
     fixture.detectChanges();
     expect(component.filteredColumns.length).toBe(1);
-    expect(component.filteredColumns[0].columnMetadata.name).toBe('guid');
+    
expect(component.filteredColumns[0].columnMetadata.name).toBe('bro_timestamp');
 
     filter.value = '';
     filter.dispatchEvent(new Event('keyup'));
     tick(300);
     fixture.detectChanges();
-    expect(component.filteredColumns.length).toBe(18);
+    expect(component.filteredColumns.length).toBe(10);
   }));
 
   it('should reset filter input and available columns when clear button is 
clicked', fakeAsync(() => {
@@ -136,7 +137,7 @@ describe('ConfigureTableComponent', () => {
     component.ngOnInit();
     component.ngAfterViewInit();
 
-    filter.value = 'guid';
+    filter.value = 'timestamp';
     filter.dispatchEvent(new Event('keyup'));
     tick(300);
     fixture.detectChanges();
@@ -145,23 +146,131 @@ describe('ConfigureTableComponent', () => {
     filterReset.dispatchEvent(new Event('click'));
     fixture.detectChanges();
     expect(filter.value).toBe('');
-    expect(component.filteredColumns.length).toBe(18);
+    expect(component.filteredColumns.length).toBe(10);
   }));
 
-  it('should filter by display name if display name is present', fakeAsync(() 
=> {
-    const filter = 
fixture.nativeElement.querySelector('[data-qe-id="filter-input"]');
+  it('should mark default columns as visible', () => {
+    expect(component.visibleColumns.length).toBe(8);
+  });
 
-    component.ngOnInit();
-    component.ngAfterViewInit();
-    expect(component.filteredColumns.length).toBe(18);
+  it('should mark other columns as available', () => {
+    expect(component.availableColumns.length).toBe(10);
+  });
 
-    component.filteredColumns[0].displayName = 'Test Display Name';
+  it('should mark added element as selected', () => {
+    const itemToAdd: ColumnMetadataWrapper = component.availableColumns[0];
 
-    filter.value = 'test';
-    filter.dispatchEvent(new Event('keyup'));
-    tick(300);
-    fixture.detectChanges();
-    expect(component.filteredColumns.length).toBe(1);
-  }));
+    expect(itemToAdd.selected).toEqual(false);
+
+    component.onColumnAdded(itemToAdd);
+
+    expect(itemToAdd.selected).toEqual(true);
+  });
+
+  it('should mark removed element as deselected', () => {
+    const itemToRemove: ColumnMetadataWrapper = component.visibleColumns[0];
+
+    expect(itemToRemove.selected).toEqual(true);
+
+    component.onColumnRemoved(itemToRemove);
+
+    expect(itemToRemove.selected).toEqual(false);
+  });
+
+  it('should update list of visible items on add', () => {
+    const itemToAdd: ColumnMetadataWrapper = component.availableColumns[0];
+
+    expect(component.visibleColumns.includes(itemToAdd)).toEqual(false);
+
+    component.onColumnAdded(itemToAdd);
+
+    expect(component.visibleColumns.includes(itemToAdd)).toEqual(true);
+  });
+
+  it('should update list of available items on add', () => {
+    const itemToAdd: ColumnMetadataWrapper = component.availableColumns[0];
+
+    expect(component.availableColumns.includes(itemToAdd)).toEqual(true);
+
+    component.onColumnAdded(itemToAdd);
+
+    expect(component.availableColumns.includes(itemToAdd)).toEqual(false);
+  });
 
+  it('should update list of visible items on remove', () => {
+    const itemToRemove: ColumnMetadataWrapper = component.visibleColumns[0];
+
+    expect(component.visibleColumns.includes(itemToRemove)).toEqual(true);
+
+    component.onColumnRemoved(itemToRemove);
+
+    expect(component.visibleColumns.includes(itemToRemove)).toEqual(false);
+  });
+
+  it('should update list of available items on remove', () => {
+    const itemToRemove: ColumnMetadataWrapper = component.visibleColumns[0];
+
+    expect(component.availableColumns.includes(itemToRemove)).toEqual(false);
+
+    component.onColumnRemoved(itemToRemove);
+
+    expect(component.availableColumns.includes(itemToRemove)).toEqual(true);
+  });
+
+  it('should sort available items on change', () => {
+    spyOn(component.availableColumns, 'sort');
+
+    const itemToRemove: ColumnMetadataWrapper = component.visibleColumns[0];
+    component.onColumnRemoved(itemToRemove);
+
+    expect(component.availableColumns.sort).toHaveBeenCalled();
+  });
+
+  describe('Config Pane Rendering', () => {
+    it('should render visible and available items separately', () => {
+      expect(fixture.debugElement.queryAll(By.css('table')).length).toBe(2);
+      
expect(fixture.debugElement.queryAll(By.css('table'))[0].queryAll(By.css('tr')).length).toBe(10);
+      
expect(fixture.debugElement.queryAll(By.css('table'))[1].queryAll(By.css('tr')).length).toBe(11);
+    });
+
+    it('should refresh both list on remove', () => {
+      
fixture.debugElement.query(By.css('[data-qe-id="remove-btn-1"]')).nativeElement.click();
+      fixture.detectChanges();
+      
expect(fixture.debugElement.queryAll(By.css('table'))[0].queryAll(By.css('tr')).length).toBe(9);
+      
expect(fixture.debugElement.queryAll(By.css('table'))[1].queryAll(By.css('tr')).length).toBe(12);
+    });
+
+    it('should refresh both list on add', () => {
+      
fixture.debugElement.query(By.css('[data-qe-id="add-btn-4"]')).nativeElement.click();
+      fixture.detectChanges();
+      
expect(fixture.debugElement.queryAll(By.css('table'))[0].queryAll(By.css('tr')).length).toBe(11);
+      
expect(fixture.debugElement.queryAll(By.css('table'))[1].queryAll(By.css('tr')).length).toBe(10);
+    });
+
+    it('should be able to move visible item DOWN in order', () => {
+      const origIndex = 2;
+      const newIndex = 3;
+      let tableOfVisible = 
fixture.debugElement.query(By.css('[data-qe-id="table-visible"]'));
+      const rowId = 
tableOfVisible.query(By.css(`[data-qe-id="field-label-${origIndex}"]`)).nativeElement.innerText;
+
+      
tableOfVisible.query(By.css(`[data-qe-id="row-${origIndex}"]`)).query(By.css('span[id^="down-"]')).nativeElement.click();
+      fixture.detectChanges();
+
+      tableOfVisible = 
fixture.debugElement.query(By.css('[data-qe-id="table-visible"]'));
+      
expect(tableOfVisible.query(By.css(`[data-qe-id="field-label-${newIndex}"]`)).nativeElement.innerText).toBe(rowId);
+    });
+
+    it('should be able to move visible item UP in order', () => {
+      const origIndex = 3;
+      const newIndex = 2;
+      let tableOfVisible = 
fixture.debugElement.query(By.css('[data-qe-id="table-visible"]'));
+      const rowId = 
tableOfVisible.query(By.css(`[data-qe-id="field-label-${origIndex}"]`)).nativeElement.innerText;
+
+      
tableOfVisible.query(By.css(`[data-qe-id="row-${origIndex}"]`)).query(By.css('span[id^="up-"]')).nativeElement.click();
+      fixture.detectChanges();
+
+      tableOfVisible = fixture.debugElement.queryAll(By.css('table'))[0];
+      
expect(tableOfVisible.query(By.css(`[data-qe-id="field-label-${newIndex}"]`)).nativeElement.innerText).toBe(rowId);
+    });
+  });
 });
diff --git 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.ts
 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.ts
index 800e94f..8970624 100644
--- 
a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.ts
+++ 
b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.ts
@@ -16,8 +16,8 @@
  * limitations under the License.
  */
 import { Component, OnInit, ViewChild, ElementRef, AfterViewInit } from 
'@angular/core';
-import {Router, ActivatedRoute} from '@angular/router';
-import {forkJoin as observableForkJoin, fromEvent} from 'rxjs';
+import { Router, ActivatedRoute } from '@angular/router';
+import { forkJoin as observableForkJoin, fromEvent, Observable, Subject } from 
'rxjs';
 
 import {ConfigureTableService} from '../../service/configure-table.service';
 import {ClusterMetaDataService} from '../../service/cluster-metadata.service';
@@ -50,11 +50,15 @@ export class ColumnMetadataWrapper {
 })
 
 export class ConfigureTableComponent implements OnInit, AfterViewInit {
-  @ViewChild('filterColResults') filterColResults: ElementRef;
+  @ViewChild('columnFilterInput') columnFilterInput: ElementRef;
 
-  allColumns: ColumnMetadataWrapper[] = [];
-  filteredColumns: ColumnMetadataWrapper[] = [];
   columnHeaders: string;
+  allColumns$: Subject<ColumnMetadataWrapper[]> = new 
Subject<ColumnMetadataWrapper[]>();
+  visibleColumns$: Observable<ColumnMetadataWrapper[]>;
+  availableColumns$: Observable<ColumnMetadataWrapper[]>;
+  visibleColumns: ColumnMetadataWrapper[] = [];
+  availableColumns: ColumnMetadataWrapper[] = [];
+  filteredColumns: ColumnMetadataWrapper[] = [];
 
   constructor(private router: Router, private activatedRoute: ActivatedRoute,
               private configureTableService: ConfigureTableService,
@@ -91,12 +95,16 @@ export class ConfigureTableComponent implements OnInit, 
AfterViewInit {
       this.searchService.getColumnMetaData(),
       this.configureTableService.getTableMetadata()
     ).subscribe((response: any) => {
-      this.prepareData(response[0], response[1], response[2].tableColumns);
+      const allColumns = this.prepareData(response[0], response[1], 
response[2].tableColumns);
+
+      this.visibleColumns = allColumns.filter(column => column.selected);
+      this.availableColumns = allColumns.filter(column => !column.selected);
+      this.filteredColumns = this.availableColumns;
     });
   }
 
   ngAfterViewInit() {
-    fromEvent(this.filterColResults.nativeElement, 'keyup')
+    fromEvent(this.columnFilterInput.nativeElement, 'keyup')
       .pipe(debounceTime(250))
       .subscribe(e => {
         this.filterColumns(e['target'].value);
@@ -105,36 +113,27 @@ export class ConfigureTableComponent implements OnInit, 
AfterViewInit {
 
   filterColumns(val: string) {
     const words = val.trim().split(' ');
-    this.filteredColumns = this.allColumns.filter(col => {
-      return !this.isColMissingFilterKeyword(words, col, col.displayName);
+    this.filteredColumns = this.availableColumns.filter(col => {
+      return !this.isColMissingFilterKeyword(words, col);
     });
   }
 
-  isColMissingFilterKeyword(words: string[], col: ColumnMetadataWrapper, 
displayName?: string) {
-    if (displayName) {
-      return !words.every(word => 
col.displayName.toLowerCase().includes(word.toLowerCase()));
-    } else {
-      return !words.every(word => 
col.columnMetadata.name.toLowerCase().includes(word.toLowerCase()));
-    }
+  isColMissingFilterKeyword(words: string[], col: ColumnMetadataWrapper) {
+    return !words.every(word => 
col.columnMetadata.name.toLowerCase().includes(word.toLowerCase()));
   }
 
   clearFilter() {
-    this.filterColResults.nativeElement.value = '';
-    this.filteredColumns = this.allColumns;
-  }
-
-  onSelectDeselectAll($event) {
-    let checked = $event.target.checked;
-    this.allColumns.forEach(colMetaData => colMetaData.selected = checked);
+    this.columnFilterInput.nativeElement.value = '';
+    this.filteredColumns = this.availableColumns;
   }
 
   /* Slight variation of insertion sort with bucketing the items in the 
display order*/
-  prepareData(defaultColumns: ColumnMetadata[], allColumns: ColumnMetadata[], 
savedColumns: ColumnMetadata[]) {
+  prepareData(defaultColumns: ColumnMetadata[], allColumns: ColumnMetadata[], 
savedColumns: ColumnMetadata[]): ColumnMetadataWrapper[] {
     let configuredColumns: ColumnMetadata[] = (savedColumns && 
savedColumns.length > 0) ?  savedColumns : defaultColumns;
     let configuredColumnNames: string[] = configuredColumns.map((mData: 
ColumnMetadata) => mData.name);
 
     allColumns = allColumns.filter((mData: ColumnMetadata) => 
configuredColumnNames.indexOf(mData.name) === -1);
-    allColumns = allColumns.sort((mData1: ColumnMetadata, mData2: 
ColumnMetadata) => { return mData1.name.localeCompare(mData2.name); });
+    allColumns = allColumns.sort(this.defaultColumnSorter);
 
     let sortedConfiguredColumns = 
JSON.parse(JSON.stringify(configuredColumns));
     sortedConfiguredColumns = sortedConfiguredColumns.sort((mData1: 
ColumnMetadata, mData2: ColumnMetadata) => {
@@ -152,11 +151,15 @@ export class ConfigureTableComponent implements OnInit, 
AfterViewInit {
       allColumns.splice.apply(allColumns, [indexInAll, 
0].concat(itemsToInsert));
     }
 
-    this.allColumns = allColumns.map(mData => {
+    return allColumns.map(mData => {
       return new ColumnMetadataWrapper(mData, 
configuredColumnNames.indexOf(mData.name) > -1,
                                         
ColumnNamesService.columnNameToDisplayValueMap[mData.name]);
       });
-    this.filteredColumns = this.allColumns;
+    this.filteredColumns = this.availableColumns;
+  }
+
+  private defaultColumnSorter(col1: ColumnMetadata, col2: ColumnMetadata): 
number {
+    return col1.name.localeCompare(col2.name);
   }
 
   postSave() {
@@ -165,21 +168,18 @@ export class ConfigureTableComponent implements OnInit, 
AfterViewInit {
   }
 
   save() {
-    let selectedColumns = this.allColumns.filter((mDataWrapper: 
ColumnMetadataWrapper) => mDataWrapper.selected)
-                          .map((mDataWrapper: ColumnMetadataWrapper) => 
mDataWrapper.columnMetadata);
-
-    
this.configureTableService.saveColumnMetaData(selectedColumns).subscribe(() => {
-      this.saveColumnNames();
-    }, error => {
-      console.log('Unable to save column preferences ...');
-      this.saveColumnNames();
-    });
-
-
+    this.configureTableService.saveColumnMetaData(
+      this.visibleColumns.map(columnMetaWrapper => 
columnMetaWrapper.columnMetadata))
+      .subscribe(() => {
+        this.saveColumnNames();
+      }, error => {
+        console.log('Unable to save column preferences ...');
+        this.saveColumnNames();
+      });
   }
 
   saveColumnNames() {
-    let columnNames = this.allColumns.map(mDataWrapper => {
+    let columnNames = this.visibleColumns.map(mDataWrapper => {
       return new ColumnNames(mDataWrapper.columnMetadata.name, 
mDataWrapper.displayName);
     });
 
@@ -191,19 +191,43 @@ export class ConfigureTableComponent implements OnInit, 
AfterViewInit {
     });
   }
 
-  selectColumn(columns: ColumnMetadataWrapper) {
-    columns.selected = !columns.selected;
+  onColumnAdded(column: ColumnMetadataWrapper) {
+    this.markColumn(column);
+    this.swapList(column, this.availableColumns, this.visibleColumns);
+    this.filterColumns(this.columnFilterInput.nativeElement.value);
+  }
+
+  onColumnRemoved(column: ColumnMetadataWrapper) {
+    this.markColumn(column);
+    this.swapList(column, this.visibleColumns, this.availableColumns);
+    this.filterColumns(this.columnFilterInput.nativeElement.value);
+  }
+
+  private markColumn(column: ColumnMetadataWrapper) {
+    column.selected = !column.selected;
+  }
+
+  private swapList(column: ColumnMetadataWrapper,
+    source: ColumnMetadataWrapper[],
+    target: ColumnMetadataWrapper[]) {
+
+    target.push(column);
+    source.splice(source.indexOf(column), 1);
+
+    this.availableColumns.sort((colWrapper1: ColumnMetadataWrapper, 
colWrapper2: ColumnMetadataWrapper) => {
+      return this.defaultColumnSorter(colWrapper1.columnMetadata, 
colWrapper2.columnMetadata)
+    });
   }
 
   swapUp(index: number) {
     if (index > 0) {
-      [this.allColumns[index], this.allColumns[index - 1]] = 
[this.allColumns[index - 1], this.allColumns[index]];
+      [this.visibleColumns[index], this.visibleColumns[index - 1]] = 
[this.visibleColumns[index - 1], this.visibleColumns[index]];
     }
   }
 
   swapDown(index: number) {
-    if (index + 1 < this.allColumns.length) {
-      [this.allColumns[index], this.allColumns[index + 1]] = 
[this.allColumns[index + 1], this.allColumns[index]];
+    if (index + 1 < this.visibleColumns.length) {
+      [this.visibleColumns[index], this.visibleColumns[index + 1]] = 
[this.visibleColumns[index + 1], this.visibleColumns[index]];
     }
   }
 }
diff --git a/metron-interface/metron-alerts/src/slider.scss 
b/metron-interface/metron-alerts/src/slider.scss
index 5168db0..ccb139f 100644
--- a/metron-interface/metron-alerts/src/slider.scss
+++ b/metron-interface/metron-alerts/src/slider.scss
@@ -22,21 +22,18 @@ $edit-background-border: #5C5C5C;
 
 $dialog-1x-width: 340px;
 $dialog-2x-width: 680px;
-$dialog-4x-width: 1380px;
+$dialog-4x-width: 1340px;
 
 .metron-slider-pane-details
 {
-  display: inline-block;
-  float: right;
-  word-wrap: break-word;
-  height: auto;
-  min-height: 100%;
-  position: absolute;
-
+  position: fixed;
   top: 0;
+  right: 0;
+  height: 100%;
+  max-height: 100%;
   z-index: 9;
+
   background: $edit-child-background;
-  border: 1px solid $edit-background-border;
 
   .close-button
   {
@@ -48,10 +45,8 @@ $dialog-4x-width: 1380px;
 .metron-slider-pane-editable
 {
   @extend .metron-slider-pane-details;
-
-  height: auto;
   background: $eden;
-  padding-bottom: 70px;
+  padding-bottom: 80px;
   border-left: 1px solid $blue-mine;
 }
 
@@ -64,7 +59,7 @@ $dialog-4x-width: 1380px;
   }
 }
 
-@media only screen and (min-width: 2020px) {
+@media only screen and (min-width: 1900px) {
   .dialog1x {
     width: $dialog-2x-width;
   }
@@ -125,15 +120,12 @@ $dialog-4x-width: 1380px;
 
 @include keyframes("keyframe-dialog-rtl", "320px", "0px")
 
-.load-right-to-left{
+.load-right-to-left {
   @include animation("keyframe-dialog-rtl", "0.5s", "linear");
-
-  right: 0px;
-  float: right;
 }
 
 @include keyframes("keyframe-dialog-ltr", "-320px", "0px")
 
-.load-left-to-right{
+.load-left-to-right {
   @include animation("keyframe-dialog-ltr", "0.5s", "linear")
 }
diff --git a/metron-interface/metron-alerts/src/styles.scss 
b/metron-interface/metron-alerts/src/styles.scss
index 50ea6ab..4b1ce8a 100644
--- a/metron-interface/metron-alerts/src/styles.scss
+++ b/metron-interface/metron-alerts/src/styles.scss
@@ -218,6 +218,23 @@ form
   border-top: 1px solid $blue-mine;
 }
 
+.metron-button-bar-1x {
+  @extend .pb-3;
+  @extend .dialog1x;
+
+  background: $eden;
+  border-top: 1px solid $blue-mine;
+}
+
+.metron-button-bar-2x {
+  @extend .pb-3;
+  @extend .dialog2x;
+
+  background: $eden;
+  border-top: 1px solid $blue-mine;
+}
+
+
 .btn-all_ports {
   background-color: $all-ports;
   border-color: $all-ports;
diff --git a/metron-interface/metron-alerts/src/vendor.scss 
b/metron-interface/metron-alerts/src/vendor.scss
index e94169e..b5fa74c 100644
--- a/metron-interface/metron-alerts/src/vendor.scss
+++ b/metron-interface/metron-alerts/src/vendor.scss
@@ -62,6 +62,7 @@
 @import "../node_modules/bootstrap/scss/media";
 @import "../node_modules/bootstrap/scss/list-group";
 @import "../node_modules/bootstrap/scss/close";
+@import "../node_modules/bootstrap/scss/spinners";
 
 // Components w/ JavaScript
 @import "../node_modules/bootstrap/scss/modal";

Reply via email to