Commit d47371d8fd253a4459aae231b417070f656e961a
1 parent
bd8af111
Implemented Timeseries table widget.
Showing
12 changed files
with
765 additions
and
41 deletions
@@ -109,12 +109,12 @@ | @@ -109,12 +109,12 @@ | ||
109 | "sizeX": 8, | 109 | "sizeX": 8, |
110 | "sizeY": 6.5, | 110 | "sizeY": 6.5, |
111 | "resources": [], | 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 | "templateCss": "", | 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,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 | div.tb-widget { | 61 | div.tb-widget { |
52 | position: relative; | 62 | position: relative; |
53 | height: 100%; | 63 | height: 100%; |
@@ -135,7 +135,7 @@ | @@ -135,7 +135,7 @@ | ||
135 | </div> | 135 | </div> |
136 | </mat-toolbar> | 136 | </mat-toolbar> |
137 | <div fxFlex class="table-container"> | 137 | <div fxFlex class="table-container"> |
138 | - <mat-table [dataSource]="dataSource" | 138 | + <mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId" |
139 | matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> | 139 | matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
140 | <ng-container matColumnDef="select" sticky> | 140 | <ng-container matColumnDef="select" sticky> |
141 | <mat-header-cell *matHeaderCellDef> | 141 | <mat-header-cell *matHeaderCellDef> |
@@ -16,13 +16,13 @@ | @@ -16,13 +16,13 @@ | ||
16 | 16 | ||
17 | import { | 17 | import { |
18 | AfterViewInit, | 18 | AfterViewInit, |
19 | - Component, ComponentFactoryResolver, | 19 | + ChangeDetectionStrategy, |
20 | + Component, | ||
21 | + ComponentFactoryResolver, | ||
20 | ElementRef, | 22 | ElementRef, |
21 | Input, | 23 | Input, |
22 | OnInit, | 24 | OnInit, |
23 | - Type, | ||
24 | - ViewChild, | ||
25 | - ChangeDetectionStrategy | 25 | + ViewChild |
26 | } from '@angular/core'; | 26 | } from '@angular/core'; |
27 | import { PageComponent } from '@shared/components/page.component'; | 27 | import { PageComponent } from '@shared/components/page.component'; |
28 | import { Store } from '@ngrx/store'; | 28 | import { Store } from '@ngrx/store'; |
@@ -35,28 +35,24 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; | @@ -35,28 +35,24 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; | ||
35 | import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; | 35 | import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; |
36 | import { TranslateService } from '@ngx-translate/core'; | 36 | import { TranslateService } from '@ngx-translate/core'; |
37 | import { BaseData, HasId } from '@shared/models/base-data'; | 37 | import { BaseData, HasId } from '@shared/models/base-data'; |
38 | -import { EntityId } from '@shared/models/id/entity-id'; | ||
39 | import { ActivatedRoute } from '@angular/router'; | 38 | import { ActivatedRoute } from '@angular/router'; |
40 | import { | 39 | import { |
41 | CellActionDescriptor, | 40 | CellActionDescriptor, |
41 | + EntityActionTableColumn, | ||
42 | + EntityColumn, | ||
42 | EntityTableColumn, | 43 | EntityTableColumn, |
43 | EntityTableConfig, | 44 | EntityTableConfig, |
44 | GroupActionDescriptor, | 45 | GroupActionDescriptor, |
45 | - HeaderActionDescriptor, | ||
46 | - EntityColumn, EntityActionTableColumn | 46 | + HeaderActionDescriptor |
47 | } from '@home/models/entity/entities-table-config.models'; | 47 | } from '@home/models/entity/entities-table-config.models'; |
48 | import { EntityTypeTranslation } from '@shared/models/entity-type.models'; | 48 | import { EntityTypeTranslation } from '@shared/models/entity-type.models'; |
49 | import { DialogService } from '@core/services/dialog.service'; | 49 | import { DialogService } from '@core/services/dialog.service'; |
50 | import { AddEntityDialogComponent } from './add-entity-dialog.component'; | 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 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | 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 | @Component({ | 57 | @Component({ |
62 | selector: 'tb-entities-table', | 58 | selector: 'tb-entities-table', |
@@ -458,4 +454,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | @@ -458,4 +454,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | ||
458 | return column.key; | 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,26 +507,22 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, | ||
507 | } | 507 | } |
508 | 508 | ||
509 | public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml { | 509 | public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml { |
510 | - let strContent = ''; | ||
511 | if (alarm && key) { | 510 | if (alarm && key) { |
512 | const contentInfo = this.contentsInfo[key.def]; | 511 | const contentInfo = this.contentsInfo[key.def]; |
513 | const value = getAlarmValue(alarm, key); | 512 | const value = getAlarmValue(alarm, key); |
513 | + let content = ''; | ||
514 | if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { | 514 | if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
515 | - if (isDefined(value)) { | ||
516 | - strContent = '' + value; | ||
517 | - } | ||
518 | - var content = strContent; | ||
519 | try { | 515 | try { |
520 | content = contentInfo.cellContentFunction(value, alarm, this.ctx); | 516 | content = contentInfo.cellContentFunction(value, alarm, this.ctx); |
521 | } catch (e) { | 517 | } catch (e) { |
522 | - content = strContent; | 518 | + content = '' + value; |
523 | } | 519 | } |
524 | } else { | 520 | } else { |
525 | content = this.defaultContent(key, value); | 521 | content = this.defaultContent(key, value); |
526 | } | 522 | } |
527 | - return this.domSanitizer.bypassSecurityTrustHtml(content); | 523 | + return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; |
528 | } else { | 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,28 +433,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni | ||
433 | } | 433 | } |
434 | 434 | ||
435 | public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { | 435 | public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { |
436 | - let strContent = ''; | ||
437 | if (entity && key) { | 436 | if (entity && key) { |
438 | const contentInfo = this.contentsInfo[key.def]; | 437 | const contentInfo = this.contentsInfo[key.def]; |
439 | const value = getEntityValue(entity, key); | 438 | const value = getEntityValue(entity, key); |
439 | + let content = ''; | ||
440 | if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { | 440 | if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
441 | - if (isDefined(value)) { | ||
442 | - strContent = '' + value; | ||
443 | - } | ||
444 | - var content = strContent; | ||
445 | try { | 441 | try { |
446 | content = contentInfo.cellContentFunction(value, entity, this.ctx); | 442 | content = contentInfo.cellContentFunction(value, entity, this.ctx); |
447 | } catch (e) { | 443 | } catch (e) { |
448 | - content = strContent; | 444 | + content = '' + value; |
449 | } | 445 | } |
450 | } else { | 446 | } else { |
451 | const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; | 447 | const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; |
452 | const units = contentInfo.units || this.ctx.widgetConfig.units; | 448 | const units = contentInfo.units || this.ctx.widgetConfig.units; |
453 | content = this.ctx.utils.formatValue(value, decimals, units, true); | 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 | } else { | 452 | } else { |
457 | - return strContent; | 453 | + return ''; |
458 | } | 454 | } |
459 | } | 455 | } |
460 | 456 |
@@ -31,7 +31,7 @@ export interface TableWidgetSettings { | @@ -31,7 +31,7 @@ export interface TableWidgetSettings { | ||
31 | } | 31 | } |
32 | 32 | ||
33 | export interface TableWidgetDataKeySettings { | 33 | export interface TableWidgetDataKeySettings { |
34 | - columnWidth: string; | 34 | + columnWidth?: string; |
35 | useCellStyleFunction: boolean; | 35 | useCellStyleFunction: boolean; |
36 | cellStyleFunction: string; | 36 | cellStyleFunction: string; |
37 | useCellContentFunction: boolean; | 37 | useCellContentFunction: boolean; |
@@ -168,6 +168,7 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { | @@ -168,6 +168,7 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { | ||
168 | const mdDark = defaultColor.setAlpha(0.87).toRgbString(); | 168 | const mdDark = defaultColor.setAlpha(0.87).toRgbString(); |
169 | const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); | 169 | const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); |
170 | const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); | 170 | const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); |
171 | + const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString(); | ||
171 | const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); | 172 | const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); |
172 | 173 | ||
173 | const cssString = | 174 | const cssString = |
@@ -189,6 +190,15 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { | @@ -189,6 +190,15 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { | ||
189 | 'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n'+ | 190 | 'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n'+ |
190 | 'color: ' + mdDarkSecondary + ';\n'+ | 191 | 'color: ' + mdDarkSecondary + ';\n'+ |
191 | '}\n'+ | 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 | '.mat-table .mat-header-row {\n'+ | 202 | '.mat-table .mat-header-row {\n'+ |
193 | 'background-color: ' + origBackgroundColor + ';\n'+ | 203 | 'background-color: ' + origBackgroundColor + ';\n'+ |
194 | '}\n'+ | 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,6 +22,7 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa | ||
22 | import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component'; | 22 | import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component'; |
23 | import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component'; | 23 | import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component'; |
24 | import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; | 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 | @NgModule({ | 27 | @NgModule({ |
27 | entryComponents: [ | 28 | entryComponents: [ |
@@ -33,7 +34,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone | @@ -33,7 +34,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone | ||
33 | DisplayColumnsPanelComponent, | 34 | DisplayColumnsPanelComponent, |
34 | AlarmStatusFilterPanelComponent, | 35 | AlarmStatusFilterPanelComponent, |
35 | EntitiesTableWidgetComponent, | 36 | EntitiesTableWidgetComponent, |
36 | - AlarmsTableWidgetComponent | 37 | + AlarmsTableWidgetComponent, |
38 | + TimeseriesTableWidgetComponent | ||
37 | ], | 39 | ], |
38 | imports: [ | 40 | imports: [ |
39 | CommonModule, | 41 | CommonModule, |
@@ -42,7 +44,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone | @@ -42,7 +44,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone | ||
42 | ], | 44 | ], |
43 | exports: [ | 45 | exports: [ |
44 | EntitiesTableWidgetComponent, | 46 | EntitiesTableWidgetComponent, |
45 | - AlarmsTableWidgetComponent | 47 | + AlarmsTableWidgetComponent, |
48 | + TimeseriesTableWidgetComponent | ||
46 | ] | 49 | ] |
47 | }) | 50 | }) |
48 | export class WidgetComponentsModule { } | 51 | export class WidgetComponentsModule { } |
@@ -857,7 +857,7 @@ mat-label { | @@ -857,7 +857,7 @@ mat-label { | ||
857 | span.no-data-found { | 857 | span.no-data-found { |
858 | position: relative; | 858 | position: relative; |
859 | display: flex; | 859 | display: flex; |
860 | - height: calc(100% - 57px); | 860 | + height: calc(100% - 60px); |
861 | text-transform: uppercase; | 861 | text-transform: uppercase; |
862 | } | 862 | } |
863 | 863 |