diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 0a6b0b0d47..bf40d495df 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -109,12 +109,12 @@
"sizeX": 8,
"sizeY": 6.5,
"resources": [],
- "templateHtml": "\n",
+ "templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}",
- "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}"
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}",
+ "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\"}"
}
},
{
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss
index 60c0f7c65e..7012daa1f9 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss
+++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss
@@ -48,6 +48,16 @@
}
}
+tb-widget.tb-widget {
+ position: relative;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ outline: none;
+
+ transition: all .2s ease-in-out;
+}
+
div.tb-widget {
position: relative;
height: 100%;
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
index 66b3ed430f..f6d0a108fc 100644
--- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
+++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
@@ -135,7 +135,7 @@
-
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
index c09ce4ac5e..b52044a601 100644
--- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
+++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
@@ -16,13 +16,13 @@
import {
AfterViewInit,
- Component, ComponentFactoryResolver,
+ ChangeDetectionStrategy,
+ Component,
+ ComponentFactoryResolver,
ElementRef,
Input,
OnInit,
- Type,
- ViewChild,
- ChangeDetectionStrategy
+ ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
@@ -35,28 +35,24 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { forkJoin, fromEvent, merge, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseData, HasId } from '@shared/models/base-data';
-import { EntityId } from '@shared/models/id/entity-id';
import { ActivatedRoute } from '@angular/router';
import {
CellActionDescriptor,
+ EntityActionTableColumn,
+ EntityColumn,
EntityTableColumn,
EntityTableConfig,
GroupActionDescriptor,
- HeaderActionDescriptor,
- EntityColumn, EntityActionTableColumn
+ HeaderActionDescriptor
} from '@home/models/entity/entities-table-config.models';
import { EntityTypeTranslation } from '@shared/models/entity-type.models';
import { DialogService } from '@core/services/dialog.service';
import { AddEntityDialogComponent } from './add-entity-dialog.component';
-import {
- AddEntityDialogData,
- EntityAction
-} from '@home/models/entity/entity-component.models';
-import { Timewindow, historyInterval } from '@shared/models/time/time.models';
-import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
+import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
+import { historyInterval, Timewindow } from '@shared/models/time/time.models';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
-import { instanceOf } from 'prop-types';
-import { isDefined, isDefinedAndNotNull, isUndefined } from '@core/utils';
+import { isDefined, isUndefined } from '@core/utils';
@Component({
selector: 'tb-entities-table',
@@ -458,4 +454,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
return column.key;
}
+ trackByEntityId(index: number, entity: BaseData) {
+ return entity.id.id;
+ }
+
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
index 7a143c80ac..d0d8b41337 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
@@ -507,26 +507,22 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml {
- let strContent = '';
if (alarm && key) {
const contentInfo = this.contentsInfo[key.def];
const value = getAlarmValue(alarm, key);
+ let content = '';
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
- if (isDefined(value)) {
- strContent = '' + value;
- }
- var content = strContent;
try {
content = contentInfo.cellContentFunction(value, alarm, this.ctx);
} catch (e) {
- content = strContent;
+ content = '' + value;
}
} else {
content = this.defaultContent(key, value);
}
- return this.domSanitizer.bypassSecurityTrustHtml(content);
+ return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
} else {
- return strContent;
+ return '';
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
index 5e427b0e1f..05ebbdccd0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
@@ -433,28 +433,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
}
public cellContent(entity: EntityData, key: EntityColumn): SafeHtml {
- let strContent = '';
if (entity && key) {
const contentInfo = this.contentsInfo[key.def];
const value = getEntityValue(entity, key);
+ let content = '';
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
- if (isDefined(value)) {
- strContent = '' + value;
- }
- var content = strContent;
try {
content = contentInfo.cellContentFunction(value, entity, this.ctx);
} catch (e) {
- content = strContent;
+ content = '' + value;
}
} else {
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
const units = contentInfo.units || this.ctx.widgetConfig.units;
content = this.ctx.utils.formatValue(value, decimals, units, true);
}
- return this.domSanitizer.bypassSecurityTrustHtml(content);
+ return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
} else {
- return strContent;
+ return '';
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
index db864fddff..f40514a572 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
@@ -31,7 +31,7 @@ export interface TableWidgetSettings {
}
export interface TableWidgetDataKeySettings {
- columnWidth: string;
+ columnWidth?: string;
useCellStyleFunction: boolean;
cellStyleFunction: string;
useCellContentFunction: boolean;
@@ -168,6 +168,7 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
const mdDark = defaultColor.setAlpha(0.87).toRgbString();
const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
+ const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString();
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
const cssString =
@@ -189,6 +190,15 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
+ '.mat-tab-label {\n'+
+ 'color: ' + mdDark + ';\n'+
+ '}\n'+
+ '.mat-tab-header-pagination-chevron {\n'+
+ 'border-color: ' + mdDark + ';\n'+
+ '}\n'+
+ '.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron {\n'+
+ 'border-color: ' + mdDarkDisabled2 + ';\n'+
+ '}\n'+
'.mat-table .mat-header-row {\n'+
'background-color: ' + origBackgroundColor + ';\n'+
'}\n'+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
new file mode 100644
index 0000000000..c6820dc351
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
@@ -0,0 +1,107 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss
new file mode 100644
index 0000000000..963ab754d6
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+:host {
+ width: 100%;
+ height: 100%;
+ .tb-table-widget {
+ mat-footer-row, mat-row {
+ min-height: 38px;
+ }
+ mat-header-row {
+ min-height: 40px;
+ }
+ mat-toolbar {
+ z-index: 10;
+ }
+ span.no-data-found {
+ height: calc(100% - 44px);
+ }
+ }
+}
+
+:host ::ng-deep {
+ .tb-table-widget {
+ .mat-tab-group {
+ height: 100%;
+ }
+ .mat-tab-body-wrapper {
+ height: 100%;
+ }
+ .mat-tab-body-content {
+ display: flex;
+ flex-direction: column;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
new file mode 100644
index 0000000000..c11a4c5f28
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
@@ -0,0 +1,554 @@
+///
+/// Copyright © 2016-2019 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ Input,
+ NgZone,
+ OnInit,
+ QueryList,
+ ViewChild,
+ ViewChildren,
+ ViewContainerRef
+} from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { WidgetAction, WidgetContext } from '@home/models/widget-component.models';
+import {
+ DataKey,
+ Datasource,
+ DatasourceData,
+ DatasourceType,
+ WidgetActionDescriptor,
+ WidgetConfig
+} from '@shared/models/widget.models';
+import { UtilsService } from '@core/services/utils.service';
+import { TranslateService } from '@ngx-translate/core';
+import { isDefined, isNumber } from '@core/utils';
+import cssjs from '@core/css/css';
+import { PageLink } from '@shared/models/page/page-link';
+import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
+import { DataSource } from '@angular/cdk/typings/collections';
+import { CollectionViewer } from '@angular/cdk/collections';
+import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
+import { emptyPageData, PageData } from '@shared/models/page/page-data';
+import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
+import { MatPaginator } from '@angular/material/paginator';
+import { MatSort } from '@angular/material/sort';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+import {
+ CellContentInfo,
+ CellStyleInfo,
+ constructTableCssString,
+ getCellContentInfo,
+ getCellStyleInfo,
+ TableWidgetDataKeySettings
+} from '@home/components/widget/lib/table-widget.models';
+import { Overlay } from '@angular/cdk/overlay';
+import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
+import { DatePipe } from '@angular/common';
+
+interface TimeseriesTableWidgetSettings {
+ showTimestamp: boolean;
+ showMilliseconds: boolean;
+ displayPagination: boolean;
+ defaultPageSize: number;
+ hideEmptyLines: boolean;
+}
+
+interface TimeseriesTableDataKeySettings extends TableWidgetDataKeySettings {
+}
+
+interface TimeseriesRow {
+ [col: number]: any;
+ formattedTs: string;
+}
+
+interface TimeseriesHeader {
+ index: number;
+ dataKey: DataKey;
+}
+
+interface TimeseriesTableSource {
+ keyStartIndex: number;
+ keyEndIndex: number;
+ datasource: Datasource;
+ rawData: Array;
+ data: TimeseriesRow[];
+ pageLink: PageLink;
+ displayedColumns: string[];
+ timeseriesDatasource: TimeseriesDatasource;
+ header: TimeseriesHeader[],
+ stylesInfo: CellStyleInfo[],
+ contentsInfo: CellContentInfo[],
+ rowDataTemplate: {[key: string]: any}
+}
+
+@Component({
+ selector: 'tb-timeseries-table-widget',
+ templateUrl: './timeseries-table-widget.component.html',
+ styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss']
+})
+export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
+
+ @Input()
+ ctx: WidgetContext;
+
+ @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
+ @ViewChildren(MatPaginator) paginators: QueryList;
+ @ViewChildren(MatSort) sorts: QueryList;
+
+ public displayPagination = true;
+ public pageSizeOptions;
+ public textSearchMode = false;
+ public textSearch: string = null;
+ public actionCellDescriptors: WidgetActionDescriptor[];
+ public sources: TimeseriesTableSource[];
+ public sourceIndex: number;
+
+ private settings: TimeseriesTableWidgetSettings;
+ private widgetConfig: WidgetConfig;
+ private data: Array;
+ private datasources: Array;
+
+ private defaultPageSize = 10;
+ private defaultSortOrder = '-0';
+ private hideEmptyLines = false;
+ private showTimestamp = true;
+ private dateFormatFilter: string;
+
+ private searchAction: WidgetAction = {
+ name: 'action.search',
+ show: true,
+ icon: 'search',
+ onAction: () => {
+ this.enterFilterMode();
+ }
+ };
+
+ constructor(protected store: Store,
+ private elementRef: ElementRef,
+ private ngZone: NgZone,
+ private overlay: Overlay,
+ private viewContainerRef: ViewContainerRef,
+ private utils: UtilsService,
+ private translate: TranslateService,
+ private domSanitizer: DomSanitizer,
+ private datePipe: DatePipe) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.ctx.$scope.timeseriesTableWidget = this;
+ this.settings = this.ctx.settings;
+ this.widgetConfig = this.ctx.widgetConfig;
+ this.data = this.ctx.data;
+ this.datasources = this.ctx.datasources;
+ this.initialize();
+ this.ctx.updateWidgetParams();
+ }
+
+ ngAfterViewInit(): void {
+ fromEvent(this.searchInputField.nativeElement, 'keyup')
+ .pipe(
+ debounceTime(150),
+ distinctUntilChanged(),
+ tap(() => {
+ if (this.displayPagination) {
+ this.paginators.forEach((paginator) => {
+ paginator.pageIndex = 0;
+ });
+ }
+ this.sources.forEach((source) => {
+ source.pageLink.textSearch = this.textSearch;
+ });
+ this.updateAllData();
+ })
+ )
+ .subscribe();
+
+ if (this.displayPagination) {
+ this.sorts.forEach((sort, index) => {
+ sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0);
+ });
+ }
+ this.sorts.forEach((sort, index) => {
+ const paginator = this.displayPagination ? this.paginators.toArray()[index] : null;
+ sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0);
+ (this.displayPagination ? merge(sort.sortChange, paginator.page) : sort.sortChange)
+ .pipe(
+ tap(() => this.updateData(sort, paginator, index))
+ )
+ .subscribe();
+ });
+ this.updateAllData();
+ }
+
+ public onDataUpdated() {
+ this.ngZone.run(() => {
+ this.sources.forEach((source) => {
+ source.timeseriesDatasource.dataUpdated(this.data);
+ });
+ this.ctx.detectChanges();
+ });
+ }
+
+ private initialize() {
+ this.ctx.widgetActions = [this.searchAction ];
+
+ this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');
+
+ this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
+ this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false;
+ this.showTimestamp = this.settings.showTimestamp !== false;
+ this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss';
+
+ const pageSize = this.settings.defaultPageSize;
+ if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
+ this.defaultPageSize = pageSize;
+ }
+ this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3];
+
+ let cssString = constructTableCssString(this.widgetConfig);
+
+ const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)';
+ cssString += '.tb-table-widget mat-toolbar.mat-table-toolbar:not([color=primary]) {\n'+
+ 'background-color: ' + origBackgroundColor + ' !important;\n'+
+ '}\n';
+
+ const cssParser = new cssjs();
+ cssParser.testMode = false;
+ const namespace = 'ts-table-' + this.utils.hashCode(cssString);
+ cssParser.cssPreviewNamespace = namespace;
+ cssParser.createStyleElement(namespace, cssString);
+ $(this.elementRef.nativeElement).addClass(namespace);
+ this.updateDatasources();
+ }
+
+ private updateDatasources() {
+ this.sources = [];
+ this.sourceIndex = 0;
+ let keyOffset = 0;
+ const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
+ if (this.datasources) {
+ for (const datasource of this.datasources) {
+ const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
+ const source = {} as TimeseriesTableSource;
+ source.keyStartIndex = keyOffset;
+ keyOffset += datasource.dataKeys.length;
+ source.keyEndIndex = keyOffset;
+ source.datasource = datasource;
+ source.data = [];
+ source.rawData = [];
+ source.displayedColumns = [];
+ source.pageLink = new PageLink(pageSize, 0, null, sortOrder);
+ source.header = [];
+ source.stylesInfo = [];
+ source.contentsInfo = [];
+ source.rowDataTemplate = {};
+ source.rowDataTemplate['Timestamp'] = null;
+ if (this.showTimestamp) {
+ source.displayedColumns.push('0');
+ }
+ for (let a = 0; a < datasource.dataKeys.length; a++ ) {
+ const dataKey = datasource.dataKeys[a];
+ const keySettings: TimeseriesTableDataKeySettings = dataKey.settings;
+ const index = a + 1;
+ source.header.push({
+ index,
+ dataKey
+ });
+ source.displayedColumns.push(index + '');
+ source.rowDataTemplate[dataKey.label] = null;
+ source.stylesInfo.push(getCellStyleInfo(keySettings));
+ const cellContentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
+ cellContentInfo.units = dataKey.units;
+ cellContentInfo.decimals = dataKey.decimals;
+ source.contentsInfo.push(cellContentInfo);
+ }
+ source.displayedColumns.push('actions');
+ const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe);
+ tsDatasource.dataUpdated(this.data);
+ this.sources.push(source);
+ }
+ }
+ this.updateActiveEntityInfo();
+ }
+
+ private updateActiveEntityInfo() {
+ const source = this.sources[this.sourceIndex];
+ let activeEntityInfo: SubscriptionEntityInfo = null;
+ if (source) {
+ const datasource = source.datasource;
+ if (datasource.type === DatasourceType.entity &&
+ datasource.entityType && datasource.entityId) {
+ activeEntityInfo = {
+ entityId: {
+ entityType: datasource.entityType,
+ id: datasource.entityId
+ },
+ entityName: datasource.entityName
+ };
+ }
+ }
+ this.ctx.activeEntityInfo = activeEntityInfo;
+ }
+
+ onSourceIndexChanged() {
+ this.updateActiveEntityInfo();
+ }
+
+ private enterFilterMode() {
+ this.textSearchMode = true;
+ this.textSearch = '';
+ this.sources.forEach((source) => {
+ source.pageLink.textSearch = this.textSearch;
+ });
+ this.ctx.hideTitlePanel = true;
+ this.ctx.detectChanges(true);
+ setTimeout(() => {
+ this.searchInputField.nativeElement.focus();
+ this.searchInputField.nativeElement.setSelectionRange(0, 0);
+ }, 10);
+ }
+
+ exitFilterMode() {
+ this.textSearchMode = false;
+ this.textSearch = null;
+ this.sources.forEach((source, index) => {
+ source.pageLink.textSearch = this.textSearch;
+ const sort = this.sorts.toArray()[index];
+ let paginator = null;
+ if (this.displayPagination) {
+ paginator = this.paginators.toArray()[index];
+ paginator.pageIndex = 0;
+ }
+ this.updateData(sort, paginator, index);
+ });
+ this.ctx.hideTitlePanel = false;
+ this.ctx.detectChanges(true);
+ }
+
+ private updateAllData() {
+ this.sources.forEach((source, index) => {
+ const sort = this.sorts.toArray()[index];
+ const paginator = this.displayPagination ? this.paginators.toArray()[index] : null;
+ this.updateData(sort, paginator, index);
+ });
+ }
+
+ private updateData(sort: MatSort, paginator: MatPaginator, index: number) {
+ const source = this.sources[index];
+ if (this.displayPagination) {
+ source.pageLink.page = paginator.pageIndex;
+ source.pageLink.pageSize = paginator.pageSize;
+ } else {
+ source.pageLink.page = 0;
+ }
+ source.pageLink.sortOrder.property = sort.active;
+ source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()];
+ source.timeseriesDatasource.loadRows();
+ this.ctx.detectChanges();
+ }
+
+ public trackByColumnIndex(index, header: TimeseriesHeader) {
+ return header.index;
+ }
+
+ public trackByRowIndex(index: number, row: TimeseriesRow) {
+ return index;
+ }
+
+ public cellStyle(source: TimeseriesTableSource, index: number, value: any): any {
+ let style: any = {};
+ if (index > 0) {
+ const styleInfo = source.stylesInfo[index-1];
+ if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
+ try {
+ style = styleInfo.cellStyleFunction(value);
+ } catch (e) {
+ style = {};
+ }
+ }
+ }
+ return style;
+ }
+
+ public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml {
+ if (index === 0) {
+ return row.formattedTs;
+ } else {
+ let content = '';
+ const contentInfo = source.contentsInfo[index-1];
+ if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
+ try {
+ const rowData = source.rowDataTemplate;
+ rowData['Timestamp'] = row[0];
+ for (let h=0; h < source.header.length; h++) {
+ const headerInfo = source.header[h];
+ rowData[headerInfo.dataKey.name] = row[headerInfo.index];
+ }
+ content = contentInfo.cellContentFunction(value, rowData, this.ctx);
+ } catch (e) {
+ content = '' + value;
+ }
+ } else {
+ const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
+ const units = contentInfo.units || this.ctx.widgetConfig.units;
+ content = this.ctx.utils.formatValue(value, decimals, units, true);
+ }
+ return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
+ }
+ }
+
+ public onRowClick($event: Event, row: TimeseriesRow) {
+ const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick');
+ if (descriptors.length) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ let entityId;
+ let entityName;
+ if (this.ctx.activeEntityInfo) {
+ entityId = this.ctx.activeEntityInfo.entityId;
+ entityName = this.ctx.activeEntityInfo.entityName;
+ }
+ this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, row);
+ }
+ }
+
+ public onActionButtonClick($event: Event, row: TimeseriesRow, actionDescriptor: WidgetActionDescriptor) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ let entityId;
+ let entityName;
+ if (this.ctx.activeEntityInfo) {
+ entityId = this.ctx.activeEntityInfo.entityId;
+ entityName = this.ctx.activeEntityInfo.entityName;
+ }
+ this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row);
+ }
+}
+
+class TimeseriesDatasource implements DataSource {
+
+ private rowsSubject = new BehaviorSubject([]);
+ private pageDataSubject = new BehaviorSubject>(emptyPageData());
+
+ private allRowsSubject = new BehaviorSubject([]);
+ private allRows$: Observable> = this.allRowsSubject.asObservable();
+
+ constructor(
+ private source: TimeseriesTableSource,
+ private hideEmptyLines: boolean,
+ private dateFormatFilter: string,
+ private datePipe: DatePipe
+ ) {
+ this.source.timeseriesDatasource = this;
+ }
+
+ connect(collectionViewer: CollectionViewer): Observable> {
+ return this.rowsSubject.asObservable();
+ }
+
+ disconnect(collectionViewer: CollectionViewer): void {
+ this.rowsSubject.complete();
+ this.pageDataSubject.complete();
+ }
+
+ loadRows() {
+ this.fetchRows(this.source.pageLink).pipe(
+ catchError(() => of(emptyPageData())),
+ ).subscribe(
+ (pageData) => {
+ this.rowsSubject.next(pageData.data);
+ this.pageDataSubject.next(pageData);
+ }
+ );
+ }
+
+ dataUpdated(data: DatasourceData[]) {
+ this.source.rawData = data.slice(this.source.keyStartIndex, this.source.keyEndIndex);
+ this.updateSourceData();
+ }
+
+ private updateSourceData() {
+ this.source.data = this.convertData(this.source.rawData);
+ this.allRowsSubject.next(this.source.data);
+ }
+
+ private convertData(data: DatasourceData[]): TimeseriesRow[] {
+ const rowsMap: {[timestamp: number]: TimeseriesRow} = {};
+ for (let d = 0; d < data.length; d++) {
+ const columnData = data[d].data;
+ for (let i = 0; i < columnData.length; i++) {
+ const cellData = columnData[i];
+ const timestamp = cellData[0];
+ let row = rowsMap[timestamp];
+ if (!row) {
+ row = {
+ formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter)
+ };
+ row[0] = timestamp;
+ for (let c = 0; c < data.length; c++) {
+ row[c+1] = undefined;
+ }
+ rowsMap[timestamp] = row;
+ }
+ row[d+1] = cellData[1];
+ }
+ }
+ const rows: TimeseriesRow[] = [];
+ for (const t of Object.keys(rowsMap)) {
+ if (this.hideEmptyLines) {
+ let hideLine = true;
+ for (let _c = 0; (_c < data.length) && hideLine; _c++) {
+ if (rowsMap[t][_c+1])
+ hideLine = false;
+ }
+ if (!hideLine) {
+ rows.push(rowsMap[t]);
+ }
+ } else {
+ rows.push(rowsMap[t]);
+ }
+ }
+ return rows;
+ }
+
+
+ isEmpty(): Observable {
+ return this.rowsSubject.pipe(
+ map((rows) => !rows.length)
+ );
+ }
+
+ total(): Observable {
+ return this.pageDataSubject.pipe(
+ map((pageData) => pageData.totalElements)
+ );
+ }
+
+ private fetchRows(pageLink: PageLink): Observable> {
+ return this.allRows$.pipe(
+ map((data) => pageLink.filterData(data))
+ );
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
index b006b317a6..369d2546b0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
@@ -22,6 +22,7 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa
import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component';
import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
+import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
@NgModule({
entryComponents: [
@@ -33,7 +34,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone
DisplayColumnsPanelComponent,
AlarmStatusFilterPanelComponent,
EntitiesTableWidgetComponent,
- AlarmsTableWidgetComponent
+ AlarmsTableWidgetComponent,
+ TimeseriesTableWidgetComponent
],
imports: [
CommonModule,
@@ -42,7 +44,8 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone
],
exports: [
EntitiesTableWidgetComponent,
- AlarmsTableWidgetComponent
+ AlarmsTableWidgetComponent,
+ TimeseriesTableWidgetComponent
]
})
export class WidgetComponentsModule { }
diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss
index 0e490633e6..d81548ac10 100644
--- a/ui-ngx/src/styles.scss
+++ b/ui-ngx/src/styles.scss
@@ -857,7 +857,7 @@ mat-label {
span.no-data-found {
position: relative;
display: flex;
- height: calc(100% - 57px);
+ height: calc(100% - 60px);
text-transform: uppercase;
}