You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by sa...@apache.org on 2019/11/26 17:13:11 UTC

[metron] branch master updated: METRON-2316 [UI] Drag drop sorting for the selected fields in the Alerts UI (ruffle1986 via sardell) closes apache/metron#1560

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

sardell 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 1ece1ed  METRON-2316 [UI] Drag drop sorting for the selected fields in the Alerts UI (ruffle1986 via sardell) closes apache/metron#1560
1ece1ed is described below

commit 1ece1ed0d2ac85d3d8114c45b555a657e2521be8
Author: ruffle1986 <ft...@gmail.com>
AuthorDate: Tue Nov 26 11:12:10 2019 -0600

    METRON-2316 [UI] Drag drop sorting for the selected fields in the Alerts UI (ruffle1986 via sardell) closes apache/metron#1560
---
 .../configure-table/configure-table.component.html |  29 ++++-
 .../configure-table/configure-table.component.scss |  58 ++++++++--
 .../configure-table.component.spec.ts              |  74 ++++++++++++-
 .../configure-table/configure-table.component.ts   | 122 +++++++++++++++++++--
 .../configure-table/configure-table.module.ts      |   5 +-
 5 files changed, 257 insertions(+), 31 deletions(-)

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 0ddf729..3103e79 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
@@ -33,7 +33,7 @@
         <div class="pt-2"><small>Loading...</small></div>
     </div>
 
-    <table data-qe-id="table-visible" class="table" *ngIf="visibleColumns.length">
+    <table data-qe-id="table-visible" class="configure-table table" *ngIf="visibleColumns.length">
       <thead>
         <tr>
           <th class="main-column" colspan=2> Visible </th>
@@ -43,8 +43,8 @@
           <th> </th>
         </tr>
       </thead>
-      <tbody>
-        <tr class="background-tiber">
+      <tbody [dragula]="'configure-table'">
+        <tr class="background-tiber out-of-dragula">
           <td>
             <button class="btn btn-secondary btn-sm" disabled>remove</button>
           </td>
@@ -56,7 +56,7 @@
           <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}">
+        <tr attr.data-index="{{ i }}" 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>
@@ -70,10 +70,27 @@
             <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>
+            <button id="up-{{ column.columnMetadata.name }}"
+                    class="up border-0 p-0"
+                    name="Move up"
+                    type="button"
+                    #moveColUpBtn
+                    aria-label="Move column up in display order"
+                    (mouseup)="swapUp(i, $event)"
+                    (keyup.enter)="swapUp(i, $event)"
+                    (keyup.space)="swapUp(i, $event)"
+                    [attr.disabled]="i === 0 ? true : null"
+                    [ngClass]="{'disabled': i === 0}"></button>
           </td>
           <td>
-            <span id="down-{{ column.columnMetadata.name }}" class="down" (click)="swapDown(i)" [ngClass]="{'disabled': i + 1 === visibleColumns.length}"></span>
+            <button id="down-{{ column.columnMetadata.name }}"
+                    class="down border-0 p-0"
+                    name="Move down"
+                    type="button"
+                    aria-label="Move column down in display order"
+                    (click)="swapDown(i)"
+                    [attr.disabled]="i + 1 === visibleColumns.length ? true : null"
+                    [ngClass]="{'disabled': i + 1 === visibleColumns.length}"></button>
           </td>
         </tr>
       </tbody>
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 d590f0c..f2f63f2 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
@@ -66,20 +66,30 @@
   text-overflow: ellipsis;
 }
 
-.up:before {
-  font-family: "FontAwesome";
-  font-style: normal;
-  font-size: 14px;
-  content: '\f077';
-  color: $gothic;
+.up {
+  background: transparent;
+
+  &:before {
+    font-family: "FontAwesome";
+    font-style: normal;
+    font-size: 14px;
+    content: '\f077';
+    color: $gothic;
+    cursor: pointer;
+  }
 }
 
-.down:before {
-  font-family: "FontAwesome";
-  font-style: normal;
-  font-size: 14px;
-  content: '\f078';
-  color: $gothic;
+.down {
+  background: transparent;
+
+  &:before {
+    font-family: "FontAwesome";
+    font-style: normal;
+    font-size: 14px;
+    content: '\f078';
+    color: $gothic;
+    cursor: pointer;
+  }
 }
 
 .disabled:before {
@@ -114,3 +124,27 @@
   font-size: 0.75rem;
   line-height: 1.1rem;
 }
+
+.configure-table tbody tr td {
+  cursor: move;
+}
+
+.gu-mirror {
+  opacity: 1;
+  cursor: move;
+  box-shadow: 5px 5px 5px rgba(0, 0, 0, .7);
+}
+
+.gu-mirror td {
+  display: none;
+}
+
+.gu-mirror td:nth-child(2) {
+  display: table-cell;
+  padding-left: 10px;
+  padding-right: 10px;
+  font-size: 14px;
+  font-weight: bold;
+  line-height: 44px;
+  vertical-align: middle;
+}
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 1dc1e3e..cc4c3fe 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
@@ -18,7 +18,7 @@
 import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
 import { FormsModule } from '@angular/forms';
 import { RouterTestingModule } from '@angular/router/testing';
-import { of } from 'rxjs';
+import { of, Subject } from 'rxjs';
 
 import { ConfigureTableComponent, ColumnMetadataWrapper } from './configure-table.component';
 import { ConfigureTableService } from '../../service/configure-table.service';
@@ -28,6 +28,8 @@ 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';
+import { DragulaService, DragulaModule } from 'ng2-dragula';
+import { ColumnMetadata } from 'app/model/column-metadata';
 
 class FakeClusterMetaDataService {
   getDefaultColumns() {
@@ -83,7 +85,7 @@ describe('ConfigureTableComponent', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      imports: [ FormsModule, RouterTestingModule ],
+      imports: [ FormsModule, RouterTestingModule, DragulaModule ],
       declarations: [
         ConfigureTableComponent,
         SwitchComponent,
@@ -94,6 +96,12 @@ describe('ConfigureTableComponent', () => {
         { provide: SearchService, useClass: FakeSearchService },
         { provide: ConfigureTableService, useClass: FakeConfigureTableService },
         { provide: ColumnNamesService, useClass: FakeColumnNamesService },
+        { provide: DragulaService, useValue: {
+          setOptions: () => {},
+          find: () => {},
+          drop: new Subject(),
+          add: () => {}
+        } }
       ]
     })
     .compileComponents();
@@ -253,7 +261,7 @@ describe('ConfigureTableComponent', () => {
       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();
+      tableOfVisible.query(By.css(`[data-qe-id="row-${origIndex}"]`)).query(By.css('button[id^="down-"]')).nativeElement.click();
       fixture.detectChanges();
 
       tableOfVisible = fixture.debugElement.query(By.css('[data-qe-id="table-visible"]'));
@@ -263,14 +271,72 @@ describe('ConfigureTableComponent', () => {
     it('should be able to move visible item UP in order', () => {
       const origIndex = 3;
       const newIndex = 2;
+      const event = new MouseEvent('mouseup', { });
       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();
+      tableOfVisible
+        .query(By.css(`[data-qe-id="row-${origIndex}"]`))
+        .query(By.css('button[id^="up-"]'))
+        .nativeElement.dispatchEvent(event);
       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);
     });
+
+    it('should rearrange the visible columns properly', () => {
+      const draguleService = TestBed.get(DragulaService);
+
+      let el = document.createElement('tr');
+      el.dataset.index = '1';
+
+      component.visibleColumns = [
+        new ColumnMetadataWrapper(new ColumnMetadata('lorem', 'ipsum'), false, 'foo'),
+        new ColumnMetadataWrapper(new ColumnMetadata('lorem', 'ipsum'), false, 'bar'),
+        new ColumnMetadataWrapper(new ColumnMetadata('lorem', 'ipsum'), false, 'lorem'),
+        new ColumnMetadataWrapper(new ColumnMetadata('lorem', 'ipsum'), false, 'ipsum'),
+        new ColumnMetadataWrapper(new ColumnMetadata('lorem', 'ipsum'), false, 'amet'),
+      ];
+
+      // el is on index 1 and there's no sibling so bar goes to the end
+      draguleService.drop.next(['group', el, null, null]);
+      expect(component.visibleColumns.map(item => item.displayName)).toEqual([
+        'foo', 'lorem', 'ipsum', 'amet', 'bar'
+      ]);
+
+      // the sibling is the first element so el (amet) goes to the beginning
+      let sibling = document.createElement('tr');
+      sibling.dataset.index = '0';
+      el = document.createElement('tr');
+      el.dataset.index = '3';
+
+      draguleService.drop.next(['group', el, null, null, sibling]);
+      expect(component.visibleColumns.map(item => item.displayName)).toEqual([
+        'amet', 'foo', 'lorem', 'ipsum', 'bar'
+      ]);
+
+      // putting the item on index 1 (foo) before the item on index 3 (ipsum)
+      sibling = document.createElement('tr');
+      sibling.dataset.index = '3';
+      el = document.createElement('tr');
+      el.dataset.index = '1';
+
+      draguleService.drop.next(['foo', el, null, null, sibling]);
+      expect(component.visibleColumns.map(item => item.displayName)).toEqual([
+        'amet', 'lorem', 'foo', 'ipsum', 'bar'
+      ]);
+
+      // putting the item on index 3 (ipsum) before the item on index 1 (lorem)
+      sibling = document.createElement('tr');
+      sibling.dataset.index = '1';
+      el = document.createElement('tr');
+      el.dataset.index = '3';
+
+      draguleService.drop.next(['foo', el, null, null, sibling]);
+      expect(component.visibleColumns.map(item => item.displayName)).toEqual([
+        'amet', 'ipsum', 'lorem', 'foo', 'bar'
+      ]);
+    });
   });
 });
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 8970624..85f4f88 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
@@ -15,7 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
+import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
 import { Router, ActivatedRoute } from '@angular/router';
 import { forkJoin as observableForkJoin, fromEvent, Observable, Subject } from 'rxjs';
 
@@ -26,6 +26,7 @@ import {ColumnNamesService} from '../../service/column-names.service';
 import {ColumnNames} from '../../model/column-names';
 import {SearchService} from '../../service/search.service';
 import { debounceTime } from 'rxjs/operators';
+import { DragulaService } from 'ng2-dragula';
 
 export enum AlertState {
   NEW, OPEN, ESCALATE, DISMISS, RESOLVE
@@ -51,6 +52,7 @@ export class ColumnMetadataWrapper {
 
 export class ConfigureTableComponent implements OnInit, AfterViewInit {
   @ViewChild('columnFilterInput') columnFilterInput: ElementRef;
+  @ViewChildren('moveColUpBtn') moveColUpBtn: QueryList<ElementRef>;
 
   columnHeaders: string;
   allColumns$: Subject<ColumnMetadataWrapper[]> = new Subject<ColumnMetadataWrapper[]>();
@@ -60,11 +62,106 @@ export class ConfigureTableComponent implements OnInit, AfterViewInit {
   availableColumns: ColumnMetadataWrapper[] = [];
   filteredColumns: ColumnMetadataWrapper[] = [];
 
-  constructor(private router: Router, private activatedRoute: ActivatedRoute,
-              private configureTableService: ConfigureTableService,
-              private clusterMetaDataService: ClusterMetaDataService,
-              private columnNamesService: ColumnNamesService,
-              private searchService: SearchService) { }
+  constructor(
+    private router: Router, private activatedRoute: ActivatedRoute,
+    private configureTableService: ConfigureTableService,
+    private clusterMetaDataService: ClusterMetaDataService,
+    private columnNamesService: ColumnNamesService,
+    private searchService: SearchService,
+    private dragulaService: DragulaService,
+    private cdRef: ChangeDetectorRef
+  ) {
+      if (!dragulaService.find('configure-table')) {
+        dragulaService.setOptions('configure-table', {
+          /**
+           * In the list of alerts there can be certain items which should not be allowed to be dragged.
+           * This is a simple solution where you can prevent items from being dragged by adding the
+           * out-of-dragula class on the list item in the html template.
+           *
+           * Reference: https://github.com/bevacqua/dragula#optionsmoves
+           */
+          moves(el: HTMLElement) {
+            return !(el.classList.contains('out-of-dragula'));
+          },
+          /**
+           * This is the same as above but it's about not allowing an element to be a drop target.
+           *
+           * Reference: https://github.com/bevacqua/dragula#optionsaccepts
+           */
+          accepts(el, target, source, sibling) {
+            if (!sibling) {
+              return true;
+            }
+            return !(sibling.classList.contains('out-of-dragula'));
+          }
+        });
+      }
+
+      /**
+       *
+       * I cannot rely on dragula's internal syncing mechanism because it doesn't force angular to re-render
+       * the component. But it's vital here because the state of the list items changes after changing the order
+       * (e.g the user is also able to reorder the list by clicking on the arrows on the right).
+       *
+       * That's why I'm subscribing the drop event here and rearrange the array manually.
+       *
+       * References:
+       * https://github.com/bevacqua/dragula#drakeon-events
+       *
+       * params[0] {String} - groupName (the name of the dragula group)
+       * params[1] {HTMLElement} - el (the dragged element)
+       * params[2] {HTMLElement} - target (the target container)
+       * params[3] {HTMLElement} - source (the source container)
+       * params[4] {HTMLElement} - sibling (after dropping the dragged element, this is the following element)
+       */
+      dragulaService.drop.subscribe((params: any[]) => {
+        const el = params[1] as HTMLElement;
+        const elIndex = +el.dataset.index;
+        const colToMove = this.visibleColumns[elIndex];
+        const cols = this.visibleColumns.filter((item, i) => i !== elIndex);
+        const sibling = params[4] as HTMLElement;
+
+        /**
+         * if there's no sibling, it means that the user is moving the item to the end of the list
+         */
+        if (!sibling) {
+          this.visibleColumns = [
+            ...cols,
+            colToMove
+          ];
+        } else {
+          const siblingIndex = +sibling.dataset.index;
+          /**
+           * if the index of the sibling is 0, it means that the user is moving the item to the
+           * beginning of the list
+           */
+          if (siblingIndex === 0) {
+            this.visibleColumns = [
+              colToMove,
+              ...cols
+            ];
+          } else {
+            /**
+             * Otherwise I'm putting the element in the appropriate place within the array
+             * by applying a simple reduce function to rearrange the array items.
+             */
+            this.visibleColumns = cols.reduce((acc, item, i) => {
+              if (elIndex < siblingIndex) { // if the dragged element took place before the new sibling originally
+                if (i === siblingIndex - 1) {
+                  acc.push(colToMove);
+                }
+              } else { // if the dragged element took place after the new sibling originally
+                if (i === siblingIndex) {
+                  acc.push(colToMove);
+                }
+              }
+              acc.push(item);
+              return acc;
+            }, []);
+          }
+        }
+      });
+  }
 
   goBack() {
     this.router.navigateByUrl('/alerts-list');
@@ -219,10 +316,21 @@ export class ConfigureTableComponent implements OnInit, AfterViewInit {
     });
   }
 
-  swapUp(index: number) {
+  swapUp(index: number, event: any) {
+    const colUpButtons = this.moveColUpBtn.toArray();
     if (index > 0) {
       [this.visibleColumns[index], this.visibleColumns[index - 1]] = [this.visibleColumns[index - 1], this.visibleColumns[index]];
     }
+    /**
+    *  The default behavior of the browser causes the up arrow button to lose focus
+    *  on enter or space keypress, which differs in behavior when compared to the down arrow button.
+    *  This condition runs change detection (which removes the focus by applying default browser behavior)
+    *  and then re-applies focus to the up arrow.
+    */
+    if (event.type === 'keyup') {
+      this.cdRef.detectChanges();
+      colUpButtons[index].nativeElement.focus();
+    }
   }
 
   swapDown(index: number) {
diff --git a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.module.ts b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.module.ts
index e67a63e..3504e66 100644
--- a/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.module.ts
+++ b/metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.module.ts
@@ -21,10 +21,11 @@ import {SharedModule} from '../../shared/shared.module';
 import {ConfigureTableComponent} from './configure-table.component';
 import {ClusterMetaDataService} from '../../service/cluster-metadata.service';
 import {ColumnNamesService} from '../../service/column-names.service';
+import { DragulaModule, DragulaService } from 'ng2-dragula';
 
 @NgModule ({
-    imports: [ routing,  SharedModule],
+    imports: [ routing,  SharedModule, DragulaModule ],
     declarations: [ ConfigureTableComponent ],
-    providers: [ ClusterMetaDataService, ColumnNamesService ]
+    providers: [ ClusterMetaDataService, ColumnNamesService, DragulaService ]
 })
 export class ConfigureTableModule { }