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";