12 changed files with 765 additions and 41 deletions
@ -0,0 +1,107 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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"> |
||||
|
<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)]="textSearch" |
||||
|
placeholder="{{ 'widget.search-data' | 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-tab-group [ngClass]="{'tb-headless': sources.length === 1}" fxFlex |
||||
|
[(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> |
||||
|
<mat-tab *ngFor="let source of sources" label="{{ source.datasource.name }}"> |
||||
|
<div fxFlex class="table-container"> |
||||
|
<mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowIndex" |
||||
|
matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="(source.pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
||||
|
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'"> |
||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let row;" |
||||
|
[innerHTML]="cellContent(source, 0, row, row[0])" |
||||
|
[ngStyle]="cellStyle(source, 0, row[0])"> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;"> |
||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ h.dataKey.label }} </mat-header-cell> |
||||
|
<mat-cell *matCellDef="let row;" |
||||
|
[innerHTML]="cellContent(source, h.index, row, row[h.index])" |
||||
|
[ngStyle]="cellStyle(source, h.index, row[h.index])"> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="actions" stickyEnd> |
||||
|
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }"> |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let row" [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }"> |
||||
|
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
*ngFor="let actionDescriptor of actionCellDescriptors" |
||||
|
matTooltip="{{ actionDescriptor.displayName }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="onActionButtonClick($event, row, 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" |
||||
|
(click)="onActionButtonClick($event, row, actionDescriptor)"> |
||||
|
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
||||
|
<span>{{ actionDescriptor.displayName }}</span> |
||||
|
</button> |
||||
|
</mat-menu> |
||||
|
</div> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: true"></mat-header-row> |
||||
|
<mat-row *matRowDef="let row; columns: source.displayedColumns;" |
||||
|
(click)="onRowClick($event, row)"></mat-row> |
||||
|
</mat-table> |
||||
|
<span [fxShow]="source.timeseriesDatasource.isEmpty() | async" |
||||
|
fxLayoutAlign="center center" |
||||
|
class="no-data-found" translate>widget.no-data-found</span> |
||||
|
</div> |
||||
|
<mat-divider *ngIf="displayPagination"></mat-divider> |
||||
|
<mat-paginator *ngIf="displayPagination" |
||||
|
[length]="source.timeseriesDatasource.total() | async" |
||||
|
[pageIndex]="source.pageLink.page" |
||||
|
[pageSize]="source.pageLink.pageSize" |
||||
|
[pageSizeOptions]="pageSizeOptions"></mat-paginator> |
||||
|
</mat-tab> |
||||
|
</mat-tab-group> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,48 @@ |
|||||
|
/** |
||||
|
* 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%; |
||||
|
.tb-table-widget { |
||||
|
mat-footer-row, mat-row { |
||||
|
min-height: 38px; |
||||
|
} |
||||
|
mat-header-row { |
||||
|
min-height: 40px; |
||||
|
} |
||||
|
mat-toolbar { |
||||
|
z-index: 10; |
||||
|
} |
||||
|
span.no-data-found { |
||||
|
height: calc(100% - 44px); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.tb-table-widget { |
||||
|
.mat-tab-group { |
||||
|
height: 100%; |
||||
|
} |
||||
|
.mat-tab-body-wrapper { |
||||
|
height: 100%; |
||||
|
} |
||||
|
.mat-tab-body-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,554 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
Input, |
||||
|
NgZone, |
||||
|
OnInit, |
||||
|
QueryList, |
||||
|
ViewChild, |
||||
|
ViewChildren, |
||||
|
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 { |
||||
|
DataKey, |
||||
|
Datasource, |
||||
|
DatasourceData, |
||||
|
DatasourceType, |
||||
|
WidgetActionDescriptor, |
||||
|
WidgetConfig |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { 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 } from '@angular/cdk/collections'; |
||||
|
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { catchError, debounceTime, distinctUntilChanged, map, 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, |
||||
|
getCellContentInfo, |
||||
|
getCellStyleInfo, |
||||
|
TableWidgetDataKeySettings |
||||
|
} from '@home/components/widget/lib/table-widget.models'; |
||||
|
import { Overlay } from '@angular/cdk/overlay'; |
||||
|
import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
|
||||
|
interface TimeseriesTableWidgetSettings { |
||||
|
showTimestamp: boolean; |
||||
|
showMilliseconds: boolean; |
||||
|
displayPagination: boolean; |
||||
|
defaultPageSize: number; |
||||
|
hideEmptyLines: boolean; |
||||
|
} |
||||
|
|
||||
|
interface TimeseriesTableDataKeySettings extends TableWidgetDataKeySettings { |
||||
|
} |
||||
|
|
||||
|
interface TimeseriesRow { |
||||
|
[col: number]: any; |
||||
|
formattedTs: string; |
||||
|
} |
||||
|
|
||||
|
interface TimeseriesHeader { |
||||
|
index: number; |
||||
|
dataKey: DataKey; |
||||
|
} |
||||
|
|
||||
|
interface TimeseriesTableSource { |
||||
|
keyStartIndex: number; |
||||
|
keyEndIndex: number; |
||||
|
datasource: Datasource; |
||||
|
rawData: Array<DatasourceData>; |
||||
|
data: TimeseriesRow[]; |
||||
|
pageLink: PageLink; |
||||
|
displayedColumns: string[]; |
||||
|
timeseriesDatasource: TimeseriesDatasource; |
||||
|
header: TimeseriesHeader[], |
||||
|
stylesInfo: CellStyleInfo[], |
||||
|
contentsInfo: CellContentInfo[], |
||||
|
rowDataTemplate: {[key: string]: any} |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-timeseries-table-widget', |
||||
|
templateUrl: './timeseries-table-widget.component.html', |
||||
|
styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] |
||||
|
}) |
||||
|
export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef; |
||||
|
@ViewChildren(MatPaginator) paginators: QueryList<MatPaginator>; |
||||
|
@ViewChildren(MatSort) sorts: QueryList<MatSort>; |
||||
|
|
||||
|
public displayPagination = true; |
||||
|
public pageSizeOptions; |
||||
|
public textSearchMode = false; |
||||
|
public textSearch: string = null; |
||||
|
public actionCellDescriptors: WidgetActionDescriptor[]; |
||||
|
public sources: TimeseriesTableSource[]; |
||||
|
public sourceIndex: number; |
||||
|
|
||||
|
private settings: TimeseriesTableWidgetSettings; |
||||
|
private widgetConfig: WidgetConfig; |
||||
|
private data: Array<DatasourceData>; |
||||
|
private datasources: Array<Datasource>; |
||||
|
|
||||
|
private defaultPageSize = 10; |
||||
|
private defaultSortOrder = '-0'; |
||||
|
private hideEmptyLines = false; |
||||
|
private showTimestamp = true; |
||||
|
private dateFormatFilter: string; |
||||
|
|
||||
|
private searchAction: WidgetAction = { |
||||
|
name: 'action.search', |
||||
|
show: true, |
||||
|
icon: 'search', |
||||
|
onAction: () => { |
||||
|
this.enterFilterMode(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private elementRef: ElementRef, |
||||
|
private ngZone: NgZone, |
||||
|
private overlay: Overlay, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private utils: UtilsService, |
||||
|
private translate: TranslateService, |
||||
|
private domSanitizer: DomSanitizer, |
||||
|
private datePipe: DatePipe) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.timeseriesTableWidget = this; |
||||
|
this.settings = this.ctx.settings; |
||||
|
this.widgetConfig = this.ctx.widgetConfig; |
||||
|
this.data = this.ctx.data; |
||||
|
this.datasources = this.ctx.datasources; |
||||
|
this.initialize(); |
||||
|
this.ctx.updateWidgetParams(); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
fromEvent(this.searchInputField.nativeElement, 'keyup') |
||||
|
.pipe( |
||||
|
debounceTime(150), |
||||
|
distinctUntilChanged(), |
||||
|
tap(() => { |
||||
|
if (this.displayPagination) { |
||||
|
this.paginators.forEach((paginator) => { |
||||
|
paginator.pageIndex = 0; |
||||
|
}); |
||||
|
} |
||||
|
this.sources.forEach((source) => { |
||||
|
source.pageLink.textSearch = this.textSearch; |
||||
|
}); |
||||
|
this.updateAllData(); |
||||
|
}) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
|
||||
|
if (this.displayPagination) { |
||||
|
this.sorts.forEach((sort, index) => { |
||||
|
sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); |
||||
|
}); |
||||
|
} |
||||
|
this.sorts.forEach((sort, index) => { |
||||
|
const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; |
||||
|
sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); |
||||
|
(this.displayPagination ? merge(sort.sortChange, paginator.page) : sort.sortChange) |
||||
|
.pipe( |
||||
|
tap(() => this.updateData(sort, paginator, index)) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
}); |
||||
|
this.updateAllData(); |
||||
|
} |
||||
|
|
||||
|
public onDataUpdated() { |
||||
|
this.ngZone.run(() => { |
||||
|
this.sources.forEach((source) => { |
||||
|
source.timeseriesDatasource.dataUpdated(this.data); |
||||
|
}); |
||||
|
this.ctx.detectChanges(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private initialize() { |
||||
|
this.ctx.widgetActions = [this.searchAction ]; |
||||
|
|
||||
|
this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton'); |
||||
|
|
||||
|
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; |
||||
|
this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false; |
||||
|
this.showTimestamp = this.settings.showTimestamp !== false; |
||||
|
this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss'; |
||||
|
|
||||
|
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]; |
||||
|
|
||||
|
let cssString = constructTableCssString(this.widgetConfig); |
||||
|
|
||||
|
const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; |
||||
|
cssString += '.tb-table-widget mat-toolbar.mat-table-toolbar:not([color=primary]) {\n'+ |
||||
|
'background-color: ' + origBackgroundColor + ' !important;\n'+ |
||||
|
'}\n'; |
||||
|
|
||||
|
const cssParser = new cssjs(); |
||||
|
cssParser.testMode = false; |
||||
|
const namespace = 'ts-table-' + this.utils.hashCode(cssString); |
||||
|
cssParser.cssPreviewNamespace = namespace; |
||||
|
cssParser.createStyleElement(namespace, cssString); |
||||
|
$(this.elementRef.nativeElement).addClass(namespace); |
||||
|
this.updateDatasources(); |
||||
|
} |
||||
|
|
||||
|
private updateDatasources() { |
||||
|
this.sources = []; |
||||
|
this.sourceIndex = 0; |
||||
|
let keyOffset = 0; |
||||
|
const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; |
||||
|
if (this.datasources) { |
||||
|
for (const datasource of this.datasources) { |
||||
|
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); |
||||
|
const source = {} as TimeseriesTableSource; |
||||
|
source.keyStartIndex = keyOffset; |
||||
|
keyOffset += datasource.dataKeys.length; |
||||
|
source.keyEndIndex = keyOffset; |
||||
|
source.datasource = datasource; |
||||
|
source.data = []; |
||||
|
source.rawData = []; |
||||
|
source.displayedColumns = []; |
||||
|
source.pageLink = new PageLink(pageSize, 0, null, sortOrder); |
||||
|
source.header = []; |
||||
|
source.stylesInfo = []; |
||||
|
source.contentsInfo = []; |
||||
|
source.rowDataTemplate = {}; |
||||
|
source.rowDataTemplate['Timestamp'] = null; |
||||
|
if (this.showTimestamp) { |
||||
|
source.displayedColumns.push('0'); |
||||
|
} |
||||
|
for (let a = 0; a < datasource.dataKeys.length; a++ ) { |
||||
|
const dataKey = datasource.dataKeys[a]; |
||||
|
const keySettings: TimeseriesTableDataKeySettings = dataKey.settings; |
||||
|
const index = a + 1; |
||||
|
source.header.push({ |
||||
|
index, |
||||
|
dataKey |
||||
|
}); |
||||
|
source.displayedColumns.push(index + ''); |
||||
|
source.rowDataTemplate[dataKey.label] = null; |
||||
|
source.stylesInfo.push(getCellStyleInfo(keySettings)); |
||||
|
const cellContentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); |
||||
|
cellContentInfo.units = dataKey.units; |
||||
|
cellContentInfo.decimals = dataKey.decimals; |
||||
|
source.contentsInfo.push(cellContentInfo); |
||||
|
} |
||||
|
source.displayedColumns.push('actions'); |
||||
|
const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe); |
||||
|
tsDatasource.dataUpdated(this.data); |
||||
|
this.sources.push(source); |
||||
|
} |
||||
|
} |
||||
|
this.updateActiveEntityInfo(); |
||||
|
} |
||||
|
|
||||
|
private updateActiveEntityInfo() { |
||||
|
const source = this.sources[this.sourceIndex]; |
||||
|
let activeEntityInfo: SubscriptionEntityInfo = null; |
||||
|
if (source) { |
||||
|
const datasource = source.datasource; |
||||
|
if (datasource.type === DatasourceType.entity && |
||||
|
datasource.entityType && datasource.entityId) { |
||||
|
activeEntityInfo = { |
||||
|
entityId: { |
||||
|
entityType: datasource.entityType, |
||||
|
id: datasource.entityId |
||||
|
}, |
||||
|
entityName: datasource.entityName |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
this.ctx.activeEntityInfo = activeEntityInfo; |
||||
|
} |
||||
|
|
||||
|
onSourceIndexChanged() { |
||||
|
this.updateActiveEntityInfo(); |
||||
|
} |
||||
|
|
||||
|
private enterFilterMode() { |
||||
|
this.textSearchMode = true; |
||||
|
this.textSearch = ''; |
||||
|
this.sources.forEach((source) => { |
||||
|
source.pageLink.textSearch = this.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.textSearch = null; |
||||
|
this.sources.forEach((source, index) => { |
||||
|
source.pageLink.textSearch = this.textSearch; |
||||
|
const sort = this.sorts.toArray()[index]; |
||||
|
let paginator = null; |
||||
|
if (this.displayPagination) { |
||||
|
paginator = this.paginators.toArray()[index]; |
||||
|
paginator.pageIndex = 0; |
||||
|
} |
||||
|
this.updateData(sort, paginator, index); |
||||
|
}); |
||||
|
this.ctx.hideTitlePanel = false; |
||||
|
this.ctx.detectChanges(true); |
||||
|
} |
||||
|
|
||||
|
private updateAllData() { |
||||
|
this.sources.forEach((source, index) => { |
||||
|
const sort = this.sorts.toArray()[index]; |
||||
|
const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; |
||||
|
this.updateData(sort, paginator, index); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateData(sort: MatSort, paginator: MatPaginator, index: number) { |
||||
|
const source = this.sources[index]; |
||||
|
if (this.displayPagination) { |
||||
|
source.pageLink.page = paginator.pageIndex; |
||||
|
source.pageLink.pageSize = paginator.pageSize; |
||||
|
} else { |
||||
|
source.pageLink.page = 0; |
||||
|
} |
||||
|
source.pageLink.sortOrder.property = sort.active; |
||||
|
source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; |
||||
|
source.timeseriesDatasource.loadRows(); |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public trackByColumnIndex(index, header: TimeseriesHeader) { |
||||
|
return header.index; |
||||
|
} |
||||
|
|
||||
|
public trackByRowIndex(index: number, row: TimeseriesRow) { |
||||
|
return index; |
||||
|
} |
||||
|
|
||||
|
public cellStyle(source: TimeseriesTableSource, index: number, value: any): any { |
||||
|
let style: any = {}; |
||||
|
if (index > 0) { |
||||
|
const styleInfo = source.stylesInfo[index-1]; |
||||
|
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { |
||||
|
try { |
||||
|
style = styleInfo.cellStyleFunction(value); |
||||
|
} catch (e) { |
||||
|
style = {}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return style; |
||||
|
} |
||||
|
|
||||
|
public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml { |
||||
|
if (index === 0) { |
||||
|
return row.formattedTs; |
||||
|
} else { |
||||
|
let content = ''; |
||||
|
const contentInfo = source.contentsInfo[index-1]; |
||||
|
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
||||
|
try { |
||||
|
const rowData = source.rowDataTemplate; |
||||
|
rowData['Timestamp'] = row[0]; |
||||
|
for (let h=0; h < source.header.length; h++) { |
||||
|
const headerInfo = source.header[h]; |
||||
|
rowData[headerInfo.dataKey.name] = row[headerInfo.index]; |
||||
|
} |
||||
|
content = contentInfo.cellContentFunction(value, rowData, this.ctx); |
||||
|
} catch (e) { |
||||
|
content = '' + value; |
||||
|
} |
||||
|
} else { |
||||
|
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; |
||||
|
const units = contentInfo.units || this.ctx.widgetConfig.units; |
||||
|
content = this.ctx.utils.formatValue(value, decimals, units, true); |
||||
|
} |
||||
|
return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onRowClick($event: Event, row: TimeseriesRow) { |
||||
|
const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick'); |
||||
|
if (descriptors.length) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
let entityId; |
||||
|
let entityName; |
||||
|
if (this.ctx.activeEntityInfo) { |
||||
|
entityId = this.ctx.activeEntityInfo.entityId; |
||||
|
entityName = this.ctx.activeEntityInfo.entityName; |
||||
|
} |
||||
|
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, row); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onActionButtonClick($event: Event, row: TimeseriesRow, actionDescriptor: WidgetActionDescriptor) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
let entityId; |
||||
|
let entityName; |
||||
|
if (this.ctx.activeEntityInfo) { |
||||
|
entityId = this.ctx.activeEntityInfo.entityId; |
||||
|
entityName = this.ctx.activeEntityInfo.entityName; |
||||
|
} |
||||
|
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class TimeseriesDatasource implements DataSource<TimeseriesRow> { |
||||
|
|
||||
|
private rowsSubject = new BehaviorSubject<TimeseriesRow[]>([]); |
||||
|
private pageDataSubject = new BehaviorSubject<PageData<TimeseriesRow>>(emptyPageData<TimeseriesRow>()); |
||||
|
|
||||
|
private allRowsSubject = new BehaviorSubject<TimeseriesRow[]>([]); |
||||
|
private allRows$: Observable<Array<TimeseriesRow>> = this.allRowsSubject.asObservable(); |
||||
|
|
||||
|
constructor( |
||||
|
private source: TimeseriesTableSource, |
||||
|
private hideEmptyLines: boolean, |
||||
|
private dateFormatFilter: string, |
||||
|
private datePipe: DatePipe |
||||
|
) { |
||||
|
this.source.timeseriesDatasource = this; |
||||
|
} |
||||
|
|
||||
|
connect(collectionViewer: CollectionViewer): Observable<TimeseriesRow[] | ReadonlyArray<TimeseriesRow>> { |
||||
|
return this.rowsSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(collectionViewer: CollectionViewer): void { |
||||
|
this.rowsSubject.complete(); |
||||
|
this.pageDataSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
loadRows() { |
||||
|
this.fetchRows(this.source.pageLink).pipe( |
||||
|
catchError(() => of(emptyPageData<TimeseriesRow>())), |
||||
|
).subscribe( |
||||
|
(pageData) => { |
||||
|
this.rowsSubject.next(pageData.data); |
||||
|
this.pageDataSubject.next(pageData); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
dataUpdated(data: DatasourceData[]) { |
||||
|
this.source.rawData = data.slice(this.source.keyStartIndex, this.source.keyEndIndex); |
||||
|
this.updateSourceData(); |
||||
|
} |
||||
|
|
||||
|
private updateSourceData() { |
||||
|
this.source.data = this.convertData(this.source.rawData); |
||||
|
this.allRowsSubject.next(this.source.data); |
||||
|
} |
||||
|
|
||||
|
private convertData(data: DatasourceData[]): TimeseriesRow[] { |
||||
|
const rowsMap: {[timestamp: number]: TimeseriesRow} = {}; |
||||
|
for (let d = 0; d < data.length; d++) { |
||||
|
const columnData = data[d].data; |
||||
|
for (let i = 0; i < columnData.length; i++) { |
||||
|
const cellData = columnData[i]; |
||||
|
const timestamp = cellData[0]; |
||||
|
let row = rowsMap[timestamp]; |
||||
|
if (!row) { |
||||
|
row = { |
||||
|
formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) |
||||
|
}; |
||||
|
row[0] = timestamp; |
||||
|
for (let c = 0; c < data.length; c++) { |
||||
|
row[c+1] = undefined; |
||||
|
} |
||||
|
rowsMap[timestamp] = row; |
||||
|
} |
||||
|
row[d+1] = cellData[1]; |
||||
|
} |
||||
|
} |
||||
|
const rows: TimeseriesRow[] = []; |
||||
|
for (const t of Object.keys(rowsMap)) { |
||||
|
if (this.hideEmptyLines) { |
||||
|
let hideLine = true; |
||||
|
for (let _c = 0; (_c < data.length) && hideLine; _c++) { |
||||
|
if (rowsMap[t][_c+1]) |
||||
|
hideLine = false; |
||||
|
} |
||||
|
if (!hideLine) { |
||||
|
rows.push(rowsMap[t]); |
||||
|
} |
||||
|
} else { |
||||
|
rows.push(rowsMap[t]); |
||||
|
} |
||||
|
} |
||||
|
return rows; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.rowsSubject.pipe( |
||||
|
map((rows) => !rows.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.pageDataSubject.pipe( |
||||
|
map((pageData) => pageData.totalElements) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private fetchRows(pageLink: PageLink): Observable<PageData<TimeseriesRow>> { |
||||
|
return this.allRows$.pipe( |
||||
|
map((data) => pageLink.filterData(data)) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue