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 1c96d7168e..8f115009d0 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -21,22 +21,6 @@
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\"}"
}
},
- {
- "alias": "entities_table",
- "name": "Entities table",
- "descriptor": {
- "type": "latest",
- "sizeX": 7.5,
- "sizeY": 6.5,
- "resources": [],
- "templateHtml": "\n",
- "templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: 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 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type 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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
- "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\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, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}"
- }
- },
{
"alias": "html_card",
"name": "HTML Card",
@@ -132,6 +116,22 @@
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: types.entitySearchDirection.from,\\n relationTypeGroup: \\\"COMMON\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}"
}
+ },
+ {
+ "alias": "entities_table",
+ "name": "Entities table",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 6.5,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n dataKeysOptional: 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 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type 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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
+ "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\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, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}"
+ }
}
]
}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
index d9e8c043c5..4fd46195d0 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -128,7 +128,6 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
serviceId = serviceInfoProvider.getServiceId();
wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback"));
tsInSqlDB = databaseTsType.equalsIgnoreCase("sql") || databaseTsType.equalsIgnoreCase("timescale");
-
}
@PreDestroy
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
index ee30fefe9a..dbd92ae2a0 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
index 5f695d14de..98ba5c954f 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
@@ -1,3 +1,18 @@
+/**
+ * Copyright © 2016-2020 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.
+ */
package org.thingsboard.server.service.subscription;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
index ee4bd2c8a0..371fd23c5c 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
index 939ea18ab1..8fb6f59fd1 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -213,13 +213,16 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
+
wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
wsClient.send(mapper.writeValueAsString(wrapper));
msg = wsClient.waitForReply();
update = mapper.readValue(msg, EntityDataUpdate.class);
+
Assert.assertEquals(1, update.getCmdId());
+
pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
@@ -243,6 +246,7 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue);
+
}
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
index 62dfdbb107..cf1c31c851 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
index 6fa9daf01f..b7cb331237 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
@@ -48,6 +48,7 @@ public class EntityKeyMapping {
static {
entityFieldColumnMap.put("createdTime", "id");
+ entityFieldColumnMap.put("entityType", "entity_type");
entityFieldColumnMap.put("name", "name");
entityFieldColumnMap.put("type", "type");
entityFieldColumnMap.put("label", "label");
diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts
index e0afe96c42..f0beea751e 100644
--- a/ui-ngx/src/app/core/api/alias-controller.ts
+++ b/ui-ngx/src/app/core/api/alias-controller.ts
@@ -22,8 +22,8 @@ import { EntityService } from '@core/http/entity.service';
import { UtilsService } from '@core/services/utils.service';
import { AliasFilterType, EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models';
-import { map } from 'rxjs/operators';
-import { defaultEntityDataPageLink } from '@shared/models/query/query.models';
+import { map, mergeMap } from 'rxjs/operators';
+import { createDefaultEntityDataPageLink, defaultEntityDataPageLink } from '@shared/models/query/query.models';
export class AliasController implements IAliasController {
@@ -169,7 +169,24 @@ export class AliasController implements IAliasController {
}
}
- private resolveDatasource(datasource: Datasource, isSingle?: boolean): Observable> {
+ resolveSingleEntityInfo(aliasId: string): Observable {
+ return this.getAliasInfo(aliasId).pipe(
+ mergeMap((aliasInfo) => {
+ if (aliasInfo.resolveMultiple) {
+ if (aliasInfo.entityFilter) {
+ return this.entityService.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter,
+ {ignoreLoading: true, ignoreErrors: true});
+ } else {
+ return of(null);
+ }
+ } else {
+ return of(aliasInfo.currentEntity);
+ }
+ })
+ );
+ }
+
+ private resolveDatasource(datasource: Datasource, isSingle?: boolean): Observable {
if (datasource.type === DatasourceType.entity) {
if (datasource.entityAliasId) {
return this.getAliasInfo(datasource.entityAliasId).pipe(
@@ -200,14 +217,14 @@ export class AliasController implements IAliasController {
datasources.push(newDatasource);
}
return datasources;*/
- return [newDatasource];
+ return newDatasource;
} else {
if (aliasInfo.stateEntity) {
newDatasource = deepClone(datasource);
newDatasource.unresolvedStateEntity = true;
- return [newDatasource];
+ return newDatasource;
} else {
- return [];
+ return null;
// throw new Error('Unable to resolve datasource.');
}
}
@@ -232,13 +249,13 @@ export class AliasController implements IAliasController {
entityType: entity.entityType
}
};
- return [datasource];
+ return datasource;
} else {
if (aliasInfo.stateEntity) {
datasource.unresolvedStateEntity = true;
- return [datasource];
+ return datasource;
} else {
- return [];
+ return null;
// throw new Error('Unable to resolve datasource.');
}
}
@@ -248,10 +265,10 @@ export class AliasController implements IAliasController {
} else {
datasource.aliasName = datasource.entityName;
datasource.name = datasource.entityName;
- return of([datasource]);
+ return of(datasource);
}
} else {
- return of([datasource]);
+ return of(datasource);
}
}
@@ -354,18 +371,14 @@ export class AliasController implements IAliasController {
);
}
- resolveDatasources(datasources: Array): Observable> {
- const newDatasources = deepClone(datasources);
- const observables = new Array>>();
+ resolveDatasources(datasources: Array, singleEntity?: boolean): Observable> {
+ const newDatasources = deepClone(singleEntity ? [datasources[0]] : datasources);
+ const observables = new Array>();
newDatasources.forEach((datasource) => {
observables.push(this.resolveDatasource(datasource));
});
return forkJoin(observables).pipe(
- map((arrayOfDatasources) => {
- const result = new Array();
- arrayOfDatasources.forEach((datasourcesArray) => {
- result.push(...datasourcesArray);
- });
+ map((result) => {
let functionIndex = 0;
result.forEach((datasource) => {
if (datasource.type === DatasourceType.function) {
@@ -386,6 +399,9 @@ export class AliasController implements IAliasController {
datasource.name = 'Unresolved';
datasource.entityName = 'Unresolved';
} else if (datasource.type === DatasourceType.entity) {
+ if (singleEntity) {
+ datasource.pageLink = createDefaultEntityDataPageLink(1);
+ }
if (!datasource.pageLink) {
datasource.pageLink = deepClone(defaultEntityDataPageLink);
}
diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts
index d700672d53..591a5436d4 100644
--- a/ui-ngx/src/app/core/api/entity-data-subscription.ts
+++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts
@@ -35,19 +35,21 @@ import {
TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
-import { EntityDataListener } from '@core/api/entity-data.service';
+import { EntityDataListener, EntityDataLoadResult } from '@core/api/entity-data.service';
import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils';
import { PageData } from '@shared/models/page/page-data';
import { DataAggregator } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
import Timeout = NodeJS.Timeout;
+import { Observable, of, ReplaySubject, Subject } from 'rxjs';
export interface EntityDataSubscriptionOptions {
datasourceType: DatasourceType;
dataKeys: Array;
type: widgetType;
entityFilter?: EntityFilter;
+ isLatestDataSubscription?: boolean;
pageLink?: EntityDataPageLink;
keyFilters?: Array;
subscriptionTimewindow?: SubscriptionTimewindow;
@@ -59,21 +61,19 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyInd
export class EntityDataSubscription {
- private listeners: Array = [];
private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType;
-
- private history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
- isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
-
- private realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
- isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
+ private history: boolean;
+ private realtime: boolean;
private subscriber: TelemetrySubscriber;
+ private dataCommand: EntityDataCmd;
+ private subsCommand: EntityDataCmd;
private attrFields: Array;
private tsFields: Array;
private latestValues: Array;
+ private entityDataResolveSubject: Subject;
private pageData: PageData;
private subsTw: SubscriptionTimewindow;
private dataAggregators: Array;
@@ -87,7 +87,11 @@ export class EntityDataSubscription {
private tickElapsed = 0;
private timer: Timeout;
- constructor(private entityDataSubscriptionOptions: EntityDataSubscriptionOptions,
+ private dataResolved = false;
+ private started = false;
+
+ constructor(public entityDataSubscriptionOptions: EntityDataSubscriptionOptions,
+ private listener: EntityDataListener,
private telemetryService: TelemetryService,
private utils: UtilsService) {
this.initializeSubscription();
@@ -126,50 +130,6 @@ export class EntityDataSubscription {
}
dataKey.key = key;
}
- if (this.datasourceType === DatasourceType.function) {
- this.frequency = 1000;
- if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
- this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
- }
- }
- }
-
- public addListener(listener: EntityDataListener) {
- this.listeners.push(listener);
- }
-
- public hasListeners(): boolean {
- return this.listeners.length > 0;
- }
-
- public removeListener(listener: EntityDataListener) {
- this.listeners.splice(this.listeners.indexOf(listener), 1);
- }
-
- public syncListener(listener: EntityDataListener) {
- if (this.pageData) {
- let key: string;
- let dataKey: SubscriptionDataKey;
- const data: Array> = [];
- for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
- data[dataIndex] = [];
- for (key of Object.keys(this.dataKeys)) {
- if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
- const dataKeysList = this.dataKeys[key] as Array;
- for (let i = 0; i < dataKeysList.length; i++) {
- dataKey = dataKeysList[i];
- const datasourceKey = `${key}_${i}`;
- data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][datasourceKey];
- }
- } else {
- dataKey = this.dataKeys[key] as SubscriptionDataKey;
- data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][key];
- }
- }
- }
- listener.dataLoaded(this.pageData, data, listener.configDatasourceIndex);
- }
- this.listeners.push(listener);
}
public unsubscribe() {
@@ -192,19 +152,30 @@ export class EntityDataSubscription {
this.pageData = null;
}
- public start() {
- this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
+ public subscribe(): Observable {
+ if (!this.entityDataSubscriptionOptions.isLatestDataSubscription) {
+ this.entityDataResolveSubject = new ReplaySubject(1);
+ } else {
+ this.started = true;
+ this.dataResolved = true;
+ }
if (this.datasourceType === DatasourceType.entity) {
const entityFields: Array =
this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
- dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
- );
+ dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
+ );
if (!entityFields.find(key => key.key === 'name')) {
entityFields.push({
type: EntityKeyType.ENTITY_FIELD,
key: 'name'
});
}
+ if (!entityFields.find(key => key.key === 'label')) {
+ entityFields.push({
+ type: EntityKeyType.ENTITY_FIELD,
+ key: 'label'
+ });
+ }
this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
@@ -217,9 +188,9 @@ export class EntityDataSubscription {
this.latestValues = this.attrFields.concat(this.tsFields);
this.subscriber = new TelemetrySubscriber(this.telemetryService);
- const command = new EntityDataCmd();
+ this.dataCommand = new EntityDataCmd();
- command.query = {
+ this.dataCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter,
pageLink: this.entityDataSubscriptionOptions.pageLink,
keyFilters: this.entityDataSubscriptionOptions.keyFilters,
@@ -227,72 +198,17 @@ export class EntityDataSubscription {
latestValues: this.latestValues
};
- if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
- if (this.tsFields.length > 0) {
- if (this.history) {
- command.historyCmd = {
- keys: this.tsFields.map(key => key.key),
- startTs: this.subsTw.fixedWindow.startTimeMs,
- endTs: this.subsTw.fixedWindow.endTimeMs,
- interval: this.subsTw.aggregation.interval,
- limit: this.subsTw.aggregation.limit,
- agg: this.subsTw.aggregation.type
+ if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
+ if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
+ if (this.latestValues.length > 0) {
+ this.dataCommand.latestCmd = {
+ keys: this.latestValues
};
- if (this.subsTw.aggregation.stateData) {
- command.historyCmd.startTs -= YEAR;
- }
- } else {
- command.tsCmd = {
- keys: this.tsFields.map(key => key.key),
- startTs: this.subsTw.startTs,
- timeWindow: this.subsTw.aggregation.timeWindow,
- interval: this.subsTw.aggregation.interval,
- limit: this.subsTw.aggregation.limit,
- agg: this.subsTw.aggregation.type
- }
- if (this.subsTw.aggregation.stateData) {
- command.historyCmd = {
- keys: this.tsFields.map(key => key.key),
- startTs: this.subsTw.startTs - YEAR,
- endTs: this.subsTw.startTs,
- interval: this.subsTw.aggregation.interval,
- limit: this.subsTw.aggregation.limit,
- agg: this.subsTw.aggregation.type
- };
- }
- this.subscriber.reconnect$.subscribe(() => {
- let newSubsTw: SubscriptionTimewindow = null;
- this.listeners.forEach((listener) => {
- if (!newSubsTw) {
- newSubsTw = listener.updateRealtimeSubscription();
- } else {
- listener.setRealtimeSubscription(newSubsTw);
- }
- });
- this.subsTw = newSubsTw;
- command.tsCmd.startTs = this.subsTw.startTs;
- command.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
- command.tsCmd.interval = this.subsTw.aggregation.interval;
- command.tsCmd.limit = this.subsTw.aggregation.limit;
- command.tsCmd.agg = this.subsTw.aggregation.type;
- if (this.subsTw.aggregation.stateData) {
- command.historyCmd.startTs = this.subsTw.startTs - YEAR;
- command.historyCmd.endTs = this.subsTw.startTs;
- command.historyCmd.interval = this.subsTw.aggregation.interval;
- command.historyCmd.limit = this.subsTw.aggregation.limit;
- command.historyCmd.agg = this.subsTw.aggregation.type;
- }
- });
}
}
- } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
- if (this.latestValues.length > 0) {
- command.latestCmd = {
- keys: this.latestValues.map(key => key.key)
- };
- }
}
- this.subscriber.subscriptionCommands.push(command);
+
+ this.subscriber.subscriptionCommands.push(this.dataCommand);
this.subscriber.entityData$.subscribe(
(entityDataUpdate) => {
@@ -304,6 +220,30 @@ export class EntityDataSubscription {
}
);
+ this.subscriber.reconnect$.subscribe(() => {
+ const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription();
+ this.listener.setRealtimeSubscription(newSubsTw);
+ this.subsTw = newSubsTw;
+ if (this.started && !this.entityDataSubscriptionOptions.isLatestDataSubscription) {
+ this.subsCommand.tsCmd.startTs = this.subsTw.startTs;
+ this.subsCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
+ this.subsCommand.tsCmd.interval = this.subsTw.aggregation.interval;
+ this.subsCommand.tsCmd.limit = this.subsTw.aggregation.limit;
+ this.subsCommand.tsCmd.agg = this.subsTw.aggregation.type;
+ if (this.subsTw.aggregation.stateData) {
+ this.subsCommand.historyCmd.startTs = this.subsTw.startTs - YEAR;
+ this.subsCommand.historyCmd.endTs = this.subsTw.startTs;
+ this.subsCommand.historyCmd.interval = this.subsTw.aggregation.interval;
+ this.subsCommand.historyCmd.limit = this.subsTw.aggregation.limit;
+ this.subsCommand.historyCmd.agg = this.subsTw.aggregation.type;
+ }
+ this.subsCommand.query = this.dataCommand.query;
+ this.subscriber.subscriptionCommands = [this.subsCommand];
+ } else {
+ this.subscriber.subscriptionCommands = [this.dataCommand];
+ }
+ });
+
this.subscriber.subscribe();
} else if (this.datasourceType === DatasourceType.function) {
const entityData: EntityData = {
@@ -325,29 +265,46 @@ export class EntityDataSubscription {
totalPages: 1
};
this.onPageData(pageData);
- this.tickScheduledTime = this.utils.currentPerfTime();
- if (this.history) {
- this.onTick(true);
- } else {
- this.timer = setTimeout(this.onTick.bind(this, true), 0);
+ if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
+ if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
+ this.frequency = 1000;
+ this.timer = setTimeout(this.onTick.bind(this, true), 0);
+ }
}
}
+ if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
+ return of(null);
+ } else {
+ return this.entityDataResolveSubject.asObservable();
+ }
}
- private onPageData(pageData: PageData) {
+ public start() {
+ if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
+ return;
+ }
+ this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
+ this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
+ isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
+ this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
+ isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
+
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+
if (this.dataAggregators) {
this.dataAggregators.forEach((aggregator) => {
aggregator.destroy();
})
- this.dataAggregators = null;
}
- this.datasourceData = [];
this.dataAggregators = [];
- this.entityIdToDataIndex = {};
- let tsKeyNames;
+ this.resetData();
+
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
+ let tsKeyNames = [];
if (this.datasourceType === DatasourceType.function) {
- tsKeyNames = [];
for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array;
dataKeysList.forEach((subscriptionDataKey) => {
@@ -357,20 +314,85 @@ export class EntityDataSubscription {
} else {
tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : [];
}
- }
- for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
- const entityData = pageData.data[dataIndex];
- this.entityIdToDataIndex[entityData.entityId.id] = dataIndex;
- this.datasourceData[dataIndex] = {};
- if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
+ for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
if (this.datasourceType === DatasourceType.function) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
- DataKeyType.function, dataIndex, this.notifyListeners.bind(this));
+ DataKeyType.function, dataIndex, this.notifyListener.bind(this));
} else if (!this.history && tsKeyNames.length) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
- DataKeyType.timeseries, dataIndex, this.notifyListeners.bind(this));
+ DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
}
}
+ }
+ if (this.datasourceType === DatasourceType.entity) {
+ this.subsCommand = new EntityDataCmd();
+ this.subsCommand.cmdId = this.dataCommand.cmdId;
+ if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
+ if (this.tsFields.length > 0) {
+ if (this.history) {
+ this.subsCommand.historyCmd = {
+ keys: this.tsFields.map(key => key.key),
+ startTs: this.subsTw.fixedWindow.startTimeMs,
+ endTs: this.subsTw.fixedWindow.endTimeMs,
+ interval: this.subsTw.aggregation.interval,
+ limit: this.subsTw.aggregation.limit,
+ agg: this.subsTw.aggregation.type
+ };
+ if (this.subsTw.aggregation.stateData) {
+ this.subsCommand.historyCmd.startTs -= YEAR;
+ }
+ } else {
+ this.subsCommand.tsCmd = {
+ keys: this.tsFields.map(key => key.key),
+ startTs: this.subsTw.startTs,
+ timeWindow: this.subsTw.aggregation.timeWindow,
+ interval: this.subsTw.aggregation.interval,
+ limit: this.subsTw.aggregation.limit,
+ agg: this.subsTw.aggregation.type
+ }
+ if (this.subsTw.aggregation.stateData) {
+ this.subsCommand.historyCmd = {
+ keys: this.tsFields.map(key => key.key),
+ startTs: this.subsTw.startTs - YEAR,
+ endTs: this.subsTw.startTs,
+ interval: this.subsTw.aggregation.interval,
+ limit: this.subsTw.aggregation.limit,
+ agg: this.subsTw.aggregation.type
+ };
+ }
+ }
+ }
+ } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
+ if (this.latestValues.length > 0) {
+ this.subsCommand.latestCmd = {
+ keys: this.latestValues
+ };
+ }
+ }
+ this.subscriber.subscriptionCommands = [this.subsCommand];
+ this.subscriber.update();
+ } else if (this.datasourceType === DatasourceType.function) {
+ this.frequency = 1000;
+ if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
+ this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
+ }
+ this.tickScheduledTime = this.utils.currentPerfTime();
+ if (this.history) {
+ this.onTick(true);
+ } else {
+ this.timer = setTimeout(this.onTick.bind(this, true), 0);
+ }
+ }
+ this.started = true;
+ }
+
+ private resetData() {
+ this.datasourceData = [];
+ this.entityIdToDataIndex = {};
+ for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
+ const entityData = this.pageData.data[dataIndex];
+ this.entityIdToDataIndex[entityData.entityId.id] = dataIndex;
+ this.datasourceData[dataIndex] = {};
for (const key of Object.keys(this.dataKeys)) {
const dataKey = this.dataKeys[key];
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
@@ -388,7 +410,23 @@ export class EntityDataSubscription {
}
}
this.datasourceOrigData = deepClone(this.datasourceData);
+ if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
+ for (const key of Object.keys(this.dataKeys)) {
+ const dataKeyList = this.dataKeys[key] as Array;
+ dataKeyList.forEach((dataKey) => {
+ delete dataKey.lastUpdateTime;
+ });
+ }
+ } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
+ for (const key of Object.keys(this.dataKeys)) {
+ delete (this.dataKeys[key] as SubscriptionDataKey).lastUpdateTime;
+ }
+ }
+ }
+ private onPageData(pageData: PageData) {
+ this.pageData = pageData;
+ this.resetData();
const data: Array> = [];
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
const entityData = pageData.data[dataIndex];
@@ -401,28 +439,33 @@ export class EntityDataSubscription {
}
);
}
-
- this.pageData = pageData;
-
- this.listeners.forEach((listener) => {
- listener.dataLoaded(pageData, data,
- listener.configDatasourceIndex);
- });
+ if (!this.dataResolved) {
+ this.dataResolved = true;
+ this.entityDataResolveSubject.next(
+ {
+ pageData,
+ data,
+ datasourceIndex: this.listener.configDatasourceIndex
+ }
+ );
+ this.entityDataResolveSubject.complete();
+ } else {
+ this.listener.dataLoaded(pageData, data,
+ this.listener.configDatasourceIndex);
+ }
}
private onDataUpdate(update: Array) {
for (const entityData of update) {
const dataIndex = this.entityIdToDataIndex[entityData.entityId.id];
- this.processEntityData(entityData, dataIndex, true, this.notifyListeners.bind(this));
+ this.processEntityData(entityData, dataIndex, true, this.notifyListener.bind(this));
}
}
- private notifyListeners(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
- this.listeners.forEach((listener) => {
- listener.dataUpdated(data,
- listener.configDatasourceIndex,
+ private notifyListener(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
+ this.listener.dataUpdated(data,
+ this.listener.configDatasourceIndex,
dataIndex, dataKeyIndex, detectChanges);
- });
}
private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean,
@@ -596,14 +639,10 @@ export class EntityDataSubscription {
const value = dataKey.func(time, prevSeries[1]);
const series: [number, any] = [time, value];
this.datasourceData[0][dataKey.key].data = [series];
- this.listeners.forEach(
- (listener) => {
- listener.dataUpdated(this.datasourceData[0][dataKey.key],
- listener.configDatasourceIndex,
- 0,
- dataKey.index, detectChanges);
- }
- );
+ this.listener.dataUpdated(this.datasourceData[0][dataKey.key],
+ this.listener.configDatasourceIndex,
+ 0,
+ dataKey.index, detectChanges);
}
private onTick(detectChanges: boolean) {
diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts
index b17e5f7b82..cddc9bcba0 100644
--- a/ui-ngx/src/app/core/api/entity-data.service.ts
+++ b/ui-ngx/src/app/core/api/entity-data.service.ts
@@ -24,17 +24,24 @@ import { UtilsService } from '@core/services/utils.service';
import { SubscriptionDataKey } from '@core/api/datasource-subcription';
import { deepClone, objectHashCode } from '@core/utils';
import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription';
+import { Observable, of } from 'rxjs';
export interface EntityDataListener {
subscriptionType: widgetType;
- subscriptionTimewindow: SubscriptionTimewindow;
+ subscriptionTimewindow?: SubscriptionTimewindow;
configDatasource: Datasource;
configDatasourceIndex: number;
dataLoaded: (pageData: PageData, data: Array>, datasourceIndex: number) => void;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
- updateRealtimeSubscription: () => SubscriptionTimewindow;
- setRealtimeSubscription: (subscriptionTimewindow: SubscriptionTimewindow) => void;
- entityDataSubscriptionKey?: number;
+ updateRealtimeSubscription?: () => SubscriptionTimewindow;
+ setRealtimeSubscription?: (subscriptionTimewindow: SubscriptionTimewindow) => void;
+ subscription?: EntityDataSubscription;
+}
+
+export interface EntityDataLoadResult {
+ pageData: PageData;
+ data: Array>;
+ datasourceIndex: number;
}
@Injectable({
@@ -42,16 +49,48 @@ export interface EntityDataListener {
})
export class EntityDataService {
- private subscriptions: {[entityDataSubscriptionKey: string]: EntityDataSubscription} = {};
-
constructor(private telemetryService: TelemetryWebsocketService,
private utils: UtilsService) {}
- public subscribeToEntityData(listener: EntityDataListener) {
+ public prepareSubscription(listener: EntityDataListener): Observable {
const datasource = listener.configDatasource;
if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) {
+ return of(null);
+ }
+ listener.subscription = this.createSubscription(listener,
+ datasource.pageLink, datasource.keyFilters,
+ false);
+ return listener.subscription.subscribe();
+ }
+
+ public startSubscription(listener: EntityDataListener) {
+ if (listener.subscriptionType === widgetType.timeseries) {
+ listener.subscription.entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
+ }
+ listener.subscription.start();
+ }
+
+ public subscribeForLatestData(listener: EntityDataListener,
+ pageLink: EntityDataPageLink,
+ keyFilters: KeyFilter[]) {
+ const datasource = listener.configDatasource;
+ if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) {
return;
}
+ listener.subscription = this.createSubscription(listener,
+ pageLink, keyFilters, true);
+ listener.subscription.subscribe();
+ }
+
+ public stopSubscription(listener: EntityDataListener) {
+ listener.subscription.unsubscribe();
+ }
+
+ private createSubscription(listener: EntityDataListener,
+ pageLink: EntityDataPageLink,
+ keyFilters: KeyFilter[],
+ isLatestDataSubscription: boolean): EntityDataSubscription {
+ const datasource = listener.configDatasource;
const subscriptionDataKeys: Array = [];
datasource.dataKeys.forEach((dataKey) => {
const subscriptionDataKey: SubscriptionDataKey = {
@@ -62,47 +101,19 @@ export class EntityDataService {
};
subscriptionDataKeys.push(subscriptionDataKey);
});
-
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
datasourceType: datasource.type,
dataKeys: subscriptionDataKeys,
type: listener.subscriptionType
};
-
- if (listener.subscriptionType === widgetType.timeseries) {
- entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
- }
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
- entityDataSubscriptionOptions.pageLink = datasource.pageLink;
- entityDataSubscriptionOptions.keyFilters = datasource.keyFilters;
- }
- listener.entityDataSubscriptionKey = objectHashCode(entityDataSubscriptionOptions);
- let subscription: EntityDataSubscription;
- if (this.subscriptions[listener.entityDataSubscriptionKey]) {
- subscription = this.subscriptions[listener.entityDataSubscriptionKey];
- subscription.syncListener(listener);
- } else {
- subscription = new EntityDataSubscription(entityDataSubscriptionOptions,
- this.telemetryService, this.utils);
- this.subscriptions[listener.entityDataSubscriptionKey] = subscription;
- subscription.addListener(listener);
- subscription.start();
- }
- }
-
- public unsubscribeFromDatasource(listener: EntityDataListener) {
- if (listener.entityDataSubscriptionKey) {
- const subscription = this.subscriptions[listener.entityDataSubscriptionKey];
- if (subscription) {
- subscription.removeListener(listener);
- if (!subscription.hasListeners()) {
- subscription.unsubscribe();
- delete this.subscriptions[listener.entityDataSubscriptionKey];
- }
- }
- listener.entityDataSubscriptionKey = null;
+ entityDataSubscriptionOptions.pageLink = pageLink;
+ entityDataSubscriptionOptions.keyFilters = keyFilters;
}
+ entityDataSubscriptionOptions.isLatestDataSubscription = isLatestDataSubscription;
+ return new EntityDataSubscription(entityDataSubscriptionOptions,
+ listener, this.telemetryService, this.utils);
}
}
diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts
index 5ab06505b7..c6a39e726b 100644
--- a/ui-ngx/src/app/core/api/widget-api.models.ts
+++ b/ui-ngx/src/app/core/api/widget-api.models.ts
@@ -98,7 +98,8 @@ export interface IAliasController {
getAliasInfo(aliasId: string): Observable;
getEntityAliasId(aliasName: string): string;
getInstantAliasInfo(aliasId: string): AliasInfo;
- resolveDatasources(datasources: Array): Observable>;
+ resolveSingleEntityInfo(aliasId: string): Observable;
+ resolveDatasources(datasources: Array, singleEntity?: boolean): Observable>;
resolveAlarmSource(alarmSource: Datasource): Observable;
getEntityAliases(): EntityAliases;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
@@ -202,8 +203,8 @@ export interface WidgetSubscriptionOptions {
alarmsMaxCountLoad?: number;
alarmsFetchSize?: number;
datasources?: Array;
- keyFilters?: Array;
- pageLink?: EntityDataPageLink;
+ hasDataPageLink?: boolean;
+ singleEntity?: boolean;
targetDeviceAliasIds?: Array;
targetDeviceIds?: Array;
useDashboardTimewindow?: boolean;
@@ -264,7 +265,7 @@ export interface IWidgetSubscription {
onAliasesChanged(aliasIds: Array): boolean;
- onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): boolean;
+ onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void;
updateDataVisibility(index: number): void;
@@ -278,6 +279,10 @@ export interface IWidgetSubscription {
subscribe(): void;
+ subscribeForLatestData(datasourceIndex: number,
+ pageLink: EntityDataPageLink,
+ keyFilters: KeyFilter[]): void;
+
isDataResolved(): boolean;
destroy(): void;
diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts
index 924f161d8f..e4cfee4f3e 100644
--- a/ui-ngx/src/app/core/api/widget-subscription.ts
+++ b/ui-ngx/src/app/core/api/widget-subscription.ts
@@ -22,7 +22,6 @@ import {
WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
import {
- DataKey,
DataSet,
DataSetHolder,
Datasource,
@@ -43,20 +42,18 @@ import {
toHistoryTimewindow,
WidgetTimewindow
} from '@app/shared/models/time/time.models';
-import { Observable, ReplaySubject, Subject, throwError } from 'rxjs';
+import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils';
import { AlarmSourceListener } from '@core/http/alarm.service';
-import { DatasourceListener } from '@core/api/datasource.service';
import { EntityId } from '@app/shared/models/id/entity-id';
-import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
-import { entityFields } from '@shared/models/entity.models';
import * as moment_ from 'moment';
import { PageData } from '@shared/models/page/page-data';
import { EntityDataListener } from '@core/api/entity-data.service';
-import { EntityData, EntityDataPageLink, EntityKeyType } from '@shared/models/query/query.models';
+import { EntityData, EntityDataPageLink, EntityKeyType, KeyFilter } from '@shared/models/query/query.models';
+import { map } from 'rxjs/operators';
const moment = moment_;
@@ -73,12 +70,14 @@ export class WidgetSubscription implements IWidgetSubscription {
subscriptionTimewindow: SubscriptionTimewindow;
useDashboardTimewindow: boolean;
+ hasDataPageLink: boolean;
+ singleEntity: boolean;
+
datasourcePages: PageData[];
dataPages: PageData>[];
entityDataListeners: Array;
configuredDatasources: Array;
- initDataSubscriptionSubject: Subject;
data: Array;
datasources: Array;
// datasourceListeners: Array;
@@ -211,6 +210,8 @@ export class WidgetSubscription implements IWidgetSubscription {
// this.datasources = this.ctx.utils.validateDatasources(options.datasources);
this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources);
this.entityDataListeners = [];
+ this.hasDataPageLink = options.hasDataPageLink;
+ this.singleEntity = options.singleEntity;
// this.datasourceListeners = [];
this.datasourcePages = [];
this.datasources = [];
@@ -271,11 +272,11 @@ export class WidgetSubscription implements IWidgetSubscription {
const initRpcSubject = new ReplaySubject();
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
- this.ctx.aliasController.getAliasInfo(this.targetDeviceAliasId).subscribe(
- (aliasInfo) => {
- if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType === EntityType.DEVICE) {
- this.targetDeviceId = aliasInfo.currentEntity.id;
- this.targetDeviceName = aliasInfo.currentEntity.name;
+ this.ctx.aliasController.resolveSingleEntityInfo(this.targetDeviceAliasId).subscribe(
+ (entityInfo) => {
+ if (entityInfo && entityInfo.entityType === EntityType.DEVICE) {
+ this.targetDeviceId = entityInfo.id;
+ this.targetDeviceName = entityInfo.name;
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
@@ -348,34 +349,72 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private initDataSubscription(): Observable {
- this.initDataSubscriptionSubject = new ReplaySubject(1);
+ const initDataSubscriptionSubject = new ReplaySubject(1);
this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) {
this.hasResolvedData = true;
- // this.configureData();
- // initDataSubscriptionSubject.next();
- // initDataSubscriptionSubject.complete();
- this.subscribe();
+ this.prepareDataSubscriptions().subscribe(
+ () => {
+ initDataSubscriptionSubject.next();
+ initDataSubscriptionSubject.complete();
+ }
+ );
} else {
- this.ctx.aliasController.resolveDatasources(this.configuredDatasources).subscribe(
+ this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe(
(datasources) => {
this.configuredDatasources = datasources;
- /* if (datasources && datasources.length) {
- this.hasResolvedData = true;
- }*/
- this.subscribe();
- // this.configureData();
- // initDataSubscriptionSubject.next();
- // initDataSubscriptionSubject.complete();
+ this.prepareDataSubscriptions().subscribe(
+ () => {
+ initDataSubscriptionSubject.next();
+ initDataSubscriptionSubject.complete();
+ }
+ );
},
(err) => {
this.notifyDataLoaded();
- this.initDataSubscriptionSubject.error(err);
+ initDataSubscriptionSubject.error(err);
}
);
}
});
- return this.initDataSubscriptionSubject.asObservable();
+ return initDataSubscriptionSubject.asObservable();
+ }
+
+ private prepareDataSubscriptions(): Observable {
+ if (this.hasDataPageLink) {
+ this.hasResolvedData = true;
+ return of(null);
+ }
+ const resolveResultObservables = this.configuredDatasources.map((datasource, index) => {
+ const listener: EntityDataListener = {
+ subscriptionType: this.type,
+ configDatasource: datasource,
+ configDatasourceIndex: index,
+ dataLoaded: (pageData, data1, datasourceIndex) => {
+ this.dataLoaded(pageData, data1, datasourceIndex, true)
+ },
+ dataUpdated: this.dataUpdated.bind(this),
+ updateRealtimeSubscription: () => {
+ this.subscriptionTimewindow = this.updateRealtimeSubscription();
+ return this.subscriptionTimewindow;
+ },
+ setRealtimeSubscription: (subscriptionTimewindow) => {
+ this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
+ }
+ };
+ this.entityDataListeners.push(listener);
+ return this.ctx.entityDataService.prepareSubscription(listener);
+ });
+ return forkJoin(resolveResultObservables).pipe(
+ map((resolveResults) => {
+ resolveResults.forEach((resolveResult) => {
+ this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, false);
+ });
+ this.configureLoadedData();
+ this.hasResolvedData = true;
+ this.notifyDataLoaded();
+ })
+ );
}
/* private initDataSubscriptionOld(): Observable {
@@ -592,13 +631,12 @@ export class WidgetSubscription implements IWidgetSubscription {
});
}
- onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow): boolean {
+ onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) {
if (this.type === widgetType.timeseries || this.type === widgetType.alarm) {
if (this.useDashboardTimewindow) {
if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
- // this.timeWindowConfig = deepClone(newDashboardTimewindow);
- // this.update();
- // TODO:
+ this.timeWindowConfig = deepClone(newDashboardTimewindow);
+ this.update();
return true;
}
}
@@ -785,8 +823,12 @@ export class WidgetSubscription implements IWidgetSubscription {
}
update() {
- this.unsubscribe();
- this.subscribe();
+ if (this.type === widgetType.rpc || this.type === widgetType.alarm) {
+ this.unsubscribe();
+ this.subscribe();
+ } else {
+ this.dataSubscribe();
+ }
}
subscribe(): void {
@@ -802,6 +844,29 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
+ subscribeForLatestData(datasourceIndex: number,
+ pageLink: EntityDataPageLink,
+ keyFilters: KeyFilter[]): void {
+ let entityDataListener = this.entityDataListeners[datasourceIndex];
+ if (entityDataListener) {
+ this.ctx.entityDataService.stopSubscription(entityDataListener);
+ }
+ const datasource = this.configuredDatasources[datasourceIndex];
+ if (datasource) {
+ entityDataListener = {
+ subscriptionType: this.type,
+ configDatasource: datasource,
+ configDatasourceIndex: datasourceIndex,
+ dataLoaded: (pageData, data1, datasourceIndex1) => {
+ this.dataLoaded(pageData, data1, datasourceIndex1, true)
+ },
+ dataUpdated: this.dataUpdated.bind(this)
+ };
+ this.entityDataListeners[datasourceIndex] = entityDataListener;
+ this.ctx.entityDataService.subscribeForLatestData(entityDataListener, pageLink, keyFilters);
+ }
+ }
+
private doSubscribe() {
if (this.type === widgetType.rpc) {
return;
@@ -809,6 +874,12 @@ export class WidgetSubscription implements IWidgetSubscription {
if (this.type === widgetType.alarm) {
this.alarmsSubscribe();
} else {
+ this.dataSubscribe();
+ }
+ }
+
+ private dataSubscribe() {
+ if (!this.hasDataPageLink) {
this.notifyDataLoading();
if (this.type === widgetType.timeseries && this.timeWindowConfig) {
this.updateRealtimeSubscription();
@@ -819,62 +890,10 @@ export class WidgetSubscription implements IWidgetSubscription {
this.onDataUpdated();
}
}
- // let index = 0;
const forceUpdate = !this.datasources.length;
- this.configuredDatasources.forEach((datasource, index) => {
- const listener: EntityDataListener = {
- subscriptionType: this.type,
- subscriptionTimewindow: this.subscriptionTimewindow,
- configDatasource: datasource,
- configDatasourceIndex: index,
- dataLoaded: this.dataLoaded.bind(this),
- dataUpdated: this.dataUpdated.bind(this),
- updateRealtimeSubscription: () => {
- this.subscriptionTimewindow = this.updateRealtimeSubscription();
- return this.subscriptionTimewindow;
- },
- setRealtimeSubscription: (subscriptionTimewindow) => {
- this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
- }
- };
-
- /*if (this.comparisonEnabled && datasource.isAdditional) {
- listener.subscriptionTimewindow = this.timewindowForComparison;
- listener.updateRealtimeSubscription = () => {
- this.subscriptionTimewindow = this.updateSubscriptionForComparison();
- return this.subscriptionTimewindow;
- };
- listener.setRealtimeSubscription = () => {
- this.updateSubscriptionForComparison();
- };
- }*/
-
-/* let entityFieldKey = false;
-
- for (let a = 0; a < datasource.dataKeys.length; a++) {
- if (datasource.dataKeys[a].type !== DataKeyType.entityField) {
- this.data[index + a].data = [];
- } else {
- entityFieldKey = true;
- }
- }
- index += datasource.dataKeys.length;*/
-
- this.entityDataListeners.push(listener);
- // this.datasourceListeners.push(listener);
-
- // if (datasource.dataKeys.length) {
- // this.ctx.datasourceService.subscribeToDatasource(listener);
- // }
-
- this.ctx.entityDataService.subscribeToEntityData(listener);
-
- /* if (datasource.unresolvedStateEntity || entityFieldKey ||
- !datasource.dataKeys.length ||
- (datasource.type === DatasourceType.entity && !datasource.entityId)
- ) {
- forceUpdate = true;
- }*/
+ this.entityDataListeners.forEach((listener) => {
+ listener.subscriptionTimewindow = this.subscriptionTimewindow;
+ this.ctx.entityDataService.startSubscription(listener);
});
if (forceUpdate) {
this.notifyDataLoaded();
@@ -1000,7 +1019,9 @@ export class WidgetSubscription implements IWidgetSubscription {
this.alarmsUnsubscribe();
} else {
this.entityDataListeners.forEach((listener) => {
- this.ctx.entityDataService.unsubscribeFromDatasource(listener);
+ if (listener != null) {
+ this.ctx.entityDataService.stopSubscription(listener);
+ }
});
this.entityDataListeners.length = 0;
this.resetData();
@@ -1129,7 +1150,9 @@ export class WidgetSubscription implements IWidgetSubscription {
return this.timewindowForComparison;
}
- private dataLoaded(pageData: PageData, data: Array>, datasourceIndex: number) {
+ private dataLoaded(pageData: PageData,
+ data: Array>,
+ datasourceIndex: number, isUpdate: boolean) {
const datasource = this.configuredDatasources[datasourceIndex];
datasource.dataReceived = true;
const datasources = pageData.data.map((entityData, index) =>
@@ -1152,14 +1175,8 @@ export class WidgetSubscription implements IWidgetSubscription {
totalPages: pageData.totalPages
};
this.dataPages[datasourceIndex] = datasourceDataPage;
- this.configureLoadedData();
- const readyCount = this.configuredDatasources.filter(d => d.dataReceived).length;
- if (this.configuredDatasources.length === readyCount) {
- this.hasResolvedData = true;
- this.initDataSubscriptionSubject.next();
- this.initDataSubscriptionSubject.complete();
+ if (isUpdate) {
this.configureLoadedData();
- this.notifyDataLoaded();
this.onDataUpdated(true);
}
}
@@ -1238,6 +1255,9 @@ export class WidgetSubscription implements IWidgetSubscription {
dataKey,
data: []
};
+ if (data && data[keyIndex] && data[keyIndex].data) {
+ datasourceData.data = data[keyIndex].data;
+ }
return datasourceData;
});
}
@@ -1275,6 +1295,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const startIndex = configuredDatasource.dataKeyStartIndex;
const dataKeysCount = configuredDatasource.dataKeys.length;
const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex;
+ this.notifyDataLoaded();
let update = true;
let currentData: DataSetHolder;
if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) {
diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts
index 076f132cb8..ff1e4e193d 100644
--- a/ui-ngx/src/app/core/http/entity.service.ts
+++ b/ui-ngx/src/app/core/http/entity.service.ts
@@ -54,9 +54,15 @@ import {
import { EntityRelationService } from '@core/http/entity-relation.service';
import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
import { Asset, AssetSearchQuery } from '@shared/models/asset.models';
-import { Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models';
+import { ClaimResult, Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models';
import { EntityViewSearchQuery } from '@shared/models/entity-view.models';
import { AttributeService } from '@core/http/attribute.service';
+import {
+ createDefaultEntityDataPageLink,
+ EntityData,
+ EntityDataQuery,
+ EntityFilter, EntityKeyType
+} from '@shared/models/query/query.models';
@Injectable({
providedIn: 'root'
@@ -360,6 +366,54 @@ export class EntityService {
}
}
+ public findEntityDataByQuery(query: EntityDataQuery, config?: RequestConfig): Observable> {
+ return this.http.post>('/api/entitiesQuery/find', query, defaultHttpOptionsFromConfig(config));
+ }
+
+ private entityDataToEntityInfo(entityData: EntityData): EntityInfo {
+ const entityInfo: EntityInfo = {
+ id: entityData.entityId.id,
+ entityType: entityData.entityId.entityType as EntityType
+ };
+ if (entityData.latest && entityData.latest[EntityKeyType.ENTITY_FIELD]) {
+ const fields = entityData.latest[EntityKeyType.ENTITY_FIELD];
+ if (fields.name) {
+ entityInfo.name = fields.name.value;
+ }
+ if (fields.label) {
+ entityInfo.label = fields.label.value;
+ }
+ }
+ return entityInfo;
+ }
+
+ public findSingleEntityInfoByEntityFilter(filter: EntityFilter, config?: RequestConfig): Observable {
+ const query: EntityDataQuery = {
+ entityFilter: filter,
+ pageLink: createDefaultEntityDataPageLink(1),
+ entityFields: [
+ {
+ type: EntityKeyType.ENTITY_FIELD,
+ key: 'name'
+ },
+ {
+ type: EntityKeyType.ENTITY_FIELD,
+ key: 'label'
+ }
+ ]
+ };
+ return this.findEntityDataByQuery(query, config).pipe(
+ map((data) => {
+ if (data.data.length) {
+ const entityData = data.data[0];
+ return this.entityDataToEntityInfo(entityData);
+ } else {
+ return null;
+ }
+ })
+ );
+ }
+
public getAliasFilterTypesByEntityTypes(entityTypes: Array): Array {
const allAliasFilterTypes: Array = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]);
if (!entityTypes || !entityTypes.length) {
@@ -605,7 +659,7 @@ export class EntityService {
public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable {
const filter = entityAlias.filter;
return this.resolveAliasFilter(filter, stateParams).pipe(
- map((result) => {
+ mergeMap((result) => {
const aliasInfo: AliasInfo = {
alias: entityAlias.alias,
entityFilter: result.entityFilter,
@@ -615,30 +669,19 @@ export class EntityService {
};
aliasInfo.resolvedEntities = result.entities;
aliasInfo.currentEntity = null;
- if (aliasInfo.resolvedEntities.length) {
- aliasInfo.currentEntity = aliasInfo.resolvedEntities[0];
+ if (!aliasInfo.resolveMultiple && aliasInfo.entityFilter) {
+ return this.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter,
+ {ignoreLoading: true, ignoreErrors: true}).pipe(
+ map((entity) => {
+ aliasInfo.currentEntity = entity;
+ return aliasInfo;
+ })
+ );
}
- return aliasInfo;
+ return of(aliasInfo);
})
);
}
-/*
- public resolveEntityFilter(filter: EntityAliasFilter, stateParams: StateParams): EntityFilter {
- const stateEntityInfo = this.getStateEntityInfo(filter, stateParams);
- let result: EntityFilter = filter;
- const stateEntityId = stateEntityInfo.entityId;
- if (filter.type === AliasFilterType.stateEntity) {
- result = {
- singleEntity: stateEntityId,
- type: AliasFilterType.singleEntity
- };
- } else if (filter.rootStateEntity) {
- let rootEntityType;
- let rootEntityId;
-
- }
- return result;
- }*/
public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable {
const result: EntityAliasFilterResult = {
diff --git a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
index 5d3daa9467..5610551d20 100644
--- a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
+++ b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
@@ -114,6 +114,17 @@ export class TelemetryWebsocketService implements TelemetryService {
this.publishCommands();
}
+ public update(subscriber: TelemetrySubscriber) {
+ subscriber.subscriptionCommands.forEach(
+ (subscriptionCommand) => {
+ if (subscriptionCommand.cmdId && subscriptionCommand instanceof EntityDataCmd) {
+ this.cmdsWrapper.entityDataCmds.push(subscriptionCommand);
+ }
+ }
+ );
+ this.publishCommands();
+ }
+
public unsubscribe(subscriber: TelemetrySubscriber) {
if (this.isActive) {
subscriber.subscriptionCommands.forEach(
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
index 6a87650526..a7f9d8e678 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
@@ -39,7 +39,7 @@
+ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
{{ column.title }}
= [];
@@ -150,8 +152,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private domSanitizer: DomSanitizer) {
super(store);
- const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
- this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
+ // const sortOrder: EntityDataSortOrder = sortOrderFromString(this.defaultSortOrder);
+ this.pageLink = {
+ page: 0,
+ pageSize: this.defaultPageSize,
+ textSearch: null
+ };
+ // new PageLink(this.defaultPageSize, 0, null, sortOrder);
}
ngOnInit(): void {
@@ -191,11 +198,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public onDataUpdated() {
this.ngZone.run(() => {
- this.entityDatasource.updateEntitiesData(this.subscription.data);
+ this.entityDatasource.dataUpdated(); // .updateEntitiesData(this.subscription.data);
this.ctx.detectChanges();
});
}
+ public pageLinkSortDirection(): SortDirection {
+ return entityDataPageLinkSortDirection(this.pageLink);
+ }
+
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
@@ -256,7 +267,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
name: 'entityName',
label: 'entityName',
def: 'entityName',
- title: entityNameColumnTitle
+ title: entityNameColumnTitle,
+ entityKey: {
+ key: 'name',
+ type: EntityKeyType.ENTITY_FIELD
+ }
} as EntityColumn
);
this.contentsInfo.entityName = {
@@ -273,7 +288,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
name: 'entityLabel',
label: 'entityLabel',
def: 'entityLabel',
- title: entityLabelColumnTitle
+ title: entityLabelColumnTitle,
+ entityKey: {
+ key: 'label',
+ type: EntityKeyType.ENTITY_FIELD
+ }
} as EntityColumn
);
this.contentsInfo.entityLabel = {
@@ -291,6 +310,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
label: 'entityType',
def: 'entityType',
title: this.translate.instant('entity.entity-type'),
+ entityKey: {
+ key: 'entityType',
+ type: EntityKeyType.ENTITY_FIELD
+ }
} as EntityColumn
);
this.contentsInfo.entityType = {
@@ -309,8 +332,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (datasource) {
datasource.dataKeys.forEach((entityDataKey) => {
const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn;
+ dataKey.entityKey = {
+ key: dataKey.name,
+ type: null
+ };
if (dataKey.type === DataKeyType.function) {
dataKey.name = dataKey.label;
+ dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD;
+ } else if (dataKey.type === DataKeyType.entityField) {
+ dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD;
+ } else if (dataKey.type === DataKeyType.attribute) {
+ dataKey.entityKey.type = EntityKeyType.ATTRIBUTE;
+ } else if (dataKey.type === DataKeyType.timeseries) {
+ dataKey.entityKey.type = EntityKeyType.TIME_SERIES;
}
dataKeys.push(dataKey);
@@ -331,14 +365,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder;
}
- this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
- this.sortOrderProperty = toEntityColumnDef(this.pageLink.sortOrder.property, this.columns);
+
+ this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns);
+ let sortColumn: EntityColumn;
+ if (this.pageLink.sortOrder) {
+ sortColumn = findColumnByEntityKey(this.pageLink.sortOrder.key, this.columns);
+ }
+ this.sortOrderProperty = sortColumn ? sortColumn.def : null;
if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions');
}
this.entityDatasource = new EntityDatasource(
- this.translate, dataKeys, this.subscription.datasources);
+ this.translate, dataKeys, this.subscription);
}
private editColumnsToDisplay($event: Event) {
@@ -416,9 +455,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} else {
this.pageLink.page = 0;
}
- this.pageLink.sortOrder.property = fromEntityColumnDef(this.sort.active, this.columns);
- this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
- this.entityDatasource.loadEntities(this.pageLink);
+ this.pageLink.sortOrder = {
+ key: findEntityKeyByColumnDef(this.sort.active, this.columns),
+ direction: Direction[this.sort.direction.toUpperCase()]
+ };
+ const keyFilters: KeyFilter[] = null; // TODO:
+ this.entityDatasource.loadEntities(this.pageLink, keyFilters);
this.ctx.detectChanges();
}
@@ -523,18 +565,19 @@ class EntityDatasource implements DataSource {
private entitiesSubject = new BehaviorSubject([]);
private pageDataSubject = new BehaviorSubject>(emptyPageData());
- private allEntities: Array = [];
- private allEntitiesSubject = new BehaviorSubject([]);
- private allEntities$: Observable> = this.allEntitiesSubject.asObservable();
+// private allEntities: Array = [];
+// private allEntitiesSubject = new BehaviorSubject([]);
+// private allEntities$: Observable> = this.allEntitiesSubject.asObservable();
private currentEntity: EntityData = null;
constructor(
private translate: TranslateService,
private dataKeys: Array,
- datasources: Array
+ private subscription: IWidgetSubscription
+ // datasources: Array
) {
-
+/*
for (const datasource of datasources) {
if (datasource.type === DatasourceType.entity && !datasource.entityId) {
continue;
@@ -558,7 +601,7 @@ class EntityDatasource implements DataSource {
});
this.allEntities.push(entity);
}
- this.allEntitiesSubject.next(this.allEntities);
+ this.allEntitiesSubject.next(this.allEntities);*/
}
connect(collectionViewer: CollectionViewer): Observable> {
@@ -570,18 +613,63 @@ class EntityDatasource implements DataSource {
this.pageDataSubject.complete();
}
- loadEntities(pageLink: PageLink) {
- this.fetchEntities(pageLink).pipe(
+ loadEntities(pageLink: EntityDataPageLink, keyFilters: KeyFilter[]) {
+ this.subscription.subscribeForLatestData(0, pageLink, keyFilters);
+/* this.fetchEntities(pageLink).pipe(
catchError(() => of(emptyPageData())),
).subscribe(
(pageData) => {
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
}
- );
+ );*/
}
- updateEntitiesData(data: DatasourceData[]) {
+ dataUpdated() {
+ const datasourcesPageData = this.subscription.datasourcePages[0];
+ const dataPageData = this.subscription.dataPages[0];
+ const entities = new Array();
+ datasourcesPageData.data.forEach((datasource, index) => {
+ entities.push(this.datasourceToEntityData(datasource, dataPageData.data[index]));
+ });
+ const entitiesPageData: PageData = {
+ data: entities,
+ totalPages: datasourcesPageData.totalPages,
+ totalElements: datasourcesPageData.totalElements,
+ hasNext: datasourcesPageData.hasNext
+ };
+ this.entitiesSubject.next(entities);
+ this.pageDataSubject.next(entitiesPageData);
+ }
+
+ private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData {
+ const entity: EntityData = {
+ id: {} as EntityId,
+ entityName: datasource.entityName,
+ entityLabel: datasource.entityLabel ? datasource.entityLabel : datasource.entityName
+ };
+ if (datasource.entityId) {
+ entity.id.id = datasource.entityId;
+ }
+ if (datasource.entityType) {
+ entity.id.entityType = datasource.entityType;
+ entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type);
+ } else {
+ entity.entityType = '';
+ }
+ this.dataKeys.forEach((dataKey, index) => {
+ const keyData = data[index].data;
+ if (keyData && keyData.length && keyData[0].length > 1) {
+ const value = keyData[0][1];
+ entity[dataKey.label] = value;
+ } else {
+ entity[dataKey.label] = '';
+ }
+ });
+ return entity;
+ }
+
+/* updateEntitiesData(data: DatasourceData[]) {
for (let i = 0; i < this.allEntities.length; i++) {
const entity = this.allEntities[i];
for (let a = 0; a < this.dataKeys.length; a++) {
@@ -597,7 +685,7 @@ class EntityDatasource implements DataSource {
}
}
this.allEntitiesSubject.next(this.allEntities);
- }
+ }*/
isEmpty(): Observable {
return this.entitiesSubject.pipe(
@@ -625,9 +713,9 @@ class EntityDatasource implements DataSource {
(this.currentEntity.id.id === entity.id.id);
}
- private fetchEntities(pageLink: PageLink): Observable> {
+ /* private fetchEntities(pageLink: PageLink): Observable> {
return this.allEntities$.pipe(
map((data) => pageLink.filterData(data))
);
- }
+ }*/
}
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 29f9e4265a..77acf524df 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
@@ -19,6 +19,7 @@ import { DataKey, WidgetConfig } from '@shared/models/widget.models';
import { getDescendantProp, isDefined } from '@core/utils';
import { alarmFields, AlarmInfo } from '@shared/models/alarm.models';
import * as tinycolor_ from 'tinycolor2';
+import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models';
const tinycolor = tinycolor_;
@@ -49,6 +50,7 @@ export interface EntityData {
export interface EntityColumn extends DataKey {
def: string;
title: string;
+ entityKey?: EntityKey;
}
export interface DisplayColumn {
@@ -73,6 +75,58 @@ export interface CellStyleInfo {
cellStyleFunction?: CellStyleFunction;
}
+
+export function entityDataSortOrderFromString(strSortOrder: string, columns: EntityColumn[]): EntityDataSortOrder {
+ if (!strSortOrder && !strSortOrder.length) {
+ return null;
+ }
+ let property: string;
+ let direction = Direction.ASC;
+ if (strSortOrder.startsWith('-')) {
+ direction = Direction.DESC;
+ property = strSortOrder.substring(1);
+ } else {
+ if (strSortOrder.startsWith('+')) {
+ property = strSortOrder.substring(1);
+ } else {
+ property = strSortOrder;
+ }
+ }
+ if (!property && !property.length) {
+ return null;
+ }
+ const column = findColumnByLabel(property, columns);
+ if (column && column.entityKey) {
+ return {key: column.entityKey, direction};
+ }
+ return null;
+}
+
+export function findColumnByEntityKey(key: EntityKey, columns: EntityColumn[]): EntityColumn {
+ if (key) {
+ return columns.find(theColumn => theColumn.entityKey &&
+ theColumn.entityKey.type === key.type && theColumn.entityKey.key === key.key);
+ } else {
+ return null;
+ }
+}
+
+export function findEntityKeyByColumnDef(def: string, columns: EntityColumn[]): EntityKey {
+ return findColumnByDef(def, columns).entityKey;
+}
+
+export function findColumn(searchProperty: string, searchValue: string, columns: EntityColumn[]): EntityColumn {
+ return columns.find(theColumn => theColumn[searchProperty] === searchValue);
+}
+
+export function findColumnByLabel(label: string, columns: EntityColumn[]): EntityColumn {
+ return findColumn('label', label, columns);
+}
+
+export function findColumnByDef(def: string, columns: EntityColumn[]): EntityColumn {
+ return findColumn('def', def, columns);
+}
+
export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string {
let res = searchValue;
const column = columns.find(theColumn => theColumn[searchProperty] === searchValue);
@@ -82,6 +136,10 @@ export function findColumnProperty(searchProperty: string, searchValue: string,
return res;
}
+export function toEntityKey(def: string, columns: EntityColumn[]): string {
+ return findColumnProperty('def', def, 'label', columns);
+}
+
export function toEntityColumnDef(label: string, columns: EntityColumn[]): string {
return findColumnProperty('label', label, 'def', columns);
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
index 3587201415..a9d7e332f4 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
@@ -346,12 +346,19 @@ export class WidgetComponentService {
} else {
result.typeParameters.useCustomDatasources = false;
}
+ if (isUndefined(result.typeParameters.hasDataPageLink)) {
+ result.typeParameters.hasDataPageLink = false;
+ }
if (isUndefined(result.typeParameters.maxDatasources)) {
result.typeParameters.maxDatasources = -1;
}
if (isUndefined(result.typeParameters.maxDataKeys)) {
result.typeParameters.maxDataKeys = -1;
}
+ if (isUndefined(result.typeParameters.singleEntity)) {
+ result.typeParameters.singleEntity = result.typeParameters.maxDatasources === 1 &&
+ result.typeParameters.maxDataKeys === 1;
+ }
if (isUndefined(result.typeParameters.dataKeysOptional)) {
result.typeParameters.dataKeysOptional = false;
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts
index 1091b4c550..800ccab847 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts
@@ -620,14 +620,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe(
(dashboardTimewindow) => {
- // TODO:
- let subscriptionChanged = false;
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
- subscriptionChanged = subscriptionChanged || subscription.onDashboardTimewindowChanged(dashboardTimewindow);
- }
- if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
- this.reInit();
+ subscription.onDashboardTimewindowChanged(dashboardTimewindow);
}
}
));
@@ -845,6 +840,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
options = {
type: this.widget.type,
stateData: this.typeParameters.stateData,
+ hasDataPageLink: this.typeParameters.hasDataPageLink,
+ singleEntity: this.typeParameters.singleEntity,
comparisonEnabled: comparisonSettings.comparisonEnabled,
timeForComparison: comparisonSettings.timeForComparison
};
diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts
index 304968841e..b2eaa52d53 100644
--- a/ui-ngx/src/app/shared/models/query/query.models.ts
+++ b/ui-ngx/src/app/shared/models/query/query.models.ts
@@ -16,6 +16,7 @@
import { AliasFilterType, EntityFilters } from '@shared/models/alias.models';
import { EntityId } from '@shared/models/id/entity-id';
+import { SortDirection } from '@angular/material/sort';
export enum EntityKeyType {
ATTRIBUTE = 'ATTRIBUTE',
@@ -122,18 +123,30 @@ export interface EntityDataPageLink {
sortOrder?: EntityDataSortOrder;
}
-export const defaultEntityDataPageLink: EntityDataPageLink = {
- pageSize: 1024,
- page: 0,
- sortOrder: {
- key: {
- type: EntityKeyType.ENTITY_FIELD,
- key: 'createdTime'
- },
- direction: Direction.DESC
+export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection {
+ if (pageLink.sortOrder) {
+ return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection;
+ } else {
+ return '' as SortDirection;
}
}
+export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPageLink {
+ return {
+ pageSize,
+ page: 0,
+ sortOrder: {
+ key: {
+ type: EntityKeyType.ENTITY_FIELD,
+ key: 'createdTime'
+ },
+ direction: Direction.DESC
+ }
+ }
+}
+
+export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1024);
+
export interface EntityCountQuery {
entityFilter: EntityFilter;
}
diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
index e3108004c8..c0ad2c80a0 100644
--- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
+++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
@@ -21,7 +21,7 @@ import { Observable, ReplaySubject, Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { map } from 'rxjs/operators';
import { NgZone } from '@angular/core';
-import { EntityData, EntityDataQuery } from '@shared/models/query/query.models';
+import { EntityData, EntityDataQuery, EntityKey } from '@shared/models/query/query.models';
import { PageData } from '@shared/models/page/page-data';
export enum DataKeyType {
@@ -139,7 +139,7 @@ export interface EntityHistoryCmd {
}
export interface LatestValueCmd {
- keys: Array;
+ keys: Array;
}
export interface TimeSeriesCmd {
@@ -153,7 +153,7 @@ export interface TimeSeriesCmd {
export class EntityDataCmd implements WebsocketCmd {
cmdId: number;
- query: EntityDataQuery;
+ query?: EntityDataQuery;
historyCmd?: EntityHistoryCmd;
latestCmd?: LatestValueCmd;
tsCmd?: TimeSeriesCmd;
@@ -314,6 +314,7 @@ export class EntityDataUpdate implements EntityDataUpdateMsg {
export interface TelemetryService {
subscribe(subscriber: TelemetrySubscriber);
+ update(subscriber: TelemetrySubscriber);
unsubscribe(subscriber: TelemetrySubscriber);
}
@@ -360,6 +361,10 @@ export class TelemetrySubscriber {
this.telemetryService.subscribe(this);
}
+ public update() {
+ this.telemetryService.update(this);
+ }
+
public unsubscribe() {
this.telemetryService.unsubscribe(this);
this.complete();
diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts
index 3b3c29f44d..3a6937974b 100644
--- a/ui-ngx/src/app/shared/models/widget.models.ts
+++ b/ui-ngx/src/app/shared/models/widget.models.ts
@@ -150,6 +150,8 @@ export interface WidgetTypeParameters {
maxDataKeys?: number;
dataKeysOptional?: boolean;
stateData?: boolean;
+ hasDataPageLink?: boolean;
+ singleEntity?: boolean;
}
export interface WidgetControllerDescriptor {