Commit d47371d8fd253a4459aae231b417070f656e961a
1 parent
bd8af111
Implemented Timeseries table widget.
Showing
12 changed files
with
765 additions
and
41 deletions
... | ... | @@ -109,12 +109,12 @@ |
109 | 109 | "sizeX": 8, |
110 | 110 | "sizeY": 6.5, |
111 | 111 | "resources": [], |
112 | - "templateHtml": "<tb-timeseries-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-timeseries-table-widget>", | |
112 | + "templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>", | |
113 | 113 | "templateCss": "", |
114 | - "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}", | |
115 | - "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}", | |
116 | - "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}", | |
117 | - "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\":{}}" | |
114 | + "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}", | |
115 | + "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}", | |
116 | + "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}", | |
117 | + "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\"}" | |
118 | 118 | } |
119 | 119 | }, |
120 | 120 | { | ... | ... |
... | ... | @@ -48,6 +48,16 @@ |
48 | 48 | } |
49 | 49 | } |
50 | 50 | |
51 | +tb-widget.tb-widget { | |
52 | + position: relative; | |
53 | + height: 100%; | |
54 | + margin: 0; | |
55 | + overflow: hidden; | |
56 | + outline: none; | |
57 | + | |
58 | + transition: all .2s ease-in-out; | |
59 | +} | |
60 | + | |
51 | 61 | div.tb-widget { |
52 | 62 | position: relative; |
53 | 63 | height: 100%; | ... | ... |
... | ... | @@ -135,7 +135,7 @@ |
135 | 135 | </div> |
136 | 136 | </mat-toolbar> |
137 | 137 | <div fxFlex class="table-container"> |
138 | - <mat-table [dataSource]="dataSource" | |
138 | + <mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId" | |
139 | 139 | matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
140 | 140 | <ng-container matColumnDef="select" sticky> |
141 | 141 | <mat-header-cell *matHeaderCellDef> | ... | ... |
... | ... | @@ -16,13 +16,13 @@ |
16 | 16 | |
17 | 17 | import { |
18 | 18 | AfterViewInit, |
19 | - Component, ComponentFactoryResolver, | |
19 | + ChangeDetectionStrategy, | |
20 | + Component, | |
21 | + ComponentFactoryResolver, | |
20 | 22 | ElementRef, |
21 | 23 | Input, |
22 | 24 | OnInit, |
23 | - Type, | |
24 | - ViewChild, | |
25 | - ChangeDetectionStrategy | |
25 | + ViewChild | |
26 | 26 | } from '@angular/core'; |
27 | 27 | import { PageComponent } from '@shared/components/page.component'; |
28 | 28 | import { Store } from '@ngrx/store'; |
... | ... | @@ -35,28 +35,24 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
35 | 35 | import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; |
36 | 36 | import { TranslateService } from '@ngx-translate/core'; |
37 | 37 | import { BaseData, HasId } from '@shared/models/base-data'; |
38 | -import { EntityId } from '@shared/models/id/entity-id'; | |
39 | 38 | import { ActivatedRoute } from '@angular/router'; |
40 | 39 | import { |
41 | 40 | CellActionDescriptor, |
41 | + EntityActionTableColumn, | |
42 | + EntityColumn, | |
42 | 43 | EntityTableColumn, |
43 | 44 | EntityTableConfig, |
44 | 45 | GroupActionDescriptor, |
45 | - HeaderActionDescriptor, | |
46 | - EntityColumn, EntityActionTableColumn | |
46 | + HeaderActionDescriptor | |
47 | 47 | } from '@home/models/entity/entities-table-config.models'; |
48 | 48 | import { EntityTypeTranslation } from '@shared/models/entity-type.models'; |
49 | 49 | import { DialogService } from '@core/services/dialog.service'; |
50 | 50 | import { AddEntityDialogComponent } from './add-entity-dialog.component'; |
51 | -import { | |
52 | - AddEntityDialogData, | |
53 | - EntityAction | |
54 | -} from '@home/models/entity/entity-component.models'; | |
55 | -import { Timewindow, historyInterval } from '@shared/models/time/time.models'; | |
56 | -import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |
51 | +import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; | |
52 | +import { historyInterval, Timewindow } from '@shared/models/time/time.models'; | |
53 | +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; | |
57 | 54 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
58 | -import { instanceOf } from 'prop-types'; | |
59 | -import { isDefined, isDefinedAndNotNull, isUndefined } from '@core/utils'; | |
55 | +import { isDefined, isUndefined } from '@core/utils'; | |
60 | 56 | |
61 | 57 | @Component({ |
62 | 58 | selector: 'tb-entities-table', |
... | ... | @@ -458,4 +454,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
458 | 454 | return column.key; |
459 | 455 | } |
460 | 456 | |
457 | + trackByEntityId(index: number, entity: BaseData<HasId>) { | |
458 | + return entity.id.id; | |
459 | + } | |
460 | + | |
461 | 461 | } | ... | ... |
... | ... | @@ -507,26 +507,22 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, |
507 | 507 | } |
508 | 508 | |
509 | 509 | public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml { |
510 | - let strContent = ''; | |
511 | 510 | if (alarm && key) { |
512 | 511 | const contentInfo = this.contentsInfo[key.def]; |
513 | 512 | const value = getAlarmValue(alarm, key); |
513 | + let content = ''; | |
514 | 514 | if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
515 | - if (isDefined(value)) { | |
516 | - strContent = '' + value; | |
517 | - } | |
518 | - var content = strContent; | |
519 | 515 | try { |
520 | 516 | content = contentInfo.cellContentFunction(value, alarm, this.ctx); |
521 | 517 | } catch (e) { |
522 | - content = strContent; | |
518 | + content = '' + value; | |
523 | 519 | } |
524 | 520 | } else { |
525 | 521 | content = this.defaultContent(key, value); |
526 | 522 | } |
527 | - return this.domSanitizer.bypassSecurityTrustHtml(content); | |
523 | + return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; | |
528 | 524 | } else { |
529 | - return strContent; | |
525 | + return ''; | |
530 | 526 | } |
531 | 527 | } |
532 | 528 | ... | ... |
... | ... | @@ -433,28 +433,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
433 | 433 | } |
434 | 434 | |
435 | 435 | public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { |
436 | - let strContent = ''; | |
437 | 436 | if (entity && key) { |
438 | 437 | const contentInfo = this.contentsInfo[key.def]; |
439 | 438 | const value = getEntityValue(entity, key); |
439 | + let content = ''; | |
440 | 440 | if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
441 | - if (isDefined(value)) { | |
442 | - strContent = '' + value; | |
443 | - } | |
444 | - var content = strContent; | |
445 | 441 | try { |
446 | 442 | content = contentInfo.cellContentFunction(value, entity, this.ctx); |
447 | 443 | } catch (e) { |
448 | - content = strContent; | |
444 | + content = '' + value; | |
449 | 445 | } |
450 | 446 | } else { |
451 | 447 | const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; |
452 | 448 | const units = contentInfo.units || this.ctx.widgetConfig.units; |
453 | 449 | content = this.ctx.utils.formatValue(value, decimals, units, true); |
454 | 450 | } |
455 | - return this.domSanitizer.bypassSecurityTrustHtml(content); | |
451 | + return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; | |
456 | 452 | } else { |
457 | - return strContent; | |
453 | + return ''; | |
458 | 454 | } |
459 | 455 | } |
460 | 456 | ... | ... |
... | ... | @@ -31,7 +31,7 @@ export interface TableWidgetSettings { |
31 | 31 | } |
32 | 32 | |
33 | 33 | export interface TableWidgetDataKeySettings { |
34 | - columnWidth: string; | |
34 | + columnWidth?: string; | |
35 | 35 | useCellStyleFunction: boolean; |
36 | 36 | cellStyleFunction: string; |
37 | 37 | useCellContentFunction: boolean; |
... | ... | @@ -168,6 +168,7 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { |
168 | 168 | const mdDark = defaultColor.setAlpha(0.87).toRgbString(); |
169 | 169 | const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); |
170 | 170 | const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); |
171 | + const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString(); | |
171 | 172 | const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); |
172 | 173 | |
173 | 174 | const cssString = |
... | ... | @@ -189,6 +190,15 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { |
189 | 190 | 'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n'+ |
190 | 191 | 'color: ' + mdDarkSecondary + ';\n'+ |
191 | 192 | '}\n'+ |
193 | + '.mat-tab-label {\n'+ | |
194 | + 'color: ' + mdDark + ';\n'+ | |
195 | + '}\n'+ | |
196 | + '.mat-tab-header-pagination-chevron {\n'+ | |
197 | + 'border-color: ' + mdDark + ';\n'+ | |
198 | + '}\n'+ | |
199 | + '.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron {\n'+ | |
200 | + 'border-color: ' + mdDarkDisabled2 + ';\n'+ | |
201 | + '}\n'+ | |
192 | 202 | '.mat-table .mat-header-row {\n'+ |
193 | 203 | 'background-color: ' + origBackgroundColor + ';\n'+ |
194 | 204 | '}\n'+ | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<div class="tb-table-widget tb-absolute-fill"> | |
19 | + <div fxFlex fxLayout="column" class="tb-absolute-fill"> | |
20 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> | |
21 | + <div class="mat-toolbar-tools"> | |
22 | + <button mat-button mat-icon-button | |
23 | + matTooltip="{{ 'action.search' | translate }}" | |
24 | + matTooltipPosition="above"> | |
25 | + <mat-icon>search</mat-icon> | |
26 | + </button> | |
27 | + <mat-form-field fxFlex> | |
28 | + <mat-label> </mat-label> | |
29 | + <input #searchInput matInput | |
30 | + [(ngModel)]="textSearch" | |
31 | + placeholder="{{ 'widget.search-data' | translate }}"/> | |
32 | + </mat-form-field> | |
33 | + <button mat-button mat-icon-button (click)="exitFilterMode()" | |
34 | + matTooltip="{{ 'action.close' | translate }}" | |
35 | + matTooltipPosition="above"> | |
36 | + <mat-icon>close</mat-icon> | |
37 | + </button> | |
38 | + </div> | |
39 | + </mat-toolbar> | |
40 | + <mat-tab-group [ngClass]="{'tb-headless': sources.length === 1}" fxFlex | |
41 | + [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> | |
42 | + <mat-tab *ngFor="let source of sources" label="{{ source.datasource.name }}"> | |
43 | + <div fxFlex class="table-container"> | |
44 | + <mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowIndex" | |
45 | + matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="(source.pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> | |
46 | + <ng-container *ngIf="showTimestamp" [matColumnDef]="'0'"> | |
47 | + <mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell> | |
48 | + <mat-cell *matCellDef="let row;" | |
49 | + [innerHTML]="cellContent(source, 0, row, row[0])" | |
50 | + [ngStyle]="cellStyle(source, 0, row[0])"> | |
51 | + </mat-cell> | |
52 | + </ng-container> | |
53 | + <ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;"> | |
54 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ h.dataKey.label }} </mat-header-cell> | |
55 | + <mat-cell *matCellDef="let row;" | |
56 | + [innerHTML]="cellContent(source, h.index, row, row[h.index])" | |
57 | + [ngStyle]="cellStyle(source, h.index, row[h.index])"> | |
58 | + </mat-cell> | |
59 | + </ng-container> | |
60 | + <ng-container matColumnDef="actions" stickyEnd> | |
61 | + <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }"> | |
62 | + </mat-header-cell> | |
63 | + <mat-cell *matCellDef="let row" [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }"> | |
64 | + <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> | |
65 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | |
66 | + *ngFor="let actionDescriptor of actionCellDescriptors" | |
67 | + matTooltip="{{ actionDescriptor.displayName }}" | |
68 | + matTooltipPosition="above" | |
69 | + (click)="onActionButtonClick($event, row, actionDescriptor)"> | |
70 | + <mat-icon>{{actionDescriptor.icon}}</mat-icon> | |
71 | + </button> | |
72 | + </div> | |
73 | + <div fxHide fxShow.lt-lg> | |
74 | + <button mat-button mat-icon-button | |
75 | + (click)="$event.stopPropagation(); ctx.detectChanges();" | |
76 | + [matMenuTriggerFor]="cellActionsMenu"> | |
77 | + <mat-icon class="material-icons">more_vert</mat-icon> | |
78 | + </button> | |
79 | + <mat-menu #cellActionsMenu="matMenu" xPosition="before"> | |
80 | + <button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors" | |
81 | + [disabled]="isLoading$ | async" | |
82 | + (click)="onActionButtonClick($event, row, actionDescriptor)"> | |
83 | + <mat-icon>{{actionDescriptor.icon}}</mat-icon> | |
84 | + <span>{{ actionDescriptor.displayName }}</span> | |
85 | + </button> | |
86 | + </mat-menu> | |
87 | + </div> | |
88 | + </mat-cell> | |
89 | + </ng-container> | |
90 | + <mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: true"></mat-header-row> | |
91 | + <mat-row *matRowDef="let row; columns: source.displayedColumns;" | |
92 | + (click)="onRowClick($event, row)"></mat-row> | |
93 | + </mat-table> | |
94 | + <span [fxShow]="source.timeseriesDatasource.isEmpty() | async" | |
95 | + fxLayoutAlign="center center" | |
96 | + class="no-data-found" translate>widget.no-data-found</span> | |
97 | + </div> | |
98 | + <mat-divider *ngIf="displayPagination"></mat-divider> | |
99 | + <mat-paginator *ngIf="displayPagination" | |
100 | + [length]="source.timeseriesDatasource.total() | async" | |
101 | + [pageIndex]="source.pageLink.page" | |
102 | + [pageSize]="source.pageLink.pageSize" | |
103 | + [pageSizeOptions]="pageSizeOptions"></mat-paginator> | |
104 | + </mat-tab> | |
105 | + </mat-tab-group> | |
106 | + </div> | |
107 | +</div> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + width: 100%; | |
18 | + height: 100%; | |
19 | + .tb-table-widget { | |
20 | + mat-footer-row, mat-row { | |
21 | + min-height: 38px; | |
22 | + } | |
23 | + mat-header-row { | |
24 | + min-height: 40px; | |
25 | + } | |
26 | + mat-toolbar { | |
27 | + z-index: 10; | |
28 | + } | |
29 | + span.no-data-found { | |
30 | + height: calc(100% - 44px); | |
31 | + } | |
32 | + } | |
33 | +} | |
34 | + | |
35 | +:host ::ng-deep { | |
36 | + .tb-table-widget { | |
37 | + .mat-tab-group { | |
38 | + height: 100%; | |
39 | + } | |
40 | + .mat-tab-body-wrapper { | |
41 | + height: 100%; | |
42 | + } | |
43 | + .mat-tab-body-content { | |
44 | + display: flex; | |
45 | + flex-direction: column; | |
46 | + } | |
47 | + } | |
48 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { | |
18 | + AfterViewInit, | |
19 | + Component, | |
20 | + ElementRef, | |
21 | + Input, | |
22 | + NgZone, | |
23 | + OnInit, | |
24 | + QueryList, | |
25 | + ViewChild, | |
26 | + ViewChildren, | |
27 | + ViewContainerRef | |
28 | +} from '@angular/core'; | |
29 | +import { PageComponent } from '@shared/components/page.component'; | |
30 | +import { Store } from '@ngrx/store'; | |
31 | +import { AppState } from '@core/core.state'; | |
32 | +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; | |
33 | +import { | |
34 | + DataKey, | |
35 | + Datasource, | |
36 | + DatasourceData, | |
37 | + DatasourceType, | |
38 | + WidgetActionDescriptor, | |
39 | + WidgetConfig | |
40 | +} from '@shared/models/widget.models'; | |
41 | +import { UtilsService } from '@core/services/utils.service'; | |
42 | +import { TranslateService } from '@ngx-translate/core'; | |
43 | +import { isDefined, isNumber } from '@core/utils'; | |
44 | +import cssjs from '@core/css/css'; | |
45 | +import { PageLink } from '@shared/models/page/page-link'; | |
46 | +import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; | |
47 | +import { DataSource } from '@angular/cdk/typings/collections'; | |
48 | +import { CollectionViewer } from '@angular/cdk/collections'; | |
49 | +import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; | |
50 | +import { emptyPageData, PageData } from '@shared/models/page/page-data'; | |
51 | +import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; | |
52 | +import { MatPaginator } from '@angular/material/paginator'; | |
53 | +import { MatSort } from '@angular/material/sort'; | |
54 | +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; | |
55 | +import { | |
56 | + CellContentInfo, | |
57 | + CellStyleInfo, | |
58 | + constructTableCssString, | |
59 | + getCellContentInfo, | |
60 | + getCellStyleInfo, | |
61 | + TableWidgetDataKeySettings | |
62 | +} from '@home/components/widget/lib/table-widget.models'; | |
63 | +import { Overlay } from '@angular/cdk/overlay'; | |
64 | +import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; | |
65 | +import { DatePipe } from '@angular/common'; | |
66 | + | |
67 | +interface TimeseriesTableWidgetSettings { | |
68 | + showTimestamp: boolean; | |
69 | + showMilliseconds: boolean; | |
70 | + displayPagination: boolean; | |
71 | + defaultPageSize: number; | |
72 | + hideEmptyLines: boolean; | |
73 | +} | |
74 | + | |
75 | +interface TimeseriesTableDataKeySettings extends TableWidgetDataKeySettings { | |
76 | +} | |
77 | + | |
78 | +interface TimeseriesRow { | |
79 | + [col: number]: any; | |
80 | + formattedTs: string; | |
81 | +} | |
82 | + | |
83 | +interface TimeseriesHeader { | |
84 | + index: number; | |
85 | + dataKey: DataKey; | |
86 | +} | |
87 | + | |
88 | +interface TimeseriesTableSource { | |
89 | + keyStartIndex: number; | |
90 | + keyEndIndex: number; | |
91 | + datasource: Datasource; | |
92 | + rawData: Array<DatasourceData>; | |
93 | + data: TimeseriesRow[]; | |
94 | + pageLink: PageLink; | |
95 | + displayedColumns: string[]; | |
96 | + timeseriesDatasource: TimeseriesDatasource; | |
97 | + header: TimeseriesHeader[], | |
98 | + stylesInfo: CellStyleInfo[], | |
99 | + contentsInfo: CellContentInfo[], | |
100 | + rowDataTemplate: {[key: string]: any} | |
101 | +} | |
102 | + | |
103 | +@Component({ | |
104 | + selector: 'tb-timeseries-table-widget', | |
105 | + templateUrl: './timeseries-table-widget.component.html', | |
106 | + styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] | |
107 | +}) | |
108 | +export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { | |
109 | + | |
110 | + @Input() | |
111 | + ctx: WidgetContext; | |
112 | + | |
113 | + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; | |
114 | + @ViewChildren(MatPaginator) paginators: QueryList<MatPaginator>; | |
115 | + @ViewChildren(MatSort) sorts: QueryList<MatSort>; | |
116 | + | |
117 | + public displayPagination = true; | |
118 | + public pageSizeOptions; | |
119 | + public textSearchMode = false; | |
120 | + public textSearch: string = null; | |
121 | + public actionCellDescriptors: WidgetActionDescriptor[]; | |
122 | + public sources: TimeseriesTableSource[]; | |
123 | + public sourceIndex: number; | |
124 | + | |
125 | + private settings: TimeseriesTableWidgetSettings; | |
126 | + private widgetConfig: WidgetConfig; | |
127 | + private data: Array<DatasourceData>; | |
128 | + private datasources: Array<Datasource>; | |
129 | + | |
130 | + private defaultPageSize = 10; | |
131 | + private defaultSortOrder = '-0'; | |
132 | + private hideEmptyLines = false; | |
133 | + private showTimestamp = true; | |
134 | + private dateFormatFilter: string; | |
135 | + | |
136 | + private searchAction: WidgetAction = { | |
137 | + name: 'action.search', | |
138 | + show: true, | |
139 | + icon: 'search', | |
140 | + onAction: () => { | |
141 | + this.enterFilterMode(); | |
142 | + } | |
143 | + }; | |
144 | + | |
145 | + constructor(protected store: Store<AppState>, | |
146 | + private elementRef: ElementRef, | |
147 | + private ngZone: NgZone, | |
148 | + private overlay: Overlay, | |
149 | + private viewContainerRef: ViewContainerRef, | |
150 | + private utils: UtilsService, | |
151 | + private translate: TranslateService, | |
152 | + private domSanitizer: DomSanitizer, | |
153 | + private datePipe: DatePipe) { | |
154 | + super(store); | |
155 | + } | |
156 | + | |
157 | + ngOnInit(): void { | |
158 | + this.ctx.$scope.timeseriesTableWidget = this; | |
159 | + this.settings = this.ctx.settings; | |
160 | + this.widgetConfig = this.ctx.widgetConfig; | |
161 | + this.data = this.ctx.data; | |
162 | + this.datasources = this.ctx.datasources; | |
163 | + this.initialize(); | |
164 | + this.ctx.updateWidgetParams(); | |
165 | + } | |
166 | + | |
167 | + ngAfterViewInit(): void { | |
168 | + fromEvent(this.searchInputField.nativeElement, 'keyup') | |
169 | + .pipe( | |
170 | + debounceTime(150), | |
171 | + distinctUntilChanged(), | |
172 | + tap(() => { | |
173 | + if (this.displayPagination) { | |
174 | + this.paginators.forEach((paginator) => { | |
175 | + paginator.pageIndex = 0; | |
176 | + }); | |
177 | + } | |
178 | + this.sources.forEach((source) => { | |
179 | + source.pageLink.textSearch = this.textSearch; | |
180 | + }); | |
181 | + this.updateAllData(); | |
182 | + }) | |
183 | + ) | |
184 | + .subscribe(); | |
185 | + | |
186 | + if (this.displayPagination) { | |
187 | + this.sorts.forEach((sort, index) => { | |
188 | + sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); | |
189 | + }); | |
190 | + } | |
191 | + this.sorts.forEach((sort, index) => { | |
192 | + const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; | |
193 | + sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); | |
194 | + (this.displayPagination ? merge(sort.sortChange, paginator.page) : sort.sortChange) | |
195 | + .pipe( | |
196 | + tap(() => this.updateData(sort, paginator, index)) | |
197 | + ) | |
198 | + .subscribe(); | |
199 | + }); | |
200 | + this.updateAllData(); | |
201 | + } | |
202 | + | |
203 | + public onDataUpdated() { | |
204 | + this.ngZone.run(() => { | |
205 | + this.sources.forEach((source) => { | |
206 | + source.timeseriesDatasource.dataUpdated(this.data); | |
207 | + }); | |
208 | + this.ctx.detectChanges(); | |
209 | + }); | |
210 | + } | |
211 | + | |
212 | + private initialize() { | |
213 | + this.ctx.widgetActions = [this.searchAction ]; | |
214 | + | |
215 | + this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton'); | |
216 | + | |
217 | + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; | |
218 | + this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false; | |
219 | + this.showTimestamp = this.settings.showTimestamp !== false; | |
220 | + this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss'; | |
221 | + | |
222 | + const pageSize = this.settings.defaultPageSize; | |
223 | + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { | |
224 | + this.defaultPageSize = pageSize; | |
225 | + } | |
226 | + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3]; | |
227 | + | |
228 | + let cssString = constructTableCssString(this.widgetConfig); | |
229 | + | |
230 | + const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; | |
231 | + cssString += '.tb-table-widget mat-toolbar.mat-table-toolbar:not([color=primary]) {\n'+ | |
232 | + 'background-color: ' + origBackgroundColor + ' !important;\n'+ | |
233 | + '}\n'; | |
234 | + | |
235 | + const cssParser = new cssjs(); | |
236 | + cssParser.testMode = false; | |
237 | + const namespace = 'ts-table-' + this.utils.hashCode(cssString); | |
238 | + cssParser.cssPreviewNamespace = namespace; | |
239 | + cssParser.createStyleElement(namespace, cssString); | |
240 | + $(this.elementRef.nativeElement).addClass(namespace); | |
241 | + this.updateDatasources(); | |
242 | + } | |
243 | + | |
244 | + private updateDatasources() { | |
245 | + this.sources = []; | |
246 | + this.sourceIndex = 0; | |
247 | + let keyOffset = 0; | |
248 | + const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; | |
249 | + if (this.datasources) { | |
250 | + for (const datasource of this.datasources) { | |
251 | + const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); | |
252 | + const source = {} as TimeseriesTableSource; | |
253 | + source.keyStartIndex = keyOffset; | |
254 | + keyOffset += datasource.dataKeys.length; | |
255 | + source.keyEndIndex = keyOffset; | |
256 | + source.datasource = datasource; | |
257 | + source.data = []; | |
258 | + source.rawData = []; | |
259 | + source.displayedColumns = []; | |
260 | + source.pageLink = new PageLink(pageSize, 0, null, sortOrder); | |
261 | + source.header = []; | |
262 | + source.stylesInfo = []; | |
263 | + source.contentsInfo = []; | |
264 | + source.rowDataTemplate = {}; | |
265 | + source.rowDataTemplate['Timestamp'] = null; | |
266 | + if (this.showTimestamp) { | |
267 | + source.displayedColumns.push('0'); | |
268 | + } | |
269 | + for (let a = 0; a < datasource.dataKeys.length; a++ ) { | |
270 | + const dataKey = datasource.dataKeys[a]; | |
271 | + const keySettings: TimeseriesTableDataKeySettings = dataKey.settings; | |
272 | + const index = a + 1; | |
273 | + source.header.push({ | |
274 | + index, | |
275 | + dataKey | |
276 | + }); | |
277 | + source.displayedColumns.push(index + ''); | |
278 | + source.rowDataTemplate[dataKey.label] = null; | |
279 | + source.stylesInfo.push(getCellStyleInfo(keySettings)); | |
280 | + const cellContentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); | |
281 | + cellContentInfo.units = dataKey.units; | |
282 | + cellContentInfo.decimals = dataKey.decimals; | |
283 | + source.contentsInfo.push(cellContentInfo); | |
284 | + } | |
285 | + source.displayedColumns.push('actions'); | |
286 | + const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe); | |
287 | + tsDatasource.dataUpdated(this.data); | |
288 | + this.sources.push(source); | |
289 | + } | |
290 | + } | |
291 | + this.updateActiveEntityInfo(); | |
292 | + } | |
293 | + | |
294 | + private updateActiveEntityInfo() { | |
295 | + const source = this.sources[this.sourceIndex]; | |
296 | + let activeEntityInfo: SubscriptionEntityInfo = null; | |
297 | + if (source) { | |
298 | + const datasource = source.datasource; | |
299 | + if (datasource.type === DatasourceType.entity && | |
300 | + datasource.entityType && datasource.entityId) { | |
301 | + activeEntityInfo = { | |
302 | + entityId: { | |
303 | + entityType: datasource.entityType, | |
304 | + id: datasource.entityId | |
305 | + }, | |
306 | + entityName: datasource.entityName | |
307 | + }; | |
308 | + } | |
309 | + } | |
310 | + this.ctx.activeEntityInfo = activeEntityInfo; | |
311 | + } | |
312 | + | |
313 | + onSourceIndexChanged() { | |
314 | + this.updateActiveEntityInfo(); | |
315 | + } | |
316 | + | |
317 | + private enterFilterMode() { | |
318 | + this.textSearchMode = true; | |
319 | + this.textSearch = ''; | |
320 | + this.sources.forEach((source) => { | |
321 | + source.pageLink.textSearch = this.textSearch; | |
322 | + }); | |
323 | + this.ctx.hideTitlePanel = true; | |
324 | + this.ctx.detectChanges(true); | |
325 | + setTimeout(() => { | |
326 | + this.searchInputField.nativeElement.focus(); | |
327 | + this.searchInputField.nativeElement.setSelectionRange(0, 0); | |
328 | + }, 10); | |
329 | + } | |
330 | + | |
331 | + exitFilterMode() { | |
332 | + this.textSearchMode = false; | |
333 | + this.textSearch = null; | |
334 | + this.sources.forEach((source, index) => { | |
335 | + source.pageLink.textSearch = this.textSearch; | |
336 | + const sort = this.sorts.toArray()[index]; | |
337 | + let paginator = null; | |
338 | + if (this.displayPagination) { | |
339 | + paginator = this.paginators.toArray()[index]; | |
340 | + paginator.pageIndex = 0; | |
341 | + } | |
342 | + this.updateData(sort, paginator, index); | |
343 | + }); | |
344 | + this.ctx.hideTitlePanel = false; | |
345 | + this.ctx.detectChanges(true); | |
346 | + } | |
347 | + | |
348 | + private updateAllData() { | |
349 | + this.sources.forEach((source, index) => { | |
350 | + const sort = this.sorts.toArray()[index]; | |
351 | + const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; | |
352 | + this.updateData(sort, paginator, index); | |
353 | + }); | |
354 | + } | |
355 | + | |
356 | + private updateData(sort: MatSort, paginator: MatPaginator, index: number) { | |
357 | + const source = this.sources[index]; | |
358 | + if (this.displayPagination) { | |
359 | + source.pageLink.page = paginator.pageIndex; | |
360 | + source.pageLink.pageSize = paginator.pageSize; | |
361 | + } else { | |
362 | + source.pageLink.page = 0; | |
363 | + } | |
364 | + source.pageLink.sortOrder.property = sort.active; | |
365 | + source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; | |
366 | + source.timeseriesDatasource.loadRows(); | |
367 | + this.ctx.detectChanges(); | |
368 | + } | |
369 | + | |
370 | + public trackByColumnIndex(index, header: TimeseriesHeader) { | |
371 | + return header.index; | |
372 | + } | |
373 | + | |
374 | + public trackByRowIndex(index: number, row: TimeseriesRow) { | |
375 | + return index; | |
376 | + } | |
377 | + | |
378 | + public cellStyle(source: TimeseriesTableSource, index: number, value: any): any { | |
379 | + let style: any = {}; | |
380 | + if (index > 0) { | |
381 | + const styleInfo = source.stylesInfo[index-1]; | |
382 | + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { | |
383 | + try { | |
384 | + style = styleInfo.cellStyleFunction(value); | |
385 | + } catch (e) { | |
386 | + style = {}; | |
387 | + } | |
388 | + } | |
389 | + } | |
390 | + return style; | |
391 | + } | |
392 | + | |
393 | + public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml { | |
394 | + if (index === 0) { | |
395 | + return row.formattedTs; | |
396 | + } else { | |
397 | + let content = ''; | |
398 | + const contentInfo = source.contentsInfo[index-1]; | |
399 | + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { | |
400 | + try { | |
401 | + const rowData = source.rowDataTemplate; | |
402 | + rowData['Timestamp'] = row[0]; | |
403 | + for (let h=0; h < source.header.length; h++) { | |
404 | + const headerInfo = source.header[h]; | |
405 | + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; | |
406 | + } | |
407 | + content = contentInfo.cellContentFunction(value, rowData, this.ctx); | |
408 | + } catch (e) { | |
409 | + content = '' + value; | |
410 | + } | |
411 | + } else { | |
412 | + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; | |
413 | + const units = contentInfo.units || this.ctx.widgetConfig.units; | |
414 | + content = this.ctx.utils.formatValue(value, decimals, units, true); | |
415 | + } | |
416 | + return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; | |
417 | + } | |
418 | + } | |
419 | + | |
420 | + public onRowClick($event: Event, row: TimeseriesRow) { | |
421 | + const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick'); | |
422 | + if (descriptors.length) { | |
423 | + if ($event) { | |
424 | + $event.stopPropagation(); | |
425 | + } | |
426 | + let entityId; | |
427 | + let entityName; | |
428 | + if (this.ctx.activeEntityInfo) { | |
429 | + entityId = this.ctx.activeEntityInfo.entityId; | |
430 | + entityName = this.ctx.activeEntityInfo.entityName; | |
431 | + } | |
432 | + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, row); | |
433 | + } | |
434 | + } | |
435 | + | |
436 | + public onActionButtonClick($event: Event, row: TimeseriesRow, actionDescriptor: WidgetActionDescriptor) { | |
437 | + if ($event) { | |
438 | + $event.stopPropagation(); | |
439 | + } | |
440 | + let entityId; | |
441 | + let entityName; | |
442 | + if (this.ctx.activeEntityInfo) { | |
443 | + entityId = this.ctx.activeEntityInfo.entityId; | |
444 | + entityName = this.ctx.activeEntityInfo.entityName; | |
445 | + } | |
446 | + this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row); | |
447 | + } | |
448 | +} | |
449 | + | |
450 | +class TimeseriesDatasource implements DataSource<TimeseriesRow> { | |
451 | + | |
452 | + private rowsSubject = new BehaviorSubject<TimeseriesRow[]>([]); | |
453 | + private pageDataSubject = new BehaviorSubject<PageData<TimeseriesRow>>(emptyPageData<TimeseriesRow>()); | |
454 | + | |
455 | + private allRowsSubject = new BehaviorSubject<TimeseriesRow[]>([]); | |
456 | + private allRows$: Observable<Array<TimeseriesRow>> = this.allRowsSubject.asObservable(); | |
457 | + | |
458 | + constructor( | |
459 | + private source: TimeseriesTableSource, | |
460 | + private hideEmptyLines: boolean, | |
461 | + private dateFormatFilter: string, | |
462 | + private datePipe: DatePipe | |
463 | + ) { | |
464 | + this.source.timeseriesDatasource = this; | |
465 | + } | |
466 | + | |
467 | + connect(collectionViewer: CollectionViewer): Observable<TimeseriesRow[] | ReadonlyArray<TimeseriesRow>> { | |
468 | + return this.rowsSubject.asObservable(); | |
469 | + } | |
470 | + | |
471 | + disconnect(collectionViewer: CollectionViewer): void { | |
472 | + this.rowsSubject.complete(); | |
473 | + this.pageDataSubject.complete(); | |
474 | + } | |
475 | + | |
476 | + loadRows() { | |
477 | + this.fetchRows(this.source.pageLink).pipe( | |
478 | + catchError(() => of(emptyPageData<TimeseriesRow>())), | |
479 | + ).subscribe( | |
480 | + (pageData) => { | |
481 | + this.rowsSubject.next(pageData.data); | |
482 | + this.pageDataSubject.next(pageData); | |
483 | + } | |
484 | + ); | |
485 | + } | |
486 | + | |
487 | + dataUpdated(data: DatasourceData[]) { | |
488 | + this.source.rawData = data.slice(this.source.keyStartIndex, this.source.keyEndIndex); | |
489 | + this.updateSourceData(); | |
490 | + } | |
491 | + | |
492 | + private updateSourceData() { | |
493 | + this.source.data = this.convertData(this.source.rawData); | |
494 | + this.allRowsSubject.next(this.source.data); | |
495 | + } | |
496 | + | |
497 | + private convertData(data: DatasourceData[]): TimeseriesRow[] { | |
498 | + const rowsMap: {[timestamp: number]: TimeseriesRow} = {}; | |
499 | + for (let d = 0; d < data.length; d++) { | |
500 | + const columnData = data[d].data; | |
501 | + for (let i = 0; i < columnData.length; i++) { | |
502 | + const cellData = columnData[i]; | |
503 | + const timestamp = cellData[0]; | |
504 | + let row = rowsMap[timestamp]; | |
505 | + if (!row) { | |
506 | + row = { | |
507 | + formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) | |
508 | + }; | |
509 | + row[0] = timestamp; | |
510 | + for (let c = 0; c < data.length; c++) { | |
511 | + row[c+1] = undefined; | |
512 | + } | |
513 | + rowsMap[timestamp] = row; | |
514 | + } | |
515 | + row[d+1] = cellData[1]; | |
516 | + } | |
517 | + } | |
518 | + const rows: TimeseriesRow[] = []; | |
519 | + for (const t of Object.keys(rowsMap)) { | |
520 | + if (this.hideEmptyLines) { | |
521 | + let hideLine = true; | |
522 | + for (let _c = 0; (_c < data.length) && hideLine; _c++) { | |
523 | + if (rowsMap[t][_c+1]) | |
524 | + hideLine = false; | |
525 | + } | |
526 | + if (!hideLine) { | |
527 | + rows.push(rowsMap[t]); | |
528 | + } | |
529 | + } else { | |
530 | + rows.push(rowsMap[t]); | |
531 | + } | |
532 | + } | |
533 | + return rows; | |
534 | + } | |
535 | + | |
536 | + | |
537 | + isEmpty(): Observable<boolean> { | |
538 | + return this.rowsSubject.pipe( | |
539 | + map((rows) => !rows.length) | |
540 | + ); | |
541 | + } | |
542 | + | |
543 | + total(): Observable<number> { | |
544 | + return this.pageDataSubject.pipe( | |
545 | + map((pageData) => pageData.totalElements) | |
546 | + ); | |
547 | + } | |
548 | + | |
549 | + private fetchRows(pageLink: PageLink): Observable<PageData<TimeseriesRow>> { | |
550 | + return this.allRows$.pipe( | |
551 | + map((data) => pageLink.filterData(data)) | |
552 | + ); | |
553 | + } | |
554 | +} | ... | ... |
... | ... | @@ -22,6 +22,7 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa |
22 | 22 | import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component'; |
23 | 23 | import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component'; |
24 | 24 | import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; |
25 | +import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; | |
25 | 26 | |
26 | 27 | @NgModule({ |
27 | 28 | entryComponents: [ |
... | ... | @@ -33,7 +34,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone |
33 | 34 | DisplayColumnsPanelComponent, |
34 | 35 | AlarmStatusFilterPanelComponent, |
35 | 36 | EntitiesTableWidgetComponent, |
36 | - AlarmsTableWidgetComponent | |
37 | + AlarmsTableWidgetComponent, | |
38 | + TimeseriesTableWidgetComponent | |
37 | 39 | ], |
38 | 40 | imports: [ |
39 | 41 | CommonModule, |
... | ... | @@ -42,7 +44,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone |
42 | 44 | ], |
43 | 45 | exports: [ |
44 | 46 | EntitiesTableWidgetComponent, |
45 | - AlarmsTableWidgetComponent | |
47 | + AlarmsTableWidgetComponent, | |
48 | + TimeseriesTableWidgetComponent | |
46 | 49 | ] |
47 | 50 | }) |
48 | 51 | export class WidgetComponentsModule { } | ... | ... |