metron-interface/metron-alerts/src/app/alerts/configure-table/configure-table.component.ts (237 lines of code) (raw):
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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';
import {ConfigureTableService} from '../../service/configure-table.service';
import {ClusterMetaDataService} from '../../service/cluster-metadata.service';
import {ColumnMetadata} from '../../model/column-metadata';
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
}
export class ColumnMetadataWrapper {
columnMetadata: ColumnMetadata;
displayName: string;
selected: boolean;
constructor(columnMetadata: ColumnMetadata, selected: boolean, displayName: string) {
this.columnMetadata = columnMetadata;
this.selected = selected;
this.displayName = displayName;
}
}
@Component({
selector: 'app-configure-table',
templateUrl: './configure-table.component.html',
styleUrls: ['./configure-table.component.scss']
})
export class ConfigureTableComponent implements OnInit, AfterViewInit {
@ViewChild('columnFilterInput') columnFilterInput: ElementRef;
@ViewChildren('moveColUpBtn') moveColUpBtn: QueryList<ElementRef>;
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,
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');
return false;
}
indexOf(columnMetadata: ColumnMetadata, configuredColumns: ColumnMetadata[]): number {
for (let i = 0; i < configuredColumns.length; i++) {
if (configuredColumns[i].name === columnMetadata.name) {
return i;
}
}
}
indexToInsert(columnMetadata: ColumnMetadata, allColumns: ColumnMetadata[], configuredColumnNames: string[]): number {
let i = 0;
for ( ; i < allColumns.length; i++) {
if (configuredColumnNames.indexOf(allColumns[i].name) === -1 && columnMetadata.name.localeCompare(allColumns[i].name) === -1 ) {
break;
}
}
return i;
}
ngOnInit() {
observableForkJoin(
this.clusterMetaDataService.getDefaultColumns(),
this.searchService.getColumnMetaData(),
this.configureTableService.getTableMetadata()
).subscribe((response: any) => {
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.columnFilterInput.nativeElement, 'keyup')
.pipe(debounceTime(250))
.subscribe(e => {
this.filterColumns(e['target'].value);
});
}
filterColumns(val: string) {
const words = val.trim().split(' ');
this.filteredColumns = this.availableColumns.filter(col => {
return !this.isColMissingFilterKeyword(words, col);
});
}
isColMissingFilterKeyword(words: string[], col: ColumnMetadataWrapper) {
return !words.every(word => col.columnMetadata.name.toLowerCase().includes(word.toLowerCase()));
}
clearFilter() {
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[]): 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(this.defaultColumnSorter);
let sortedConfiguredColumns = JSON.parse(JSON.stringify(configuredColumns));
sortedConfiguredColumns = sortedConfiguredColumns.sort((mData1: ColumnMetadata, mData2: ColumnMetadata) => {
return mData1.name.localeCompare(mData2.name);
});
while (configuredColumns.length > 0 ) {
let columnMetadata = sortedConfiguredColumns.shift();
let index = this.indexOf(columnMetadata, configuredColumns);
let itemsToInsert: any[] = configuredColumns.splice(0, index + 1);
let indexInAll = this.indexToInsert(columnMetadata, allColumns, configuredColumnNames);
allColumns.splice.apply(allColumns, [indexInAll, 0].concat(itemsToInsert));
}
return allColumns.map(mData => {
return new ColumnMetadataWrapper(mData, configuredColumnNames.indexOf(mData.name) > -1,
ColumnNamesService.columnNameToDisplayValueMap[mData.name]);
});
this.filteredColumns = this.availableColumns;
}
private defaultColumnSorter(col1: ColumnMetadata, col2: ColumnMetadata): number {
return col1.name.localeCompare(col2.name);
}
postSave() {
this.configureTableService.fireTableChanged();
this.goBack();
}
save() {
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.visibleColumns.map(mDataWrapper => {
return new ColumnNames(mDataWrapper.columnMetadata.name, mDataWrapper.displayName);
});
this.columnNamesService.save(columnNames).subscribe(() => {
this.postSave();
}, error => {
console.log('Unable to column names ...');
this.postSave();
});
}
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, 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) {
if (index + 1 < this.visibleColumns.length) {
[this.visibleColumns[index], this.visibleColumns[index + 1]] = [this.visibleColumns[index + 1], this.visibleColumns[index]];
}
}
}