Implemented Timeseries table widget.
This commit is contained in:
parent
bd8af1111e
commit
d47371d8fd
@ -109,12 +109,12 @@
|
||||
"sizeX": 8,
|
||||
"sizeY": 6.5,
|
||||
"resources": [],
|
||||
"templateHtml": "<tb-timeseries-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-timeseries-table-widget>",
|
||||
"templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>",
|
||||
"templateCss": "",
|
||||
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}",
|
||||
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}",
|
||||
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
|
||||
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}"
|
||||
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}",
|
||||
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}",
|
||||
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
|
||||
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -48,6 +48,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
tb-widget.tb-widget {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
|
||||
transition: all .2s ease-in-out;
|
||||
}
|
||||
|
||||
div.tb-widget {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
@ -135,7 +135,7 @@
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
<div fxFlex class="table-container">
|
||||
<mat-table [dataSource]="dataSource"
|
||||
<mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId"
|
||||
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
|
||||
<ng-container matColumnDef="select" sticky>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
|
||||
@ -16,13 +16,13 @@
|
||||
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component, ComponentFactoryResolver,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnInit,
|
||||
Type,
|
||||
ViewChild,
|
||||
ChangeDetectionStrategy
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
@ -35,28 +35,24 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order';
|
||||
import { forkJoin, fromEvent, merge, Observable } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BaseData, HasId } from '@shared/models/base-data';
|
||||
import { EntityId } from '@shared/models/id/entity-id';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
CellActionDescriptor,
|
||||
EntityActionTableColumn,
|
||||
EntityColumn,
|
||||
EntityTableColumn,
|
||||
EntityTableConfig,
|
||||
GroupActionDescriptor,
|
||||
HeaderActionDescriptor,
|
||||
EntityColumn, EntityActionTableColumn
|
||||
HeaderActionDescriptor
|
||||
} from '@home/models/entity/entities-table-config.models';
|
||||
import { EntityTypeTranslation } from '@shared/models/entity-type.models';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { AddEntityDialogComponent } from './add-entity-dialog.component';
|
||||
import {
|
||||
AddEntityDialogData,
|
||||
EntityAction
|
||||
} from '@home/models/entity/entity-component.models';
|
||||
import { Timewindow, historyInterval } from '@shared/models/time/time.models';
|
||||
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||||
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
|
||||
import { historyInterval, Timewindow } from '@shared/models/time/time.models';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
|
||||
import { instanceOf } from 'prop-types';
|
||||
import { isDefined, isDefinedAndNotNull, isUndefined } from '@core/utils';
|
||||
import { isDefined, isUndefined } from '@core/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-entities-table',
|
||||
@ -458,4 +454,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
|
||||
return column.key;
|
||||
}
|
||||
|
||||
trackByEntityId(index: number, entity: BaseData<HasId>) {
|
||||
return entity.id.id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -507,26 +507,22 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
|
||||
}
|
||||
|
||||
public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml {
|
||||
let strContent = '';
|
||||
if (alarm && key) {
|
||||
const contentInfo = this.contentsInfo[key.def];
|
||||
const value = getAlarmValue(alarm, key);
|
||||
let content = '';
|
||||
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;
|
||||
content = '' + value;
|
||||
}
|
||||
} else {
|
||||
content = this.defaultContent(key, value);
|
||||
}
|
||||
return this.domSanitizer.bypassSecurityTrustHtml(content);
|
||||
return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
|
||||
} else {
|
||||
return strContent;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -433,28 +433,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
|
||||
}
|
||||
|
||||
public cellContent(entity: EntityData, key: EntityColumn): SafeHtml {
|
||||
let strContent = '';
|
||||
if (entity && key) {
|
||||
const contentInfo = this.contentsInfo[key.def];
|
||||
const value = getEntityValue(entity, key);
|
||||
let content = '';
|
||||
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
|
||||
if (isDefined(value)) {
|
||||
strContent = '' + value;
|
||||
}
|
||||
var content = strContent;
|
||||
try {
|
||||
content = contentInfo.cellContentFunction(value, entity, this.ctx);
|
||||
} catch (e) {
|
||||
content = strContent;
|
||||
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 this.domSanitizer.bypassSecurityTrustHtml(content);
|
||||
return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
|
||||
} else {
|
||||
return strContent;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ export interface TableWidgetSettings {
|
||||
}
|
||||
|
||||
export interface TableWidgetDataKeySettings {
|
||||
columnWidth: string;
|
||||
columnWidth?: string;
|
||||
useCellStyleFunction: boolean;
|
||||
cellStyleFunction: string;
|
||||
useCellContentFunction: boolean;
|
||||
@ -168,6 +168,7 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
|
||||
const mdDark = defaultColor.setAlpha(0.87).toRgbString();
|
||||
const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
|
||||
const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
|
||||
const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString();
|
||||
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
|
||||
|
||||
const cssString =
|
||||
@ -189,6 +190,15 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
|
||||
'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n'+
|
||||
'color: ' + mdDarkSecondary + ';\n'+
|
||||
'}\n'+
|
||||
'.mat-tab-label {\n'+
|
||||
'color: ' + mdDark + ';\n'+
|
||||
'}\n'+
|
||||
'.mat-tab-header-pagination-chevron {\n'+
|
||||
'border-color: ' + mdDark + ';\n'+
|
||||
'}\n'+
|
||||
'.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron {\n'+
|
||||
'border-color: ' + mdDarkDisabled2 + ';\n'+
|
||||
'}\n'+
|
||||
'.mat-table .mat-header-row {\n'+
|
||||
'background-color: ' + origBackgroundColor + ';\n'+
|
||||
'}\n'+
|
||||
|
||||
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,7 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa
|
||||
import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component';
|
||||
import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component';
|
||||
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
|
||||
import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
|
||||
|
||||
@NgModule({
|
||||
entryComponents: [
|
||||
@ -33,7 +34,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone
|
||||
DisplayColumnsPanelComponent,
|
||||
AlarmStatusFilterPanelComponent,
|
||||
EntitiesTableWidgetComponent,
|
||||
AlarmsTableWidgetComponent
|
||||
AlarmsTableWidgetComponent,
|
||||
TimeseriesTableWidgetComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -42,7 +44,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone
|
||||
],
|
||||
exports: [
|
||||
EntitiesTableWidgetComponent,
|
||||
AlarmsTableWidgetComponent
|
||||
AlarmsTableWidgetComponent,
|
||||
TimeseriesTableWidgetComponent
|
||||
]
|
||||
})
|
||||
export class WidgetComponentsModule { }
|
||||
|
||||
@ -857,7 +857,7 @@ mat-label {
|
||||
span.no-data-found {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: calc(100% - 57px);
|
||||
height: calc(100% - 60px);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user