Commit 961455b4af0c0656304ded3a5c9551633dd692fe

Authored by Igor Kulikov
Committed by GitHub
2 parents 18d0d369 cbc8991b

Merge pull request #4096 from vvlladd28/improvement/timeseries-table/load

UI: Improvement load and update time into time series table
@@ -47,7 +47,7 @@ @@ -47,7 +47,7 @@
47 "resources": [], 47 "resources": [],
48 "templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>", 48 "templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>",
49 "templateCss": "", 49 "templateCss": "",
50 - "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}", 50 + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true\n };\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}",
51 "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}", 51 "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}",
52 "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}", 52 "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}",
53 "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\"}" 53 "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\"}"
@@ -134,4 +134,4 @@ @@ -134,4 +134,4 @@
134 } 134 }
135 } 135 }
136 ] 136 ]
137 -}  
  137 +}
@@ -71,6 +71,7 @@ export class DataAggregator { @@ -71,6 +71,7 @@ export class DataAggregator {
71 71
72 private dataReceived = false; 72 private dataReceived = false;
73 private resetPending = false; 73 private resetPending = false;
  74 + private updatedData = false;
74 75
75 private noAggregation = this.aggregationType === AggregationType.NONE; 76 private noAggregation = this.aggregationType === AggregationType.NONE;
76 private aggregationTimeout = Math.max(this.interval, 1000); 77 private aggregationTimeout = Math.max(this.interval, 1000);
@@ -90,7 +91,8 @@ export class DataAggregator { @@ -90,7 +91,8 @@ export class DataAggregator {
90 private timeWindow: number, 91 private timeWindow: number,
91 private interval: number, 92 private interval: number,
92 private stateData: boolean, 93 private stateData: boolean,
93 - private utils: UtilsService) { 94 + private utils: UtilsService,
  95 + private ignoreDataUpdateOnIntervalTick: boolean) {
94 this.tsKeyNames.forEach((key) => { 96 this.tsKeyNames.forEach((key) => {
95 this.dataBuffer[key] = []; 97 this.dataBuffer[key] = [];
96 }); 98 });
@@ -140,6 +142,7 @@ export class DataAggregator { @@ -140,6 +142,7 @@ export class DataAggregator {
140 this.elapsed = 0; 142 this.elapsed = 0;
141 this.aggregationTimeout = Math.max(this.interval, 1000); 143 this.aggregationTimeout = Math.max(this.interval, 1000);
142 this.resetPending = true; 144 this.resetPending = true;
  145 + this.updatedData = false;
143 this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); 146 this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
144 } 147 }
145 148
@@ -180,6 +183,7 @@ export class DataAggregator { @@ -180,6 +183,7 @@ export class DataAggregator {
180 this.onInterval(history, detectChanges); 183 this.onInterval(history, detectChanges);
181 } 184 }
182 } 185 }
  186 + this.updatedData = true;
183 } 187 }
184 188
185 private onInterval(history?: boolean, detectChanges?: boolean) { 189 private onInterval(history?: boolean, detectChanges?: boolean) {
@@ -201,8 +205,9 @@ export class DataAggregator { @@ -201,8 +205,9 @@ export class DataAggregator {
201 } else { 205 } else {
202 this.data = this.updateData(); 206 this.data = this.updateData();
203 } 207 }
204 - if (this.onDataCb) { 208 + if (this.onDataCb && (!this.ignoreDataUpdateOnIntervalTick || this.updatedData)) {
205 this.onDataCb(this.data, detectChanges); 209 this.onDataCb(this.data, detectChanges);
  210 + this.updatedData = false;
206 } 211 }
207 if (!history) { 212 if (!history) {
208 this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); 213 this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
@@ -223,6 +228,7 @@ export class DataAggregator { @@ -223,6 +228,7 @@ export class DataAggregator {
223 this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; 228 this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue];
224 } 229 }
225 aggKeyData.delete(aggTimestamp); 230 aggKeyData.delete(aggTimestamp);
  231 + this.updatedData = true;
226 } else if (aggTimestamp <= this.endTs) { 232 } else if (aggTimestamp <= this.endTs) {
227 const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; 233 const kvPair: [number, any] = [aggTimestamp, aggData.aggValue];
228 keyData.push(kvPair); 234 keyData.push(kvPair);
@@ -69,6 +69,7 @@ export interface EntityDataSubscriptionOptions { @@ -69,6 +69,7 @@ export interface EntityDataSubscriptionOptions {
69 type: widgetType; 69 type: widgetType;
70 entityFilter?: EntityFilter; 70 entityFilter?: EntityFilter;
71 isPaginatedDataSubscription?: boolean; 71 isPaginatedDataSubscription?: boolean;
  72 + ignoreDataUpdateOnIntervalTick?: boolean;
72 pageLink?: EntityDataPageLink; 73 pageLink?: EntityDataPageLink;
73 keyFilters?: Array<KeyFilter>; 74 keyFilters?: Array<KeyFilter>;
74 additionalKeyFilters?: Array<KeyFilter>; 75 additionalKeyFilters?: Array<KeyFilter>;
@@ -750,7 +751,8 @@ export class EntityDataSubscription { @@ -750,7 +751,8 @@ export class EntityDataSubscription {
750 subsTw.aggregation.timeWindow, 751 subsTw.aggregation.timeWindow,
751 subsTw.aggregation.interval, 752 subsTw.aggregation.interval,
752 subsTw.aggregation.stateData, 753 subsTw.aggregation.stateData,
753 - this.utils 754 + this.utils,
  755 + this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick
754 ); 756 );
755 } 757 }
756 758
@@ -60,7 +60,8 @@ export class EntityDataService { @@ -60,7 +60,8 @@ export class EntityDataService {
60 constructor(private telemetryService: TelemetryWebsocketService, 60 constructor(private telemetryService: TelemetryWebsocketService,
61 private utils: UtilsService) {} 61 private utils: UtilsService) {}
62 62
63 - public prepareSubscription(listener: EntityDataListener): Observable<EntityDataLoadResult> { 63 + public prepareSubscription(listener: EntityDataListener,
  64 + ignoreDataUpdateOnIntervalTick = false): Observable<EntityDataLoadResult> {
64 const datasource = listener.configDatasource; 65 const datasource = listener.configDatasource;
65 listener.subscriptionOptions = this.createSubscriptionOptions( 66 listener.subscriptionOptions = this.createSubscriptionOptions(
66 datasource, 67 datasource,
@@ -68,7 +69,8 @@ export class EntityDataService { @@ -68,7 +69,8 @@ export class EntityDataService {
68 datasource.pageLink, 69 datasource.pageLink,
69 datasource.keyFilters, 70 datasource.keyFilters,
70 null, 71 null,
71 - false); 72 + false,
  73 + ignoreDataUpdateOnIntervalTick);
72 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) { 74 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) {
73 return of(null); 75 return of(null);
74 } 76 }
@@ -87,7 +89,8 @@ export class EntityDataService { @@ -87,7 +89,8 @@ export class EntityDataService {
87 89
88 public subscribeForPaginatedData(listener: EntityDataListener, 90 public subscribeForPaginatedData(listener: EntityDataListener,
89 pageLink: EntityDataPageLink, 91 pageLink: EntityDataPageLink,
90 - keyFilters: KeyFilter[]): Observable<EntityDataLoadResult> { 92 + keyFilters: KeyFilter[],
  93 + ignoreDataUpdateOnIntervalTick = false): Observable<EntityDataLoadResult> {
91 const datasource = listener.configDatasource; 94 const datasource = listener.configDatasource;
92 listener.subscriptionOptions = this.createSubscriptionOptions( 95 listener.subscriptionOptions = this.createSubscriptionOptions(
93 datasource, 96 datasource,
@@ -95,7 +98,8 @@ export class EntityDataService { @@ -95,7 +98,8 @@ export class EntityDataService {
95 pageLink, 98 pageLink,
96 datasource.keyFilters, 99 datasource.keyFilters,
97 keyFilters, 100 keyFilters,
98 - true); 101 + true,
  102 + ignoreDataUpdateOnIntervalTick);
99 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) { 103 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) {
100 listener.dataLoaded(emptyPageData<EntityData>(), [], 104 listener.dataLoaded(emptyPageData<EntityData>(), [],
101 listener.configDatasourceIndex, listener.subscriptionOptions.pageLink); 105 listener.configDatasourceIndex, listener.subscriptionOptions.pageLink);
@@ -119,7 +123,8 @@ export class EntityDataService { @@ -119,7 +123,8 @@ export class EntityDataService {
119 pageLink: EntityDataPageLink, 123 pageLink: EntityDataPageLink,
120 keyFilters: KeyFilter[], 124 keyFilters: KeyFilter[],
121 additionalKeyFilters: KeyFilter[], 125 additionalKeyFilters: KeyFilter[],
122 - isPaginatedDataSubscription: boolean): EntityDataSubscriptionOptions { 126 + isPaginatedDataSubscription: boolean,
  127 + ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions {
123 const subscriptionDataKeys: Array<SubscriptionDataKey> = []; 128 const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
124 datasource.dataKeys.forEach((dataKey) => { 129 datasource.dataKeys.forEach((dataKey) => {
125 const subscriptionDataKey: SubscriptionDataKey = { 130 const subscriptionDataKey: SubscriptionDataKey = {
@@ -145,6 +150,7 @@ export class EntityDataService { @@ -145,6 +150,7 @@ export class EntityDataService {
145 } 150 }
146 } 151 }
147 entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription; 152 entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription;
  153 + entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick;
148 return entityDataSubscriptionOptions; 154 return entityDataSubscriptionOptions;
149 } 155 }
150 } 156 }
@@ -226,6 +226,7 @@ export interface WidgetSubscriptionOptions { @@ -226,6 +226,7 @@ export interface WidgetSubscriptionOptions {
226 hasDataPageLink?: boolean; 226 hasDataPageLink?: boolean;
227 singleEntity?: boolean; 227 singleEntity?: boolean;
228 warnOnPageDataOverflow?: boolean; 228 warnOnPageDataOverflow?: boolean;
  229 + ignoreDataUpdateOnIntervalTick?: boolean;
229 targetDeviceAliasIds?: Array<string>; 230 targetDeviceAliasIds?: Array<string>;
230 targetDeviceIds?: Array<string>; 231 targetDeviceIds?: Array<string>;
231 useDashboardTimewindow?: boolean; 232 useDashboardTimewindow?: boolean;
@@ -83,6 +83,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -83,6 +83,7 @@ export class WidgetSubscription implements IWidgetSubscription {
83 hasDataPageLink: boolean; 83 hasDataPageLink: boolean;
84 singleEntity: boolean; 84 singleEntity: boolean;
85 warnOnPageDataOverflow: boolean; 85 warnOnPageDataOverflow: boolean;
  86 + ignoreDataUpdateOnIntervalTick: boolean;
86 87
87 datasourcePages: PageData<Datasource>[]; 88 datasourcePages: PageData<Datasource>[];
88 dataPages: PageData<Array<DatasourceData>>[]; 89 dataPages: PageData<Array<DatasourceData>>[];
@@ -200,6 +201,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -200,6 +201,7 @@ export class WidgetSubscription implements IWidgetSubscription {
200 this.hasDataPageLink = options.hasDataPageLink; 201 this.hasDataPageLink = options.hasDataPageLink;
201 this.singleEntity = options.singleEntity; 202 this.singleEntity = options.singleEntity;
202 this.warnOnPageDataOverflow = options.warnOnPageDataOverflow; 203 this.warnOnPageDataOverflow = options.warnOnPageDataOverflow;
  204 + this.ignoreDataUpdateOnIntervalTick = options.ignoreDataUpdateOnIntervalTick;
203 this.datasourcePages = []; 205 this.datasourcePages = [];
204 this.datasources = []; 206 this.datasources = [];
205 this.dataPages = []; 207 this.dataPages = [];
@@ -423,7 +425,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -423,7 +425,7 @@ export class WidgetSubscription implements IWidgetSubscription {
423 } 425 }
424 }; 426 };
425 this.entityDataListeners.push(listener); 427 this.entityDataListeners.push(listener);
426 - return this.ctx.entityDataService.prepareSubscription(listener); 428 + return this.ctx.entityDataService.prepareSubscription(listener, this.ignoreDataUpdateOnIntervalTick);
427 }); 429 });
428 return forkJoin(resolveResultObservables).pipe( 430 return forkJoin(resolveResultObservables).pipe(
429 map((resolveResults) => { 431 map((resolveResults) => {
@@ -815,7 +817,8 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -815,7 +817,8 @@ export class WidgetSubscription implements IWidgetSubscription {
815 } 817 }
816 }; 818 };
817 this.entityDataListeners[datasourceIndex] = entityDataListener; 819 this.entityDataListeners[datasourceIndex] = entityDataListener;
818 - return this.ctx.entityDataService.subscribeForPaginatedData(entityDataListener, pageLink, keyFilters); 820 + return this.ctx.entityDataService.subscribeForPaginatedData(entityDataListener, pageLink, keyFilters,
  821 + this.ignoreDataUpdateOnIntervalTick);
819 } else { 822 } else {
820 return of(null); 823 return of(null);
821 } 824 }
@@ -39,73 +39,75 @@ @@ -39,73 +39,75 @@
39 </mat-toolbar> 39 </mat-toolbar>
40 <mat-tab-group [ngClass]="{'tb-headless': sources.length === 1}" fxFlex 40 <mat-tab-group [ngClass]="{'tb-headless': sources.length === 1}" fxFlex
41 [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> 41 [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()">
42 - <mat-tab *ngFor="let source of sources; trackBy: trackBySourcesIndex" label="{{ source.datasource.name }}">  
43 - <div fxFlex class="table-container">  
44 - <table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"  
45 - matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" 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 * 40) + 'px',  
62 - maxWidth: (actionCellDescriptors.length * 40) + 'px',  
63 - width: (actionCellDescriptors.length * 40) + 'px' }">  
64 - </mat-header-cell>  
65 - <mat-cell *matCellDef="let row" [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 40) + 'px',  
66 - maxWidth: (actionCellDescriptors.length * 40) + 'px',  
67 - width: (actionCellDescriptors.length * 40) + 'px' }">  
68 - <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">  
69 - <button mat-button mat-icon-button [disabled]="isLoading$ | async"  
70 - *ngFor="let actionDescriptor of actionCellDescriptors; trackBy: trackByActionCellDescriptionId"  
71 - matTooltip="{{ actionDescriptor.displayName }}"  
72 - matTooltipPosition="above"  
73 - (click)="onActionButtonClick($event, row, actionDescriptor)">  
74 - <mat-icon>{{actionDescriptor.icon}}</mat-icon>  
75 - </button>  
76 - </div>  
77 - <div fxHide fxShow.lt-lg *ngIf="actionCellDescriptors.length">  
78 - <button mat-button mat-icon-button  
79 - (click)="$event.stopPropagation(); ctx.detectChanges();"  
80 - [matMenuTriggerFor]="cellActionsMenu">  
81 - <mat-icon class="material-icons">more_vert</mat-icon>  
82 - </button>  
83 - <mat-menu #cellActionsMenu="matMenu" xPosition="before">  
84 - <button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors; trackBy: trackByActionCellDescriptionId"  
85 - [disabled]="isLoading$ | async" 42 + <mat-tab *ngFor="let source of sources; trackBy: trackBySourcesIndex; let index = index;" label="{{ source.datasource.name }}">
  43 + <ng-template [ngIf]="isActiveTab(index)">
  44 + <div fxFlex class="table-container">
  45 + <table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"
  46 + matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear>
  47 + <ng-container *ngIf="showTimestamp" [matColumnDef]="'0'">
  48 + <mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
  49 + <mat-cell *matCellDef="let row;"
  50 + [innerHTML]="cellContent(source, 0, row, row[0])"
  51 + [ngStyle]="cellStyle(source, 0, row[0])">
  52 + </mat-cell>
  53 + </ng-container>
  54 + <ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;">
  55 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ h.dataKey.label }} </mat-header-cell>
  56 + <mat-cell *matCellDef="let row;"
  57 + [innerHTML]="cellContent(source, h.index, row, row[h.index])"
  58 + [ngStyle]="cellStyle(source, h.index, row[h.index])">
  59 + </mat-cell>
  60 + </ng-container>
  61 + <ng-container matColumnDef="actions" stickyEnd>
  62 + <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 40) + 'px',
  63 + maxWidth: (actionCellDescriptors.length * 40) + 'px',
  64 + width: (actionCellDescriptors.length * 40) + 'px' }">
  65 + </mat-header-cell>
  66 + <mat-cell *matCellDef="let row" [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 40) + 'px',
  67 + maxWidth: (actionCellDescriptors.length * 40) + 'px',
  68 + width: (actionCellDescriptors.length * 40) + 'px' }">
  69 + <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
  70 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  71 + *ngFor="let actionDescriptor of actionCellDescriptors; trackBy: trackByActionCellDescriptionId"
  72 + matTooltip="{{ actionDescriptor.displayName }}"
  73 + matTooltipPosition="above"
86 (click)="onActionButtonClick($event, row, actionDescriptor)"> 74 (click)="onActionButtonClick($event, row, actionDescriptor)">
87 <mat-icon>{{actionDescriptor.icon}}</mat-icon> 75 <mat-icon>{{actionDescriptor.icon}}</mat-icon>
88 - <span>{{ actionDescriptor.displayName }}</span>  
89 </button> 76 </button>
90 - </mat-menu>  
91 - </div>  
92 - </mat-cell>  
93 - </ng-container>  
94 - <mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: true"></mat-header-row>  
95 - <mat-row *matRowDef="let row; columns: source.displayedColumns;"  
96 - (click)="onRowClick($event, row)"></mat-row>  
97 - </table>  
98 - <span [fxShow]="source.timeseriesDatasource.isEmpty() | async"  
99 - fxLayoutAlign="center center"  
100 - class="no-data-found" translate>widget.no-data-found</span>  
101 - </div>  
102 - <mat-divider *ngIf="displayPagination"></mat-divider>  
103 - <mat-paginator *ngIf="displayPagination"  
104 - [length]="source.timeseriesDatasource.total() | async"  
105 - [pageIndex]="source.pageLink.page"  
106 - [pageSize]="source.pageLink.pageSize"  
107 - [pageSizeOptions]="pageSizeOptions"  
108 - showFirstLastButtons></mat-paginator> 77 + </div>
  78 + <div fxHide fxShow.lt-lg *ngIf="actionCellDescriptors.length">
  79 + <button mat-button mat-icon-button
  80 + (click)="$event.stopPropagation(); ctx.detectChanges();"
  81 + [matMenuTriggerFor]="cellActionsMenu">
  82 + <mat-icon class="material-icons">more_vert</mat-icon>
  83 + </button>
  84 + <mat-menu #cellActionsMenu="matMenu" xPosition="before">
  85 + <button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors; trackBy: trackByActionCellDescriptionId"
  86 + [disabled]="isLoading$ | async"
  87 + (click)="onActionButtonClick($event, row, actionDescriptor)">
  88 + <mat-icon>{{actionDescriptor.icon}}</mat-icon>
  89 + <span>{{ actionDescriptor.displayName }}</span>
  90 + </button>
  91 + </mat-menu>
  92 + </div>
  93 + </mat-cell>
  94 + </ng-container>
  95 + <mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: true"></mat-header-row>
  96 + <mat-row *matRowDef="let row; columns: source.displayedColumns;"
  97 + (click)="onRowClick($event, row)"></mat-row>
  98 + </table>
  99 + <span [fxShow]="source.timeseriesDatasource.isEmpty() | async"
  100 + fxLayoutAlign="center center"
  101 + class="no-data-found" translate>widget.no-data-found</span>
  102 + </div>
  103 + <mat-divider *ngIf="displayPagination"></mat-divider>
  104 + <mat-paginator *ngIf="displayPagination"
  105 + [length]="source.timeseriesDatasource.total() | async"
  106 + [pageIndex]="source.pageLink.page"
  107 + [pageSize]="source.pageLink.pageSize"
  108 + [pageSizeOptions]="pageSizeOptions"
  109 + showFirstLastButtons></mat-paginator>
  110 + </ng-template>
109 </mat-tab> 111 </mat-tab>
110 </mat-tab-group> 112 </mat-tab-group>
111 </div> 113 </div>
@@ -40,12 +40,12 @@ import { @@ -40,12 +40,12 @@ import {
40 } from '@shared/models/widget.models'; 40 } from '@shared/models/widget.models';
41 import { UtilsService } from '@core/services/utils.service'; 41 import { UtilsService } from '@core/services/utils.service';
42 import { TranslateService } from '@ngx-translate/core'; 42 import { TranslateService } from '@ngx-translate/core';
43 -import {hashCode, isDefined, isDefinedAndNotNull, isNumber} from '@core/utils'; 43 +import { hashCode, isDefined, isNumber } from '@core/utils';
44 import cssjs from '@core/css/css'; 44 import cssjs from '@core/css/css';
45 import { PageLink } from '@shared/models/page/page-link'; 45 import { PageLink } from '@shared/models/page/page-link';
46 import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; 46 import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
47 import { CollectionViewer, DataSource } from '@angular/cdk/collections'; 47 import { CollectionViewer, DataSource } from '@angular/cdk/collections';
48 -import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; 48 +import { BehaviorSubject, fromEvent, merge, Observable, of, Subscription } from 'rxjs';
49 import { emptyPageData, PageData } from '@shared/models/page/page-data'; 49 import { emptyPageData, PageData } from '@shared/models/page/page-data';
50 import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; 50 import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
51 import { MatPaginator } from '@angular/material/paginator'; 51 import { MatPaginator } from '@angular/material/paginator';
@@ -129,6 +129,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @@ -129,6 +129,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
129 public showTimestamp = true; 129 public showTimestamp = true;
130 private dateFormatFilter: string; 130 private dateFormatFilter: string;
131 131
  132 + private subscriptions: Subscription[] = [];
  133 +
132 private searchAction: WidgetAction = { 134 private searchAction: WidgetAction = {
133 name: 'action.search', 135 name: 'action.search',
134 show: true, 136 show: true,
@@ -166,40 +168,27 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @@ -166,40 +168,27 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
166 debounceTime(150), 168 debounceTime(150),
167 distinctUntilChanged(), 169 distinctUntilChanged(),
168 tap(() => { 170 tap(() => {
169 - if (this.displayPagination) {  
170 - this.paginators.forEach((paginator) => {  
171 - paginator.pageIndex = 0;  
172 - });  
173 - }  
174 this.sources.forEach((source) => { 171 this.sources.forEach((source) => {
175 source.pageLink.textSearch = this.textSearch; 172 source.pageLink.textSearch = this.textSearch;
  173 + if (this.displayPagination) {
  174 + source.pageLink.page = 0;
  175 + }
176 }); 176 });
177 - this.updateAllData(); 177 + this.loadCurrentSourceRow();
  178 + this.ctx.detectChanges();
178 }) 179 })
179 ) 180 )
180 .subscribe(); 181 .subscribe();
181 182
182 - if (this.displayPagination) {  
183 - this.sorts.forEach((sort, index) => {  
184 - sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0);  
185 - });  
186 - }  
187 - this.sorts.forEach((sort, index) => {  
188 - const paginator = this.displayPagination ? this.paginators.toArray()[index] : null;  
189 - sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0);  
190 - ((this.displayPagination ? merge(sort.sortChange, paginator.page) : sort.sortChange) as Observable<any>)  
191 - .pipe(  
192 - tap(() => this.updateData(sort, paginator, index))  
193 - )  
194 - .subscribe(); 183 + this.sorts.changes.subscribe(() => {
  184 + this.initSubscriptionsToSortAndPaginator();
195 }); 185 });
196 - this.updateAllData(); 186 +
  187 + this.initSubscriptionsToSortAndPaginator();
197 } 188 }
198 189
199 public onDataUpdated() { 190 public onDataUpdated() {
200 - this.sources.forEach((source) => {  
201 - source.timeseriesDatasource.dataUpdated(this.data);  
202 - }); 191 + this.updateCurrentSourceData();
203 } 192 }
204 193
205 private initialize() { 194 private initialize() {
@@ -305,7 +294,27 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @@ -305,7 +294,27 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
305 this.ctx.activeEntityInfo = activeEntityInfo; 294 this.ctx.activeEntityInfo = activeEntityInfo;
306 } 295 }
307 296
  297 + private initSubscriptionsToSortAndPaginator() {
  298 + this.subscriptions.forEach(subscription => subscription.unsubscribe());
  299 + this.sorts.forEach((sort, index) => {
  300 + let paginator = null;
  301 + const observables = [sort.sortChange];
  302 + if (this.displayPagination) {
  303 + paginator = this.paginators.toArray()[index];
  304 + this.subscriptions.push(
  305 + sort.sortChange.subscribe(() => paginator.pageIndex = 0)
  306 + );
  307 + observables.push(paginator.page);
  308 + }
  309 + this.updateData(sort, paginator);
  310 + this.subscriptions.push(merge(...observables).pipe(
  311 + tap(() => this.updateData(sort, paginator))
  312 + ).subscribe());
  313 + });
  314 + }
  315 +
308 onSourceIndexChanged() { 316 onSourceIndexChanged() {
  317 + this.updateCurrentSourceData();
309 this.updateActiveEntityInfo(); 318 this.updateActiveEntityInfo();
310 } 319 }
311 320
@@ -326,30 +335,19 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @@ -326,30 +335,19 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
326 exitFilterMode() { 335 exitFilterMode() {
327 this.textSearchMode = false; 336 this.textSearchMode = false;
328 this.textSearch = null; 337 this.textSearch = null;
329 - this.sources.forEach((source, index) => { 338 + this.sources.forEach((source) => {
330 source.pageLink.textSearch = this.textSearch; 339 source.pageLink.textSearch = this.textSearch;
331 - const sort = this.sorts.toArray()[index];  
332 - let paginator = null;  
333 if (this.displayPagination) { 340 if (this.displayPagination) {
334 - paginator = this.paginators.toArray()[index];  
335 - paginator.pageIndex = 0; 341 + source.pageLink.page = 0;
336 } 342 }
337 - this.updateData(sort, paginator, index);  
338 }); 343 });
  344 + this.loadCurrentSourceRow();
339 this.ctx.hideTitlePanel = false; 345 this.ctx.hideTitlePanel = false;
340 this.ctx.detectChanges(true); 346 this.ctx.detectChanges(true);
341 } 347 }
342 348
343 - private updateAllData() {  
344 - this.sources.forEach((source, index) => {  
345 - const sort = this.sorts.toArray()[index];  
346 - const paginator = this.displayPagination ? this.paginators.toArray()[index] : null;  
347 - this.updateData(sort, paginator, index);  
348 - });  
349 - }  
350 -  
351 - private updateData(sort: MatSort, paginator: MatPaginator, index: number) {  
352 - const source = this.sources[index]; 349 + private updateData(sort: MatSort, paginator: MatPaginator) {
  350 + const source = this.sources[this.sourceIndex];
353 if (this.displayPagination) { 351 if (this.displayPagination) {
354 source.pageLink.page = paginator.pageIndex; 352 source.pageLink.page = paginator.pageIndex;
355 source.pageLink.pageSize = paginator.pageSize; 353 source.pageLink.pageSize = paginator.pageSize;
@@ -418,7 +416,6 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @@ -418,7 +416,6 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
418 416
419 if (!isDefined(content)) { 417 if (!isDefined(content)) {
420 return ''; 418 return '';
421 -  
422 } else { 419 } else {
423 switch (typeof content) { 420 switch (typeof content) {
424 case 'string': 421 case 'string':
@@ -462,6 +459,18 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @@ -462,6 +459,18 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
462 } 459 }
463 this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row, entityLabel); 460 this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row, entityLabel);
464 } 461 }
  462 +
  463 + public isActiveTab(index: number): boolean {
  464 + return index === this.sourceIndex;
  465 + }
  466 +
  467 + private updateCurrentSourceData() {
  468 + this.sources[this.sourceIndex].timeseriesDatasource.dataUpdated(this.data);
  469 + }
  470 +
  471 + private loadCurrentSourceRow() {
  472 + this.sources[this.sourceIndex].timeseriesDatasource.loadRows();
  473 + }
465 } 474 }
466 475
467 class TimeseriesDatasource implements DataSource<TimeseriesRow> { 476 class TimeseriesDatasource implements DataSource<TimeseriesRow> {
@@ -482,6 +491,10 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> { @@ -482,6 +491,10 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
482 } 491 }
483 492
484 connect(collectionViewer: CollectionViewer): Observable<TimeseriesRow[] | ReadonlyArray<TimeseriesRow>> { 493 connect(collectionViewer: CollectionViewer): Observable<TimeseriesRow[] | ReadonlyArray<TimeseriesRow>> {
  494 + if (this.rowsSubject.isStopped) {
  495 + this.rowsSubject.isStopped = false;
  496 + this.pageDataSubject.isStopped = false;
  497 + }
485 return this.rowsSubject.asObservable(); 498 return this.rowsSubject.asObservable();
486 } 499 }
487 500
@@ -485,6 +485,9 @@ export class WidgetComponentService { @@ -485,6 +485,9 @@ export class WidgetComponentService {
485 if (isUndefined(result.typeParameters.warnOnPageDataOverflow)) { 485 if (isUndefined(result.typeParameters.warnOnPageDataOverflow)) {
486 result.typeParameters.warnOnPageDataOverflow = true; 486 result.typeParameters.warnOnPageDataOverflow = true;
487 } 487 }
  488 + if (isUndefined(result.typeParameters.ignoreDataUpdateOnIntervalTick)) {
  489 + result.typeParameters.ignoreDataUpdateOnIntervalTick = false;
  490 + }
488 if (isUndefined(result.typeParameters.dataKeysOptional)) { 491 if (isUndefined(result.typeParameters.dataKeysOptional)) {
489 result.typeParameters.dataKeysOptional = false; 492 result.typeParameters.dataKeysOptional = false;
490 } 493 }
@@ -895,6 +895,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -895,6 +895,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
895 hasDataPageLink: this.typeParameters.hasDataPageLink, 895 hasDataPageLink: this.typeParameters.hasDataPageLink,
896 singleEntity: this.typeParameters.singleEntity, 896 singleEntity: this.typeParameters.singleEntity,
897 warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow, 897 warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow,
  898 + ignoreDataUpdateOnIntervalTick: this.typeParameters.ignoreDataUpdateOnIntervalTick,
898 comparisonEnabled: comparisonSettings.comparisonEnabled, 899 comparisonEnabled: comparisonSettings.comparisonEnabled,
899 timeForComparison: comparisonSettings.timeForComparison 900 timeForComparison: comparisonSettings.timeForComparison
900 }; 901 };
@@ -154,6 +154,7 @@ export interface WidgetTypeParameters { @@ -154,6 +154,7 @@ export interface WidgetTypeParameters {
154 hasDataPageLink?: boolean; 154 hasDataPageLink?: boolean;
155 singleEntity?: boolean; 155 singleEntity?: boolean;
156 warnOnPageDataOverflow?: boolean; 156 warnOnPageDataOverflow?: boolean;
  157 + ignoreDataUpdateOnIntervalTick?: boolean;
157 } 158 }
158 159
159 export interface WidgetControllerDescriptor { 160 export interface WidgetControllerDescriptor {