34 changed files with 1444 additions and 303 deletions
@ -0,0 +1,135 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed 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. |
|||
|
|||
--> |
|||
<div class="tb-table-widget tb-absolute-fill"> |
|||
<div fxFlex fxLayout="column" class="tb-absolute-fill"> |
|||
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && alarmsDatasource.selection.isEmpty()"> |
|||
<div class="mat-toolbar-tools"> |
|||
<button mat-button mat-icon-button |
|||
matTooltip="{{ 'action.search' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
<mat-form-field fxFlex> |
|||
<mat-label> </mat-label> |
|||
<input #searchInput matInput |
|||
[(ngModel)]="pageLink.textSearch" |
|||
placeholder="{{ 'alarm.search' | translate }}"/> |
|||
</mat-form-field> |
|||
<button mat-button mat-icon-button (click)="exitFilterMode()" |
|||
matTooltip="{{ 'action.close' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="!alarmsDatasource.selection.isEmpty()"> |
|||
<div class="mat-toolbar-tools"> |
|||
<span> |
|||
{{ translate.get('alarm.selected-alarms', |
|||
{count: alarmsDatasource.selection.selected.length}) | async }} |
|||
</span> |
|||
<span fxFlex></span> |
|||
<button *ngIf="allowAcknowledgment" |
|||
mat-button mat-icon-button [disabled]="isLoading$ | async" |
|||
matTooltip="{{ 'alarm.acknowledge' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="ackAlarms($event)"> |
|||
<mat-icon>done</mat-icon> |
|||
</button> |
|||
<button mat-button mat-icon-button |
|||
[disabled]="isLoading$ | async" |
|||
matTooltip="{{ 'alarm.clear' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="clearAlarms($event)"> |
|||
<mat-icon>clear</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<div fxFlex class="table-container"> |
|||
<mat-table [dataSource]="alarmsDatasource" |
|||
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
|||
<ng-container matColumnDef="select" sticky> |
|||
<mat-header-cell *matHeaderCellDef style="width: 30px;"> |
|||
<mat-checkbox (change)="$event ? alarmsDatasource.masterToggle() : null" |
|||
[checked]="alarmsDatasource.selection.hasValue() && (alarmsDatasource.isAllSelected() | async)" |
|||
[indeterminate]="alarmsDatasource.selection.hasValue() && !(alarmsDatasource.isAllSelected() | async)"> |
|||
</mat-checkbox> |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let alarm" style="width: 30px;"> |
|||
<mat-checkbox (click)="$event.stopPropagation();" |
|||
(change)="$event ? alarmsDatasource.toggleSelection(alarm) : null" |
|||
[checked]="alarmsDatasource.isSelected(alarm)"> |
|||
</mat-checkbox> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;"> |
|||
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let alarm;" |
|||
[innerHTML]="cellContent(alarm, column)" |
|||
[ngStyle]="cellStyle(alarm, column)"> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ width: (actionCellDescriptors.length * 36) + 'px' }"> |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let alarm" [ngStyle.gt-md]="{ width: (actionCellDescriptors.length * 36) + 'px' }"> |
|||
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
|||
<button mat-button mat-icon-button [disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)" |
|||
*ngFor="let actionDescriptor of actionCellDescriptors" |
|||
matTooltip="{{ actionDescriptor.displayName }}" |
|||
matTooltipPosition="above" |
|||
(click)="onActionButtonClick($event, alarm, actionDescriptor)"> |
|||
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div fxHide fxShow.lt-lg> |
|||
<button mat-button mat-icon-button |
|||
(click)="$event.stopPropagation(); ctx.detectChanges();" |
|||
[matMenuTriggerFor]="cellActionsMenu"> |
|||
<mat-icon class="material-icons">more_vert</mat-icon> |
|||
</button> |
|||
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
|||
<button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors" |
|||
[disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)" |
|||
(click)="onActionButtonClick($event, alarm, actionDescriptor)"> |
|||
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
|||
<span>{{ actionDescriptor.displayName }}</span> |
|||
</button> |
|||
</mat-menu> |
|||
</div> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
|||
<mat-row [ngClass]="{'mat-row-select': enableSelection, |
|||
'mat-selected': alarmsDatasource.isSelected(alarm), |
|||
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm)}" |
|||
*matRowDef="let alarm; columns: displayedColumns;" |
|||
(click)="onRowClick($event, alarm)"></mat-row> |
|||
</mat-table> |
|||
<span [fxShow]="alarmsDatasource.isEmpty() | async" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found" translate>alarm.no-alarms-prompt</span> |
|||
</div> |
|||
<mat-divider *ngIf="displayPagination"></mat-divider> |
|||
<mat-paginator *ngIf="displayPagination" |
|||
[length]="alarmsDatasource.total() | async" |
|||
[pageIndex]="pageLink.page" |
|||
[pageSize]="pageLink.pageSize" |
|||
[pageSizeOptions]="pageSizeOptions"></mat-paginator> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,19 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed 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. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
@ -0,0 +1,770 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed 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 { |
|||
AfterViewInit, |
|||
Component, |
|||
ElementRef, |
|||
EventEmitter, |
|||
Input, |
|||
NgZone, |
|||
OnInit, |
|||
ViewChild, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; |
|||
import { Datasource, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models'; |
|||
import { IWidgetSubscription } from '@core/api/widget-api.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { deepClone, isDefined, isNumber } from '@core/utils'; |
|||
import cssjs from '@core/css/css'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; |
|||
import { DataSource } from '@angular/cdk/typings/collections'; |
|||
import { CollectionViewer, SelectionModel } from '@angular/cdk/collections'; |
|||
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; |
|||
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
|||
import { entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { catchError, debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators'; |
|||
import { MatPaginator } from '@angular/material/paginator'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; |
|||
import { |
|||
CellContentInfo, |
|||
CellStyleInfo, |
|||
constructTableCssString, |
|||
DisplayColumn, |
|||
EntityColumn, |
|||
fromAlarmColumnDef, |
|||
getAlarmValue, |
|||
getCellContentInfo, |
|||
getCellStyleInfo, |
|||
getColumnWidth, |
|||
TableWidgetDataKeySettings, |
|||
TableWidgetSettings, |
|||
toAlarmColumnDef |
|||
} from '@home/components/widget/lib/table-widget.models'; |
|||
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
|||
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; |
|||
import { |
|||
DISPLAY_COLUMNS_PANEL_DATA, |
|||
DisplayColumnsPanelComponent, |
|||
DisplayColumnsPanelData |
|||
} from '@home/components/widget/lib/display-columns-panel.component'; |
|||
import { |
|||
alarmFields, |
|||
AlarmInfo, |
|||
alarmSeverityColors, |
|||
alarmSeverityTranslations, |
|||
AlarmStatus, |
|||
alarmStatusTranslations |
|||
} from '@shared/models/alarm.models'; |
|||
import { DatePipe } from '@angular/common'; |
|||
|
|||
interface AlarmsTableWidgetSettings extends TableWidgetSettings { |
|||
alarmsTitle: string; |
|||
enableSelection: boolean; |
|||
enableStatusFilter: boolean; |
|||
displayDetails: boolean; |
|||
allowAcknowledgment: boolean; |
|||
allowClear: boolean; |
|||
} |
|||
|
|||
interface AlarmsTableDataKeySettings extends TableWidgetDataKeySettings { |
|||
} |
|||
|
|||
interface AlarmWidgetActionDescriptor extends WidgetActionDescriptor { |
|||
details?: boolean; |
|||
acknowledge?: boolean; |
|||
clear?: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-alarms-table-widget', |
|||
templateUrl: './alarms-table-widget.component.html', |
|||
styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss'] |
|||
}) |
|||
export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef; |
|||
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; |
|||
@ViewChild(MatSort, {static: false}) sort: MatSort; |
|||
|
|||
public enableSelection = true; |
|||
public displayPagination = true; |
|||
public pageSizeOptions; |
|||
public pageLink: PageLink; |
|||
public sortOrderProperty: string; |
|||
public textSearchMode = false; |
|||
public columns: Array<EntityColumn> = []; |
|||
public displayedColumns: string[] = []; |
|||
public actionCellDescriptors: AlarmWidgetActionDescriptor[] = []; |
|||
public alarmsDatasource: AlarmsDatasource; |
|||
|
|||
private settings: AlarmsTableWidgetSettings; |
|||
private widgetConfig: WidgetConfig; |
|||
private subscription: IWidgetSubscription; |
|||
private alarmSource: Datasource; |
|||
|
|||
private displayDetails = true; |
|||
private allowAcknowledgment = true; |
|||
private allowClear = true; |
|||
|
|||
private defaultPageSize = 10; |
|||
private defaultSortOrder = '-' + alarmFields.createdTime.value; |
|||
|
|||
private contentsInfo: {[key: string]: CellContentInfo} = {}; |
|||
private stylesInfo: {[key: string]: CellStyleInfo} = {}; |
|||
private columnWidth: {[key: string]: string} = {}; |
|||
|
|||
private searchAction: WidgetAction = { |
|||
name: 'action.search', |
|||
show: true, |
|||
icon: 'search', |
|||
onAction: () => { |
|||
this.enterFilterMode(); |
|||
} |
|||
}; |
|||
|
|||
private columnDisplayAction: WidgetAction = { |
|||
name: 'entity.columns-to-display', |
|||
show: true, |
|||
icon: 'view_column', |
|||
onAction: ($event) => { |
|||
this.editColumnsToDisplay($event); |
|||
} |
|||
}; |
|||
|
|||
private statusFilterAction: WidgetAction = { |
|||
name: 'alarm.alarm-status-filter', |
|||
show: true, |
|||
onAction: ($event) => { |
|||
this.editAlarmStatusFilter($event); |
|||
}, |
|||
icon: 'filter_list' |
|||
}; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private elementRef: ElementRef, |
|||
private ngZone: NgZone, |
|||
private overlay: Overlay, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private utils: UtilsService, |
|||
public translate: TranslateService, |
|||
private domSanitizer: DomSanitizer, |
|||
private datePipe: DatePipe) { |
|||
super(store); |
|||
|
|||
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); |
|||
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.ctx.$scope.alarmsTableWidget = this; |
|||
this.settings = this.ctx.settings; |
|||
this.widgetConfig = this.ctx.widgetConfig; |
|||
this.subscription = this.ctx.defaultSubscription; |
|||
this.alarmSource = this.subscription.alarmSource; |
|||
this.initializeConfig(); |
|||
this.updateAlarmSource(); |
|||
this.ctx.updateWidgetParams(); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
fromEvent(this.searchInputField.nativeElement, 'keyup') |
|||
.pipe( |
|||
debounceTime(150), |
|||
distinctUntilChanged(), |
|||
tap(() => { |
|||
if (this.displayPagination) { |
|||
this.paginator.pageIndex = 0; |
|||
} |
|||
this.updateData(); |
|||
}) |
|||
) |
|||
.subscribe(); |
|||
|
|||
if (this.displayPagination) { |
|||
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); |
|||
} |
|||
(this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) |
|||
.pipe( |
|||
tap(() => this.updateData()) |
|||
) |
|||
.subscribe(); |
|||
this.updateData(); |
|||
} |
|||
|
|||
public onDataUpdated() { |
|||
this.ngZone.run(() => { |
|||
this.alarmsDatasource.updateAlarms(this.subscription.alarms); |
|||
this.ctx.detectChanges(); |
|||
}); |
|||
} |
|||
|
|||
private initializeConfig() { |
|||
this.ctx.widgetActions = [this.searchAction, this.statusFilterAction, this.columnDisplayAction]; |
|||
|
|||
this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; |
|||
this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true; |
|||
this.allowClear = isDefined(this.settings.allowClear) ? this.settings.allowClear : true; |
|||
|
|||
if (this.displayDetails) { |
|||
this.actionCellDescriptors.push( |
|||
{ |
|||
displayName: this.translate.instant('alarm.details'), |
|||
icon: 'more_horiz', |
|||
details: true |
|||
} as AlarmWidgetActionDescriptor |
|||
); |
|||
} |
|||
|
|||
if (this.allowAcknowledgment) { |
|||
this.actionCellDescriptors.push( |
|||
{ |
|||
displayName: this.translate.instant('alarm.acknowledge'), |
|||
icon: 'done', |
|||
acknowledge: true |
|||
} as AlarmWidgetActionDescriptor |
|||
); |
|||
} |
|||
|
|||
if (this.allowClear) { |
|||
this.actionCellDescriptors.push( |
|||
{ |
|||
displayName: this.translate.instant('alarm.clear'), |
|||
icon: 'clear', |
|||
clear: true |
|||
} as AlarmWidgetActionDescriptor |
|||
); |
|||
} |
|||
|
|||
this.actionCellDescriptors = this.actionCellDescriptors.concat(this.ctx.actionsApi.getActionDescriptors('actionCellButton')); |
|||
|
|||
let alarmsTitle: string; |
|||
|
|||
if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) { |
|||
alarmsTitle = this.utils.customTranslation(this.settings.alarmsTitle, this.settings.alarmsTitle); |
|||
} else { |
|||
alarmsTitle = this.translate.instant('alarm.alarms'); |
|||
} |
|||
|
|||
this.ctx.widgetTitle = this.utils.createLabelFromDatasource(this.alarmSource, alarmsTitle); |
|||
|
|||
this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true; |
|||
if (!this.allowAcknowledgment && !this.allowClear) { |
|||
this.enableSelection = false; |
|||
} |
|||
|
|||
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; |
|||
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; |
|||
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; |
|||
this.statusFilterAction.show = isDefined(this.settings.enableStatusFilter) ? this.settings.enableStatusFilter : true; |
|||
|
|||
const pageSize = this.settings.defaultPageSize; |
|||
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { |
|||
this.defaultPageSize = pageSize; |
|||
} |
|||
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3]; |
|||
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; |
|||
|
|||
const cssString = constructTableCssString(this.widgetConfig); |
|||
const cssParser = new cssjs(); |
|||
cssParser.testMode = false; |
|||
const namespace = 'alarms-table-' + this.utils.hashCode(cssString); |
|||
cssParser.cssPreviewNamespace = namespace; |
|||
cssParser.createStyleElement(namespace, cssString); |
|||
$(this.elementRef.nativeElement).addClass(namespace); |
|||
} |
|||
|
|||
private updateAlarmSource() { |
|||
|
|||
if (this.enableSelection) { |
|||
this.displayedColumns.push('select'); |
|||
} |
|||
|
|||
if (this.alarmSource) { |
|||
this.alarmSource.dataKeys.forEach((_dataKey) => { |
|||
const dataKey: EntityColumn = deepClone(_dataKey) as EntityColumn; |
|||
dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label); |
|||
dataKey.def = 'def' + this.columns.length; |
|||
const keySettings: AlarmsTableDataKeySettings = dataKey.settings; |
|||
|
|||
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings); |
|||
this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); |
|||
this.columnWidth[dataKey.def] = getColumnWidth(keySettings); |
|||
this.columns.push(dataKey); |
|||
}); |
|||
this.displayedColumns.push(...this.columns.map(column => column.def)); |
|||
} |
|||
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { |
|||
this.defaultSortOrder = this.settings.defaultSortOrder; |
|||
} |
|||
this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder); |
|||
this.sortOrderProperty = toAlarmColumnDef(this.pageLink.sortOrder.property, this.columns); |
|||
|
|||
if (this.actionCellDescriptors.length) { |
|||
this.displayedColumns.push('actions'); |
|||
} |
|||
this.alarmsDatasource = new AlarmsDatasource(); |
|||
if (this.enableSelection) { |
|||
this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => { |
|||
const hideTitlePanel = selectionMode || this.textSearchMode; |
|||
if (this.ctx.hideTitlePanel !== hideTitlePanel) { |
|||
this.ctx.hideTitlePanel = hideTitlePanel; |
|||
this.ctx.detectChanges(true); |
|||
} else { |
|||
this.ctx.detectChanges(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private editColumnsToDisplay($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const target = $event.target || $event.srcElement || $event.currentTarget; |
|||
const config = new OverlayConfig(); |
|||
config.backdropClass = 'cdk-overlay-transparent-backdrop'; |
|||
config.hasBackdrop = true; |
|||
const connectedPosition: ConnectedPosition = { |
|||
originX: 'end', |
|||
originY: 'bottom', |
|||
overlayX: 'end', |
|||
overlayY: 'top' |
|||
}; |
|||
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) |
|||
.withPositions([connectedPosition]); |
|||
|
|||
const overlayRef = this.overlay.create(config); |
|||
overlayRef.backdropClick().subscribe(() => { |
|||
overlayRef.dispose(); |
|||
}); |
|||
|
|||
const columns: DisplayColumn[] = this.columns.map(column => { |
|||
return { |
|||
title: column.title, |
|||
def: column.def, |
|||
display: this.displayedColumns.indexOf(column.def) > -1 |
|||
} |
|||
}); |
|||
|
|||
const injectionTokens = new WeakMap<any, any>([ |
|||
[DISPLAY_COLUMNS_PANEL_DATA, { |
|||
columns, |
|||
columnsUpdated: (newColumns) => { |
|||
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def); |
|||
if (this.enableSelection) { |
|||
this.displayedColumns.unshift('select'); |
|||
} |
|||
this.displayedColumns.push('actions'); |
|||
} |
|||
} as DisplayColumnsPanelData], |
|||
[OverlayRef, overlayRef] |
|||
]); |
|||
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); |
|||
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, |
|||
this.viewContainerRef, injector)); |
|||
this.ctx.detectChanges(); |
|||
} |
|||
|
|||
private editAlarmStatusFilter($event: Event) { |
|||
// TODO:
|
|||
} |
|||
|
|||
private enterFilterMode() { |
|||
this.textSearchMode = true; |
|||
this.pageLink.textSearch = ''; |
|||
this.ctx.hideTitlePanel = true; |
|||
this.ctx.detectChanges(true); |
|||
setTimeout(() => { |
|||
this.searchInputField.nativeElement.focus(); |
|||
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
|||
}, 10); |
|||
} |
|||
|
|||
exitFilterMode() { |
|||
this.textSearchMode = false; |
|||
this.pageLink.textSearch = null; |
|||
if (this.displayPagination) { |
|||
this.paginator.pageIndex = 0; |
|||
} |
|||
this.updateData(); |
|||
this.ctx.hideTitlePanel = false; |
|||
this.ctx.detectChanges(true); |
|||
} |
|||
|
|||
private updateData() { |
|||
if (this.displayPagination) { |
|||
this.pageLink.page = this.paginator.pageIndex; |
|||
this.pageLink.pageSize = this.paginator.pageSize; |
|||
} else { |
|||
this.pageLink.page = 0; |
|||
} |
|||
this.pageLink.sortOrder.property = fromAlarmColumnDef(this.sort.active, this.columns); |
|||
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
|||
this.alarmsDatasource.loadAlarms(this.pageLink); |
|||
this.ctx.detectChanges(); |
|||
} |
|||
|
|||
public trackByColumnDef(index, column: EntityColumn) { |
|||
return column.def; |
|||
} |
|||
|
|||
public headerStyle(key: EntityColumn): any { |
|||
const columnWidth = this.columnWidth[key.def]; |
|||
return { |
|||
width: columnWidth |
|||
} |
|||
} |
|||
|
|||
public cellStyle(alarm: AlarmInfo, key: EntityColumn): any { |
|||
let style: any = {}; |
|||
if (alarm && key) { |
|||
const styleInfo = this.stylesInfo[key.def]; |
|||
const value = getAlarmValue(alarm, key); |
|||
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { |
|||
try { |
|||
style = styleInfo.cellStyleFunction(value); |
|||
} catch (e) { |
|||
style = {}; |
|||
} |
|||
} else { |
|||
style = this.defaultStyle(key, value); |
|||
} |
|||
} |
|||
if (!style.width) { |
|||
const columnWidth = this.columnWidth[key.def]; |
|||
style.width = columnWidth; |
|||
} |
|||
return style; |
|||
} |
|||
|
|||
public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml { |
|||
let strContent = ''; |
|||
if (alarm && key) { |
|||
const contentInfo = this.contentsInfo[key.def]; |
|||
const value = getAlarmValue(alarm, key); |
|||
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
|||
if (isDefined(value)) { |
|||
strContent = '' + value; |
|||
} |
|||
var content = strContent; |
|||
try { |
|||
content = contentInfo.cellContentFunction(value, alarm, this.ctx); |
|||
} catch (e) { |
|||
content = strContent; |
|||
} |
|||
} else { |
|||
content = this.defaultContent(key, value); |
|||
} |
|||
return this.domSanitizer.bypassSecurityTrustHtml(content); |
|||
} else { |
|||
return strContent; |
|||
} |
|||
} |
|||
|
|||
public onRowClick($event: Event, alarm: AlarmInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.alarmsDatasource.toggleCurrentAlarm(alarm); |
|||
const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick'); |
|||
if (descriptors.length) { |
|||
let entityId; |
|||
let entityName; |
|||
if (alarm && alarm.originator) { |
|||
entityId = alarm.originator; |
|||
entityName = alarm.originatorName; |
|||
} |
|||
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm}); |
|||
} |
|||
} |
|||
|
|||
public onActionButtonClick($event: Event, alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor) { |
|||
if (actionDescriptor.details) { |
|||
this.openAlarmDetails($event, alarm); |
|||
} else if (actionDescriptor.acknowledge) { |
|||
this.ackAlarm($event, alarm); |
|||
} else if (actionDescriptor.clear) { |
|||
this.clearAlarm($event, alarm); |
|||
} else { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
let entityId; |
|||
let entityName; |
|||
if (alarm && alarm.originator) { |
|||
entityId = alarm.originator; |
|||
entityName = alarm.originatorName; |
|||
} |
|||
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm}); |
|||
} |
|||
} |
|||
|
|||
public actionEnabled(alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor): boolean { |
|||
if (actionDescriptor.acknowledge) { |
|||
return (alarm.status === AlarmStatus.ACTIVE_UNACK || |
|||
alarm.status === AlarmStatus.CLEARED_UNACK); |
|||
} else if (actionDescriptor.clear) { |
|||
return (alarm.status === AlarmStatus.ACTIVE_ACK || |
|||
alarm.status === AlarmStatus.ACTIVE_UNACK); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
private openAlarmDetails($event: Event, alarm: AlarmInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
// TODO:
|
|||
} |
|||
|
|||
private ackAlarm($event: Event, alarm: AlarmInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
// TODO:
|
|||
} |
|||
|
|||
public ackAlarms($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
// TODO:
|
|||
} |
|||
|
|||
private clearAlarm($event: Event, alarm: AlarmInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
// TODO:
|
|||
} |
|||
|
|||
public clearAlarms($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
// TODO:
|
|||
} |
|||
|
|||
private defaultContent(key: EntityColumn, value: any): any { |
|||
if (isDefined(value)) { |
|||
const alarmField = alarmFields[key.name]; |
|||
if (alarmField) { |
|||
if (alarmField.time) { |
|||
return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss'); |
|||
} else if (alarmField.value === alarmFields.severity.value) { |
|||
return this.translate.instant(alarmSeverityTranslations.get(value)); |
|||
} else if (alarmField.value === alarmFields.status.value) { |
|||
return this.translate.instant(alarmStatusTranslations.get(value)); |
|||
} else if (alarmField.value === alarmFields.originatorType.value) { |
|||
return this.translate.instant(entityTypeTranslations.get(value).type); |
|||
} |
|||
else { |
|||
return value; |
|||
} |
|||
} else { |
|||
return value; |
|||
} |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
private defaultStyle(key: EntityColumn, value: any): any { |
|||
if (isDefined(value)) { |
|||
const alarmField = alarmFields[key.name]; |
|||
if (alarmField) { |
|||
if (alarmField.value == alarmFields.severity.value) { |
|||
return { |
|||
fontWeight: 'bold', |
|||
color: alarmSeverityColors.get(value) |
|||
}; |
|||
} else { |
|||
return {}; |
|||
} |
|||
} else { |
|||
return {}; |
|||
} |
|||
} else { |
|||
return {}; |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
class AlarmsDatasource implements DataSource<AlarmInfo> { |
|||
|
|||
private alarmsSubject = new BehaviorSubject<AlarmInfo[]>([]); |
|||
private pageDataSubject = new BehaviorSubject<PageData<AlarmInfo>>(emptyPageData<AlarmInfo>()); |
|||
|
|||
public selection = new SelectionModel<AlarmInfo>(true, [], false); |
|||
|
|||
private selectionModeChanged = new EventEmitter<boolean>(); |
|||
|
|||
public selectionModeChanged$ = this.selectionModeChanged.asObservable(); |
|||
|
|||
private allAlarms: Array<AlarmInfo> = []; |
|||
private allAlarmsSubject = new BehaviorSubject<AlarmInfo[]>([]); |
|||
private allAlarms$: Observable<Array<AlarmInfo>> = this.allAlarmsSubject.asObservable(); |
|||
|
|||
private currentAlarm: AlarmInfo = null; |
|||
|
|||
constructor() { |
|||
} |
|||
|
|||
connect(collectionViewer: CollectionViewer): Observable<AlarmInfo[] | ReadonlyArray<AlarmInfo>> { |
|||
return this.alarmsSubject.asObservable(); |
|||
} |
|||
|
|||
disconnect(collectionViewer: CollectionViewer): void { |
|||
this.alarmsSubject.complete(); |
|||
this.pageDataSubject.complete(); |
|||
} |
|||
|
|||
loadAlarms(pageLink: PageLink) { |
|||
if (this.selection.hasValue()) { |
|||
this.selection.clear(); |
|||
this.onSelectionModeChanged(false); |
|||
} |
|||
this.fetchAlarms(pageLink).pipe( |
|||
catchError(() => of(emptyPageData<AlarmInfo>())), |
|||
).subscribe( |
|||
(pageData) => { |
|||
this.alarmsSubject.next(pageData.data); |
|||
this.pageDataSubject.next(pageData); |
|||
} |
|||
); |
|||
} |
|||
|
|||
updateAlarms(alarms: AlarmInfo[]) { |
|||
alarms.forEach((newAlarm) => { |
|||
const existingAlarmIndex = this.allAlarms.findIndex(alarm => alarm.id.id === newAlarm.id.id); |
|||
if (existingAlarmIndex > -1) { |
|||
Object.assign(this.allAlarms[existingAlarmIndex], newAlarm); |
|||
} else { |
|||
this.allAlarms.push(newAlarm); |
|||
} |
|||
}); |
|||
for (let i = this.allAlarms.length - 1; i >= 0; i--) { |
|||
const oldAlarm = this.allAlarms[i]; |
|||
const newAlarmIndex = alarms.findIndex(alarm => alarm.id.id === oldAlarm.id.id); |
|||
if (newAlarmIndex === -1) { |
|||
this.allAlarms.splice(i, 1); |
|||
} |
|||
} |
|||
if (this.selection.hasValue()) { |
|||
const toRemove: AlarmInfo[] = []; |
|||
this.selection.selected.forEach((selectedAlarm) => { |
|||
const existingAlarm = this.allAlarms.find(alarm => alarm.id.id === selectedAlarm.id.id); |
|||
if (!existingAlarm) { |
|||
toRemove.push(selectedAlarm); |
|||
} |
|||
}); |
|||
this.selection.deselect(...toRemove); |
|||
if (this.selection.isEmpty()) { |
|||
this.onSelectionModeChanged(false); |
|||
} |
|||
} |
|||
this.allAlarmsSubject.next(this.allAlarms); |
|||
} |
|||
|
|||
isAllSelected(): Observable<boolean> { |
|||
const numSelected = this.selection.selected.length; |
|||
return this.alarmsSubject.pipe( |
|||
map((alarms) => numSelected === alarms.length) |
|||
); |
|||
} |
|||
|
|||
isEmpty(): Observable<boolean> { |
|||
return this.alarmsSubject.pipe( |
|||
map((alarms) => !alarms.length) |
|||
); |
|||
} |
|||
|
|||
total(): Observable<number> { |
|||
return this.pageDataSubject.pipe( |
|||
map((pageData) => pageData.totalElements) |
|||
); |
|||
} |
|||
|
|||
toggleSelection(alarm: AlarmInfo) { |
|||
const hasValue = this.selection.hasValue(); |
|||
this.selection.toggle(alarm); |
|||
if (hasValue !== this.selection.hasValue()) { |
|||
this.onSelectionModeChanged(this.selection.hasValue()); |
|||
} |
|||
} |
|||
|
|||
isSelected(alarm: AlarmInfo): boolean { |
|||
return this.selection.isSelected(alarm); |
|||
} |
|||
|
|||
masterToggle() { |
|||
this.alarmsSubject.pipe( |
|||
tap((alarms) => { |
|||
const numSelected = this.selection.selected.length; |
|||
if (numSelected === alarms.length) { |
|||
this.selection.clear(); |
|||
if (numSelected > 0) { |
|||
this.onSelectionModeChanged(false); |
|||
} |
|||
} else { |
|||
alarms.forEach(row => { |
|||
this.selection.select(row); |
|||
}); |
|||
if (numSelected === 0) { |
|||
this.onSelectionModeChanged(true); |
|||
} |
|||
} |
|||
}), |
|||
take(1) |
|||
).subscribe(); |
|||
} |
|||
|
|||
public toggleCurrentAlarm(alarm: AlarmInfo): boolean { |
|||
if (this.currentAlarm !== alarm) { |
|||
this.currentAlarm = alarm; |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public isCurrentAlarm(alarm: AlarmInfo): boolean { |
|||
return (this.currentAlarm && alarm && this.currentAlarm.id && alarm.id) && |
|||
(this.currentAlarm.id.id === alarm.id.id); |
|||
} |
|||
|
|||
private onSelectionModeChanged(selectionMode: boolean) { |
|||
this.selectionModeChanged.emit(selectionMode); |
|||
} |
|||
|
|||
private fetchAlarms(pageLink: PageLink): Observable<PageData<AlarmInfo>> { |
|||
return this.allAlarms$.pipe( |
|||
map((data) => pageLink.filterData(data)) |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed 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. |
|||
*/ |
|||
:host { |
|||
.tb-table-widget { |
|||
.mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar:not([color="primary"]) { |
|||
background: transparent; |
|||
} |
|||
mat-toolbar { |
|||
height: 39px; |
|||
max-height: 39px; |
|||
.mat-toolbar-tools { |
|||
height: 39px; |
|||
max-height: 39px; |
|||
} |
|||
} |
|||
.table-container { |
|||
overflow: auto; |
|||
} |
|||
|
|||
.mat-row:not(.mat-row-select), .mat-header-row:not(.mat-row-select) { |
|||
mat-cell:nth-child(n+2):nth-last-child(n+2), mat-footer-cell:nth-child(n+2):nth-last-child(n+2), mat-header-cell:nth-child(n+2):nth-last-child(n+2) { |
|||
padding: 0px 5px; |
|||
} |
|||
} |
|||
|
|||
.mat-row.mat-row-select, .mat-header-row.mat-row-select { |
|||
mat-cell:nth-child(2), mat-footer-cell:nth-child(2), mat-header-cell:nth-child(2) { |
|||
padding: 0px 5px; |
|||
} |
|||
mat-cell:nth-child(n+3):nth-last-child(n+2), mat-footer-cell:nth-child(n+3):nth-last-child(n+2), mat-header-cell:nth-child(n+3):nth-last-child(n+2) { |
|||
padding: 0px 5px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host-context(.tb-has-timewindow) { |
|||
.tb-table-widget { |
|||
mat-toolbar { |
|||
height: 65px; |
|||
max-height: 65px; |
|||
.mat-toolbar-tools { |
|||
height: 65px; |
|||
max-height: 65px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue