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 { }