Commit f37ebb66aa39283a16fd80fae3bdb015292bfd8f

Authored by Igor Kulikov
1 parent 2aaa51fe

UI: Entities data query

@@ -22,22 +22,6 @@ @@ -22,22 +22,6 @@
22 } 22 }
23 }, 23 },
24 { 24 {
25 - "alias": "entities_table",  
26 - "name": "Entities table",  
27 - "descriptor": {  
28 - "type": "latest",  
29 - "sizeX": 7.5,  
30 - "sizeY": 6.5,  
31 - "resources": [],  
32 - "templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",  
33 - "templateCss": "",  
34 - "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",  
35 - "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}",  
36 - "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}",  
37 - "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;\"}]}]}"  
38 - }  
39 - },  
40 - {  
41 "alias": "html_card", 25 "alias": "html_card",
42 "name": "HTML Card", 26 "name": "HTML Card",
43 "descriptor": { 27 "descriptor": {
@@ -132,6 +116,22 @@ @@ -132,6 +116,22 @@
132 "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", 116 "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}",
133 "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 += \\\" <b>\\\"+ data['temperature'] +\\\" °C</b>\\\";\\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\":{}}" 117 "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 += \\\" <b>\\\"+ data['temperature'] +\\\" °C</b>\\\";\\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\":{}}"
134 } 118 }
  119 + },
  120 + {
  121 + "alias": "entities_table",
  122 + "name": "Entities table",
  123 + "descriptor": {
  124 + "type": "latest",
  125 + "sizeX": 7.5,
  126 + "sizeY": 6.5,
  127 + "resources": [],
  128 + "templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
  129 + "templateCss": "",
  130 + "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",
  131 + "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}",
  132 + "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}",
  133 + "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;\"}]}]}"
  134 + }
135 } 135 }
136 ] 136 ]
137 } 137 }
@@ -452,7 +452,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { @@ -452,7 +452,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
452 } 452 }
453 453
454 private String entityNameQuery(EntityNameFilter filter) { 454 private String entityNameQuery(EntityNameFilter filter) {
455 - return String.format("lower(e.search_text) like lower(concat(%s, '%%'))", filter.getEntityNameFilter()); 455 + return String.format("lower(e.search_text) like lower(concat('%s', '%%'))", filter.getEntityNameFilter());
456 } 456 }
457 457
458 private String typeQuery(EntityFilter filter) { 458 private String typeQuery(EntityFilter filter) {
@@ -48,6 +48,7 @@ public class EntityKeyMapping { @@ -48,6 +48,7 @@ public class EntityKeyMapping {
48 48
49 static { 49 static {
50 entityFieldColumnMap.put("createdTime", "id"); 50 entityFieldColumnMap.put("createdTime", "id");
  51 + entityFieldColumnMap.put("entityType", "entity_type");
51 entityFieldColumnMap.put("name", "name"); 52 entityFieldColumnMap.put("name", "name");
52 entityFieldColumnMap.put("type", "type"); 53 entityFieldColumnMap.put("type", "type");
53 entityFieldColumnMap.put("label", "label"); 54 entityFieldColumnMap.put("label", "label");
@@ -22,8 +22,8 @@ import { EntityService } from '@core/http/entity.service'; @@ -22,8 +22,8 @@ import { EntityService } from '@core/http/entity.service';
22 import { UtilsService } from '@core/services/utils.service'; 22 import { UtilsService } from '@core/services/utils.service';
23 import { AliasFilterType, EntityAliases } from '@shared/models/alias.models'; 23 import { AliasFilterType, EntityAliases } from '@shared/models/alias.models';
24 import { EntityInfo } from '@shared/models/entity.models'; 24 import { EntityInfo } from '@shared/models/entity.models';
25 -import { map } from 'rxjs/operators';  
26 -import { defaultEntityDataPageLink } from '@shared/models/query/query.models'; 25 +import { map, mergeMap } from 'rxjs/operators';
  26 +import { createDefaultEntityDataPageLink, defaultEntityDataPageLink } from '@shared/models/query/query.models';
27 27
28 export class AliasController implements IAliasController { 28 export class AliasController implements IAliasController {
29 29
@@ -169,7 +169,24 @@ export class AliasController implements IAliasController { @@ -169,7 +169,24 @@ export class AliasController implements IAliasController {
169 } 169 }
170 } 170 }
171 171
172 - private resolveDatasource(datasource: Datasource, isSingle?: boolean): Observable<Array<Datasource>> { 172 + resolveSingleEntityInfo(aliasId: string): Observable<EntityInfo> {
  173 + return this.getAliasInfo(aliasId).pipe(
  174 + mergeMap((aliasInfo) => {
  175 + if (aliasInfo.resolveMultiple) {
  176 + if (aliasInfo.entityFilter) {
  177 + return this.entityService.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter,
  178 + {ignoreLoading: true, ignoreErrors: true});
  179 + } else {
  180 + return of(null);
  181 + }
  182 + } else {
  183 + return of(aliasInfo.currentEntity);
  184 + }
  185 + })
  186 + );
  187 + }
  188 +
  189 + private resolveDatasource(datasource: Datasource, isSingle?: boolean): Observable<Datasource> {
173 if (datasource.type === DatasourceType.entity) { 190 if (datasource.type === DatasourceType.entity) {
174 if (datasource.entityAliasId) { 191 if (datasource.entityAliasId) {
175 return this.getAliasInfo(datasource.entityAliasId).pipe( 192 return this.getAliasInfo(datasource.entityAliasId).pipe(
@@ -200,14 +217,14 @@ export class AliasController implements IAliasController { @@ -200,14 +217,14 @@ export class AliasController implements IAliasController {
200 datasources.push(newDatasource); 217 datasources.push(newDatasource);
201 } 218 }
202 return datasources;*/ 219 return datasources;*/
203 - return [newDatasource]; 220 + return newDatasource;
204 } else { 221 } else {
205 if (aliasInfo.stateEntity) { 222 if (aliasInfo.stateEntity) {
206 newDatasource = deepClone(datasource); 223 newDatasource = deepClone(datasource);
207 newDatasource.unresolvedStateEntity = true; 224 newDatasource.unresolvedStateEntity = true;
208 - return [newDatasource]; 225 + return newDatasource;
209 } else { 226 } else {
210 - return []; 227 + return null;
211 // throw new Error('Unable to resolve datasource.'); 228 // throw new Error('Unable to resolve datasource.');
212 } 229 }
213 } 230 }
@@ -232,13 +249,13 @@ export class AliasController implements IAliasController { @@ -232,13 +249,13 @@ export class AliasController implements IAliasController {
232 entityType: entity.entityType 249 entityType: entity.entityType
233 } 250 }
234 }; 251 };
235 - return [datasource]; 252 + return datasource;
236 } else { 253 } else {
237 if (aliasInfo.stateEntity) { 254 if (aliasInfo.stateEntity) {
238 datasource.unresolvedStateEntity = true; 255 datasource.unresolvedStateEntity = true;
239 - return [datasource]; 256 + return datasource;
240 } else { 257 } else {
241 - return []; 258 + return null;
242 // throw new Error('Unable to resolve datasource.'); 259 // throw new Error('Unable to resolve datasource.');
243 } 260 }
244 } 261 }
@@ -248,10 +265,10 @@ export class AliasController implements IAliasController { @@ -248,10 +265,10 @@ export class AliasController implements IAliasController {
248 } else { 265 } else {
249 datasource.aliasName = datasource.entityName; 266 datasource.aliasName = datasource.entityName;
250 datasource.name = datasource.entityName; 267 datasource.name = datasource.entityName;
251 - return of([datasource]); 268 + return of(datasource);
252 } 269 }
253 } else { 270 } else {
254 - return of([datasource]); 271 + return of(datasource);
255 } 272 }
256 } 273 }
257 274
@@ -354,18 +371,14 @@ export class AliasController implements IAliasController { @@ -354,18 +371,14 @@ export class AliasController implements IAliasController {
354 ); 371 );
355 } 372 }
356 373
357 - resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {  
358 - const newDatasources = deepClone(datasources);  
359 - const observables = new Array<Observable<Array<Datasource>>>(); 374 + resolveDatasources(datasources: Array<Datasource>, singleEntity?: boolean): Observable<Array<Datasource>> {
  375 + const newDatasources = deepClone(singleEntity ? [datasources[0]] : datasources);
  376 + const observables = new Array<Observable<Datasource>>();
360 newDatasources.forEach((datasource) => { 377 newDatasources.forEach((datasource) => {
361 observables.push(this.resolveDatasource(datasource)); 378 observables.push(this.resolveDatasource(datasource));
362 }); 379 });
363 return forkJoin(observables).pipe( 380 return forkJoin(observables).pipe(
364 - map((arrayOfDatasources) => {  
365 - const result = new Array<Datasource>();  
366 - arrayOfDatasources.forEach((datasourcesArray) => {  
367 - result.push(...datasourcesArray);  
368 - }); 381 + map((result) => {
369 let functionIndex = 0; 382 let functionIndex = 0;
370 result.forEach((datasource) => { 383 result.forEach((datasource) => {
371 if (datasource.type === DatasourceType.function) { 384 if (datasource.type === DatasourceType.function) {
@@ -386,6 +399,9 @@ export class AliasController implements IAliasController { @@ -386,6 +399,9 @@ export class AliasController implements IAliasController {
386 datasource.name = 'Unresolved'; 399 datasource.name = 'Unresolved';
387 datasource.entityName = 'Unresolved'; 400 datasource.entityName = 'Unresolved';
388 } else if (datasource.type === DatasourceType.entity) { 401 } else if (datasource.type === DatasourceType.entity) {
  402 + if (singleEntity) {
  403 + datasource.pageLink = createDefaultEntityDataPageLink(1);
  404 + }
389 if (!datasource.pageLink) { 405 if (!datasource.pageLink) {
390 datasource.pageLink = deepClone(defaultEntityDataPageLink); 406 datasource.pageLink = deepClone(defaultEntityDataPageLink);
391 } 407 }
@@ -35,19 +35,21 @@ import { @@ -35,19 +35,21 @@ import {
35 TelemetrySubscriber 35 TelemetrySubscriber
36 } from '@shared/models/telemetry/telemetry.models'; 36 } from '@shared/models/telemetry/telemetry.models';
37 import { UtilsService } from '@core/services/utils.service'; 37 import { UtilsService } from '@core/services/utils.service';
38 -import { EntityDataListener } from '@core/api/entity-data.service'; 38 +import { EntityDataListener, EntityDataLoadResult } from '@core/api/entity-data.service';
39 import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils'; 39 import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils';
40 import { PageData } from '@shared/models/page/page-data'; 40 import { PageData } from '@shared/models/page/page-data';
41 import { DataAggregator } from '@core/api/data-aggregator'; 41 import { DataAggregator } from '@core/api/data-aggregator';
42 import { NULL_UUID } from '@shared/models/id/has-uuid'; 42 import { NULL_UUID } from '@shared/models/id/has-uuid';
43 import { EntityType } from '@shared/models/entity-type.models'; 43 import { EntityType } from '@shared/models/entity-type.models';
44 import Timeout = NodeJS.Timeout; 44 import Timeout = NodeJS.Timeout;
  45 +import { Observable, of, ReplaySubject, Subject } from 'rxjs';
45 46
46 export interface EntityDataSubscriptionOptions { 47 export interface EntityDataSubscriptionOptions {
47 datasourceType: DatasourceType; 48 datasourceType: DatasourceType;
48 dataKeys: Array<SubscriptionDataKey>; 49 dataKeys: Array<SubscriptionDataKey>;
49 type: widgetType; 50 type: widgetType;
50 entityFilter?: EntityFilter; 51 entityFilter?: EntityFilter;
  52 + isLatestDataSubscription?: boolean;
51 pageLink?: EntityDataPageLink; 53 pageLink?: EntityDataPageLink;
52 keyFilters?: Array<KeyFilter>; 54 keyFilters?: Array<KeyFilter>;
53 subscriptionTimewindow?: SubscriptionTimewindow; 55 subscriptionTimewindow?: SubscriptionTimewindow;
@@ -59,21 +61,19 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyInd @@ -59,21 +61,19 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyInd
59 61
60 export class EntityDataSubscription { 62 export class EntityDataSubscription {
61 63
62 - private listeners: Array<EntityDataListener> = [];  
63 private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType; 64 private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType;
64 -  
65 - private history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&  
66 - isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);  
67 -  
68 - private realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&  
69 - isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); 65 + private history: boolean;
  66 + private realtime: boolean;
70 67
71 private subscriber: TelemetrySubscriber; 68 private subscriber: TelemetrySubscriber;
  69 + private dataCommand: EntityDataCmd;
  70 + private subsCommand: EntityDataCmd;
72 71
73 private attrFields: Array<EntityKey>; 72 private attrFields: Array<EntityKey>;
74 private tsFields: Array<EntityKey>; 73 private tsFields: Array<EntityKey>;
75 private latestValues: Array<EntityKey>; 74 private latestValues: Array<EntityKey>;
76 75
  76 + private entityDataResolveSubject: Subject<EntityDataLoadResult>;
77 private pageData: PageData<EntityData>; 77 private pageData: PageData<EntityData>;
78 private subsTw: SubscriptionTimewindow; 78 private subsTw: SubscriptionTimewindow;
79 private dataAggregators: Array<DataAggregator>; 79 private dataAggregators: Array<DataAggregator>;
@@ -87,7 +87,11 @@ export class EntityDataSubscription { @@ -87,7 +87,11 @@ export class EntityDataSubscription {
87 private tickElapsed = 0; 87 private tickElapsed = 0;
88 private timer: Timeout; 88 private timer: Timeout;
89 89
90 - constructor(private entityDataSubscriptionOptions: EntityDataSubscriptionOptions, 90 + private dataResolved = false;
  91 + private started = false;
  92 +
  93 + constructor(public entityDataSubscriptionOptions: EntityDataSubscriptionOptions,
  94 + private listener: EntityDataListener,
91 private telemetryService: TelemetryService, 95 private telemetryService: TelemetryService,
92 private utils: UtilsService) { 96 private utils: UtilsService) {
93 this.initializeSubscription(); 97 this.initializeSubscription();
@@ -126,50 +130,6 @@ export class EntityDataSubscription { @@ -126,50 +130,6 @@ export class EntityDataSubscription {
126 } 130 }
127 dataKey.key = key; 131 dataKey.key = key;
128 } 132 }
129 - if (this.datasourceType === DatasourceType.function) {  
130 - this.frequency = 1000;  
131 - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {  
132 - this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);  
133 - }  
134 - }  
135 - }  
136 -  
137 - public addListener(listener: EntityDataListener) {  
138 - this.listeners.push(listener);  
139 - }  
140 -  
141 - public hasListeners(): boolean {  
142 - return this.listeners.length > 0;  
143 - }  
144 -  
145 - public removeListener(listener: EntityDataListener) {  
146 - this.listeners.splice(this.listeners.indexOf(listener), 1);  
147 - }  
148 -  
149 - public syncListener(listener: EntityDataListener) {  
150 - if (this.pageData) {  
151 - let key: string;  
152 - let dataKey: SubscriptionDataKey;  
153 - const data: Array<Array<DataSetHolder>> = [];  
154 - for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {  
155 - data[dataIndex] = [];  
156 - for (key of Object.keys(this.dataKeys)) {  
157 - if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {  
158 - const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;  
159 - for (let i = 0; i < dataKeysList.length; i++) {  
160 - dataKey = dataKeysList[i];  
161 - const datasourceKey = `${key}_${i}`;  
162 - data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][datasourceKey];  
163 - }  
164 - } else {  
165 - dataKey = this.dataKeys[key] as SubscriptionDataKey;  
166 - data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][key];  
167 - }  
168 - }  
169 - }  
170 - listener.dataLoaded(this.pageData, data, listener.configDatasourceIndex);  
171 - }  
172 - this.listeners.push(listener);  
173 } 133 }
174 134
175 public unsubscribe() { 135 public unsubscribe() {
@@ -192,19 +152,30 @@ export class EntityDataSubscription { @@ -192,19 +152,30 @@ export class EntityDataSubscription {
192 this.pageData = null; 152 this.pageData = null;
193 } 153 }
194 154
195 - public start() {  
196 - this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; 155 + public subscribe(): Observable<EntityDataLoadResult> {
  156 + if (!this.entityDataSubscriptionOptions.isLatestDataSubscription) {
  157 + this.entityDataResolveSubject = new ReplaySubject(1);
  158 + } else {
  159 + this.started = true;
  160 + this.dataResolved = true;
  161 + }
197 if (this.datasourceType === DatasourceType.entity) { 162 if (this.datasourceType === DatasourceType.entity) {
198 const entityFields: Array<EntityKey> = 163 const entityFields: Array<EntityKey> =
199 this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map( 164 this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
200 - dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })  
201 - ); 165 + dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
  166 + );
202 if (!entityFields.find(key => key.key === 'name')) { 167 if (!entityFields.find(key => key.key === 'name')) {
203 entityFields.push({ 168 entityFields.push({
204 type: EntityKeyType.ENTITY_FIELD, 169 type: EntityKeyType.ENTITY_FIELD,
205 key: 'name' 170 key: 'name'
206 }); 171 });
207 } 172 }
  173 + if (!entityFields.find(key => key.key === 'label')) {
  174 + entityFields.push({
  175 + type: EntityKeyType.ENTITY_FIELD,
  176 + key: 'label'
  177 + });
  178 + }
208 179
209 this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( 180 this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
210 dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) 181 dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
@@ -217,9 +188,9 @@ export class EntityDataSubscription { @@ -217,9 +188,9 @@ export class EntityDataSubscription {
217 this.latestValues = this.attrFields.concat(this.tsFields); 188 this.latestValues = this.attrFields.concat(this.tsFields);
218 189
219 this.subscriber = new TelemetrySubscriber(this.telemetryService); 190 this.subscriber = new TelemetrySubscriber(this.telemetryService);
220 - const command = new EntityDataCmd(); 191 + this.dataCommand = new EntityDataCmd();
221 192
222 - command.query = { 193 + this.dataCommand.query = {
223 entityFilter: this.entityDataSubscriptionOptions.entityFilter, 194 entityFilter: this.entityDataSubscriptionOptions.entityFilter,
224 pageLink: this.entityDataSubscriptionOptions.pageLink, 195 pageLink: this.entityDataSubscriptionOptions.pageLink,
225 keyFilters: this.entityDataSubscriptionOptions.keyFilters, 196 keyFilters: this.entityDataSubscriptionOptions.keyFilters,
@@ -227,72 +198,17 @@ export class EntityDataSubscription { @@ -227,72 +198,17 @@ export class EntityDataSubscription {
227 latestValues: this.latestValues 198 latestValues: this.latestValues
228 }; 199 };
229 200
230 - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {  
231 - if (this.tsFields.length > 0) {  
232 - if (this.history) {  
233 - command.historyCmd = {  
234 - keys: this.tsFields.map(key => key.key),  
235 - startTs: this.subsTw.fixedWindow.startTimeMs,  
236 - endTs: this.subsTw.fixedWindow.endTimeMs,  
237 - interval: this.subsTw.aggregation.interval,  
238 - limit: this.subsTw.aggregation.limit,  
239 - agg: this.subsTw.aggregation.type 201 + if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
  202 + if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  203 + if (this.latestValues.length > 0) {
  204 + this.dataCommand.latestCmd = {
  205 + keys: this.latestValues
240 }; 206 };
241 - if (this.subsTw.aggregation.stateData) {  
242 - command.historyCmd.startTs -= YEAR;  
243 - }  
244 - } else {  
245 - command.tsCmd = {  
246 - keys: this.tsFields.map(key => key.key),  
247 - startTs: this.subsTw.startTs,  
248 - timeWindow: this.subsTw.aggregation.timeWindow,  
249 - interval: this.subsTw.aggregation.interval,  
250 - limit: this.subsTw.aggregation.limit,  
251 - agg: this.subsTw.aggregation.type  
252 - }  
253 - if (this.subsTw.aggregation.stateData) {  
254 - command.historyCmd = {  
255 - keys: this.tsFields.map(key => key.key),  
256 - startTs: this.subsTw.startTs - YEAR,  
257 - endTs: this.subsTw.startTs,  
258 - interval: this.subsTw.aggregation.interval,  
259 - limit: this.subsTw.aggregation.limit,  
260 - agg: this.subsTw.aggregation.type  
261 - };  
262 - }  
263 - this.subscriber.reconnect$.subscribe(() => {  
264 - let newSubsTw: SubscriptionTimewindow = null;  
265 - this.listeners.forEach((listener) => {  
266 - if (!newSubsTw) {  
267 - newSubsTw = listener.updateRealtimeSubscription();  
268 - } else {  
269 - listener.setRealtimeSubscription(newSubsTw);  
270 - }  
271 - });  
272 - this.subsTw = newSubsTw;  
273 - command.tsCmd.startTs = this.subsTw.startTs;  
274 - command.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;  
275 - command.tsCmd.interval = this.subsTw.aggregation.interval;  
276 - command.tsCmd.limit = this.subsTw.aggregation.limit;  
277 - command.tsCmd.agg = this.subsTw.aggregation.type;  
278 - if (this.subsTw.aggregation.stateData) {  
279 - command.historyCmd.startTs = this.subsTw.startTs - YEAR;  
280 - command.historyCmd.endTs = this.subsTw.startTs;  
281 - command.historyCmd.interval = this.subsTw.aggregation.interval;  
282 - command.historyCmd.limit = this.subsTw.aggregation.limit;  
283 - command.historyCmd.agg = this.subsTw.aggregation.type;  
284 - }  
285 - });  
286 } 207 }
287 } 208 }
288 - } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {  
289 - if (this.latestValues.length > 0) {  
290 - command.latestCmd = {  
291 - keys: this.latestValues.map(key => key.key)  
292 - };  
293 - }  
294 } 209 }
295 - this.subscriber.subscriptionCommands.push(command); 210 +
  211 + this.subscriber.subscriptionCommands.push(this.dataCommand);
296 212
297 this.subscriber.entityData$.subscribe( 213 this.subscriber.entityData$.subscribe(
298 (entityDataUpdate) => { 214 (entityDataUpdate) => {
@@ -304,6 +220,30 @@ export class EntityDataSubscription { @@ -304,6 +220,30 @@ export class EntityDataSubscription {
304 } 220 }
305 ); 221 );
306 222
  223 + this.subscriber.reconnect$.subscribe(() => {
  224 + const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription();
  225 + this.listener.setRealtimeSubscription(newSubsTw);
  226 + this.subsTw = newSubsTw;
  227 + if (this.started && !this.entityDataSubscriptionOptions.isLatestDataSubscription) {
  228 + this.subsCommand.tsCmd.startTs = this.subsTw.startTs;
  229 + this.subsCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
  230 + this.subsCommand.tsCmd.interval = this.subsTw.aggregation.interval;
  231 + this.subsCommand.tsCmd.limit = this.subsTw.aggregation.limit;
  232 + this.subsCommand.tsCmd.agg = this.subsTw.aggregation.type;
  233 + if (this.subsTw.aggregation.stateData) {
  234 + this.subsCommand.historyCmd.startTs = this.subsTw.startTs - YEAR;
  235 + this.subsCommand.historyCmd.endTs = this.subsTw.startTs;
  236 + this.subsCommand.historyCmd.interval = this.subsTw.aggregation.interval;
  237 + this.subsCommand.historyCmd.limit = this.subsTw.aggregation.limit;
  238 + this.subsCommand.historyCmd.agg = this.subsTw.aggregation.type;
  239 + }
  240 + this.subsCommand.query = this.dataCommand.query;
  241 + this.subscriber.subscriptionCommands = [this.subsCommand];
  242 + } else {
  243 + this.subscriber.subscriptionCommands = [this.dataCommand];
  244 + }
  245 + });
  246 +
307 this.subscriber.subscribe(); 247 this.subscriber.subscribe();
308 } else if (this.datasourceType === DatasourceType.function) { 248 } else if (this.datasourceType === DatasourceType.function) {
309 const entityData: EntityData = { 249 const entityData: EntityData = {
@@ -325,29 +265,46 @@ export class EntityDataSubscription { @@ -325,29 +265,46 @@ export class EntityDataSubscription {
325 totalPages: 1 265 totalPages: 1
326 }; 266 };
327 this.onPageData(pageData); 267 this.onPageData(pageData);
328 - this.tickScheduledTime = this.utils.currentPerfTime();  
329 - if (this.history) {  
330 - this.onTick(true);  
331 - } else {  
332 - this.timer = setTimeout(this.onTick.bind(this, true), 0); 268 + if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
  269 + if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  270 + this.frequency = 1000;
  271 + this.timer = setTimeout(this.onTick.bind(this, true), 0);
  272 + }
333 } 273 }
334 } 274 }
  275 + if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
  276 + return of(null);
  277 + } else {
  278 + return this.entityDataResolveSubject.asObservable();
  279 + }
335 } 280 }
336 281
337 - private onPageData(pageData: PageData<EntityData>) { 282 + public start() {
  283 + if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
  284 + return;
  285 + }
  286 + this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  287 + this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
  288 + isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
  289 + this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
  290 + isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
  291 +
  292 + if (this.timer) {
  293 + clearTimeout(this.timer);
  294 + this.timer = null;
  295 + }
  296 +
338 if (this.dataAggregators) { 297 if (this.dataAggregators) {
339 this.dataAggregators.forEach((aggregator) => { 298 this.dataAggregators.forEach((aggregator) => {
340 aggregator.destroy(); 299 aggregator.destroy();
341 }) 300 })
342 - this.dataAggregators = null;  
343 } 301 }
344 - this.datasourceData = [];  
345 this.dataAggregators = []; 302 this.dataAggregators = [];
346 - this.entityIdToDataIndex = {};  
347 - let tsKeyNames; 303 + this.resetData();
  304 +
348 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { 305 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  306 + let tsKeyNames = [];
349 if (this.datasourceType === DatasourceType.function) { 307 if (this.datasourceType === DatasourceType.function) {
350 - tsKeyNames = [];  
351 for (const key of Object.keys(this.dataKeys)) { 308 for (const key of Object.keys(this.dataKeys)) {
352 const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>; 309 const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
353 dataKeysList.forEach((subscriptionDataKey) => { 310 dataKeysList.forEach((subscriptionDataKey) => {
@@ -357,20 +314,85 @@ export class EntityDataSubscription { @@ -357,20 +314,85 @@ export class EntityDataSubscription {
357 } else { 314 } else {
358 tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : []; 315 tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : [];
359 } 316 }
360 - }  
361 - for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {  
362 - const entityData = pageData.data[dataIndex];  
363 - this.entityIdToDataIndex[entityData.entityId.id] = dataIndex;  
364 - this.datasourceData[dataIndex] = {};  
365 - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { 317 + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
366 if (this.datasourceType === DatasourceType.function) { 318 if (this.datasourceType === DatasourceType.function) {
367 this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, 319 this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
368 - DataKeyType.function, dataIndex, this.notifyListeners.bind(this)); 320 + DataKeyType.function, dataIndex, this.notifyListener.bind(this));
369 } else if (!this.history && tsKeyNames.length) { 321 } else if (!this.history && tsKeyNames.length) {
370 this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, 322 this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
371 - DataKeyType.timeseries, dataIndex, this.notifyListeners.bind(this)); 323 + DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
372 } 324 }
373 } 325 }
  326 + }
  327 + if (this.datasourceType === DatasourceType.entity) {
  328 + this.subsCommand = new EntityDataCmd();
  329 + this.subsCommand.cmdId = this.dataCommand.cmdId;
  330 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  331 + if (this.tsFields.length > 0) {
  332 + if (this.history) {
  333 + this.subsCommand.historyCmd = {
  334 + keys: this.tsFields.map(key => key.key),
  335 + startTs: this.subsTw.fixedWindow.startTimeMs,
  336 + endTs: this.subsTw.fixedWindow.endTimeMs,
  337 + interval: this.subsTw.aggregation.interval,
  338 + limit: this.subsTw.aggregation.limit,
  339 + agg: this.subsTw.aggregation.type
  340 + };
  341 + if (this.subsTw.aggregation.stateData) {
  342 + this.subsCommand.historyCmd.startTs -= YEAR;
  343 + }
  344 + } else {
  345 + this.subsCommand.tsCmd = {
  346 + keys: this.tsFields.map(key => key.key),
  347 + startTs: this.subsTw.startTs,
  348 + timeWindow: this.subsTw.aggregation.timeWindow,
  349 + interval: this.subsTw.aggregation.interval,
  350 + limit: this.subsTw.aggregation.limit,
  351 + agg: this.subsTw.aggregation.type
  352 + }
  353 + if (this.subsTw.aggregation.stateData) {
  354 + this.subsCommand.historyCmd = {
  355 + keys: this.tsFields.map(key => key.key),
  356 + startTs: this.subsTw.startTs - YEAR,
  357 + endTs: this.subsTw.startTs,
  358 + interval: this.subsTw.aggregation.interval,
  359 + limit: this.subsTw.aggregation.limit,
  360 + agg: this.subsTw.aggregation.type
  361 + };
  362 + }
  363 + }
  364 + }
  365 + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  366 + if (this.latestValues.length > 0) {
  367 + this.subsCommand.latestCmd = {
  368 + keys: this.latestValues
  369 + };
  370 + }
  371 + }
  372 + this.subscriber.subscriptionCommands = [this.subsCommand];
  373 + this.subscriber.update();
  374 + } else if (this.datasourceType === DatasourceType.function) {
  375 + this.frequency = 1000;
  376 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  377 + this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
  378 + }
  379 + this.tickScheduledTime = this.utils.currentPerfTime();
  380 + if (this.history) {
  381 + this.onTick(true);
  382 + } else {
  383 + this.timer = setTimeout(this.onTick.bind(this, true), 0);
  384 + }
  385 + }
  386 + this.started = true;
  387 + }
  388 +
  389 + private resetData() {
  390 + this.datasourceData = [];
  391 + this.entityIdToDataIndex = {};
  392 + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
  393 + const entityData = this.pageData.data[dataIndex];
  394 + this.entityIdToDataIndex[entityData.entityId.id] = dataIndex;
  395 + this.datasourceData[dataIndex] = {};
374 for (const key of Object.keys(this.dataKeys)) { 396 for (const key of Object.keys(this.dataKeys)) {
375 const dataKey = this.dataKeys[key]; 397 const dataKey = this.dataKeys[key];
376 if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { 398 if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
@@ -388,7 +410,23 @@ export class EntityDataSubscription { @@ -388,7 +410,23 @@ export class EntityDataSubscription {
388 } 410 }
389 } 411 }
390 this.datasourceOrigData = deepClone(this.datasourceData); 412 this.datasourceOrigData = deepClone(this.datasourceData);
  413 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  414 + for (const key of Object.keys(this.dataKeys)) {
  415 + const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
  416 + dataKeyList.forEach((dataKey) => {
  417 + delete dataKey.lastUpdateTime;
  418 + });
  419 + }
  420 + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  421 + for (const key of Object.keys(this.dataKeys)) {
  422 + delete (this.dataKeys[key] as SubscriptionDataKey).lastUpdateTime;
  423 + }
  424 + }
  425 + }
391 426
  427 + private onPageData(pageData: PageData<EntityData>) {
  428 + this.pageData = pageData;
  429 + this.resetData();
392 const data: Array<Array<DataSetHolder>> = []; 430 const data: Array<Array<DataSetHolder>> = [];
393 for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { 431 for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
394 const entityData = pageData.data[dataIndex]; 432 const entityData = pageData.data[dataIndex];
@@ -401,28 +439,33 @@ export class EntityDataSubscription { @@ -401,28 +439,33 @@ export class EntityDataSubscription {
401 } 439 }
402 ); 440 );
403 } 441 }
404 -  
405 - this.pageData = pageData;  
406 -  
407 - this.listeners.forEach((listener) => {  
408 - listener.dataLoaded(pageData, data,  
409 - listener.configDatasourceIndex);  
410 - }); 442 + if (!this.dataResolved) {
  443 + this.dataResolved = true;
  444 + this.entityDataResolveSubject.next(
  445 + {
  446 + pageData,
  447 + data,
  448 + datasourceIndex: this.listener.configDatasourceIndex
  449 + }
  450 + );
  451 + this.entityDataResolveSubject.complete();
  452 + } else {
  453 + this.listener.dataLoaded(pageData, data,
  454 + this.listener.configDatasourceIndex);
  455 + }
411 } 456 }
412 457
413 private onDataUpdate(update: Array<EntityData>) { 458 private onDataUpdate(update: Array<EntityData>) {
414 for (const entityData of update) { 459 for (const entityData of update) {
415 const dataIndex = this.entityIdToDataIndex[entityData.entityId.id]; 460 const dataIndex = this.entityIdToDataIndex[entityData.entityId.id];
416 - this.processEntityData(entityData, dataIndex, true, this.notifyListeners.bind(this)); 461 + this.processEntityData(entityData, dataIndex, true, this.notifyListener.bind(this));
417 } 462 }
418 } 463 }
419 464
420 - private notifyListeners(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {  
421 - this.listeners.forEach((listener) => {  
422 - listener.dataUpdated(data,  
423 - listener.configDatasourceIndex, 465 + private notifyListener(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
  466 + this.listener.dataUpdated(data,
  467 + this.listener.configDatasourceIndex,
424 dataIndex, dataKeyIndex, detectChanges); 468 dataIndex, dataKeyIndex, detectChanges);
425 - });  
426 } 469 }
427 470
428 private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean, 471 private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean,
@@ -596,14 +639,10 @@ export class EntityDataSubscription { @@ -596,14 +639,10 @@ export class EntityDataSubscription {
596 const value = dataKey.func(time, prevSeries[1]); 639 const value = dataKey.func(time, prevSeries[1]);
597 const series: [number, any] = [time, value]; 640 const series: [number, any] = [time, value];
598 this.datasourceData[0][dataKey.key].data = [series]; 641 this.datasourceData[0][dataKey.key].data = [series];
599 - this.listeners.forEach(  
600 - (listener) => {  
601 - listener.dataUpdated(this.datasourceData[0][dataKey.key],  
602 - listener.configDatasourceIndex,  
603 - 0,  
604 - dataKey.index, detectChanges);  
605 - }  
606 - ); 642 + this.listener.dataUpdated(this.datasourceData[0][dataKey.key],
  643 + this.listener.configDatasourceIndex,
  644 + 0,
  645 + dataKey.index, detectChanges);
607 } 646 }
608 647
609 private onTick(detectChanges: boolean) { 648 private onTick(detectChanges: boolean) {
@@ -24,17 +24,24 @@ import { UtilsService } from '@core/services/utils.service'; @@ -24,17 +24,24 @@ import { UtilsService } from '@core/services/utils.service';
24 import { SubscriptionDataKey } from '@core/api/datasource-subcription'; 24 import { SubscriptionDataKey } from '@core/api/datasource-subcription';
25 import { deepClone, objectHashCode } from '@core/utils'; 25 import { deepClone, objectHashCode } from '@core/utils';
26 import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription'; 26 import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription';
  27 +import { Observable, of } from 'rxjs';
27 28
28 export interface EntityDataListener { 29 export interface EntityDataListener {
29 subscriptionType: widgetType; 30 subscriptionType: widgetType;
30 - subscriptionTimewindow: SubscriptionTimewindow; 31 + subscriptionTimewindow?: SubscriptionTimewindow;
31 configDatasource: Datasource; 32 configDatasource: Datasource;
32 configDatasourceIndex: number; 33 configDatasourceIndex: number;
33 dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void; 34 dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void;
34 dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void; 35 dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
35 - updateRealtimeSubscription: () => SubscriptionTimewindow;  
36 - setRealtimeSubscription: (subscriptionTimewindow: SubscriptionTimewindow) => void;  
37 - entityDataSubscriptionKey?: number; 36 + updateRealtimeSubscription?: () => SubscriptionTimewindow;
  37 + setRealtimeSubscription?: (subscriptionTimewindow: SubscriptionTimewindow) => void;
  38 + subscription?: EntityDataSubscription;
  39 +}
  40 +
  41 +export interface EntityDataLoadResult {
  42 + pageData: PageData<EntityData>;
  43 + data: Array<Array<DataSetHolder>>;
  44 + datasourceIndex: number;
38 } 45 }
39 46
40 @Injectable({ 47 @Injectable({
@@ -42,16 +49,48 @@ export interface EntityDataListener { @@ -42,16 +49,48 @@ export interface EntityDataListener {
42 }) 49 })
43 export class EntityDataService { 50 export class EntityDataService {
44 51
45 - private subscriptions: {[entityDataSubscriptionKey: string]: EntityDataSubscription} = {};  
46 -  
47 constructor(private telemetryService: TelemetryWebsocketService, 52 constructor(private telemetryService: TelemetryWebsocketService,
48 private utils: UtilsService) {} 53 private utils: UtilsService) {}
49 54
50 - public subscribeToEntityData(listener: EntityDataListener) { 55 + public prepareSubscription(listener: EntityDataListener): Observable<EntityDataLoadResult> {
51 const datasource = listener.configDatasource; 56 const datasource = listener.configDatasource;
52 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) { 57 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) {
  58 + return of(null);
  59 + }
  60 + listener.subscription = this.createSubscription(listener,
  61 + datasource.pageLink, datasource.keyFilters,
  62 + false);
  63 + return listener.subscription.subscribe();
  64 + }
  65 +
  66 + public startSubscription(listener: EntityDataListener) {
  67 + if (listener.subscriptionType === widgetType.timeseries) {
  68 + listener.subscription.entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  69 + }
  70 + listener.subscription.start();
  71 + }
  72 +
  73 + public subscribeForLatestData(listener: EntityDataListener,
  74 + pageLink: EntityDataPageLink,
  75 + keyFilters: KeyFilter[]) {
  76 + const datasource = listener.configDatasource;
  77 + if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) {
53 return; 78 return;
54 } 79 }
  80 + listener.subscription = this.createSubscription(listener,
  81 + pageLink, keyFilters, true);
  82 + listener.subscription.subscribe();
  83 + }
  84 +
  85 + public stopSubscription(listener: EntityDataListener) {
  86 + listener.subscription.unsubscribe();
  87 + }
  88 +
  89 + private createSubscription(listener: EntityDataListener,
  90 + pageLink: EntityDataPageLink,
  91 + keyFilters: KeyFilter[],
  92 + isLatestDataSubscription: boolean): EntityDataSubscription {
  93 + const datasource = listener.configDatasource;
55 const subscriptionDataKeys: Array<SubscriptionDataKey> = []; 94 const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
56 datasource.dataKeys.forEach((dataKey) => { 95 datasource.dataKeys.forEach((dataKey) => {
57 const subscriptionDataKey: SubscriptionDataKey = { 96 const subscriptionDataKey: SubscriptionDataKey = {
@@ -62,47 +101,19 @@ export class EntityDataService { @@ -62,47 +101,19 @@ export class EntityDataService {
62 }; 101 };
63 subscriptionDataKeys.push(subscriptionDataKey); 102 subscriptionDataKeys.push(subscriptionDataKey);
64 }); 103 });
65 -  
66 const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = { 104 const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
67 datasourceType: datasource.type, 105 datasourceType: datasource.type,
68 dataKeys: subscriptionDataKeys, 106 dataKeys: subscriptionDataKeys,
69 type: listener.subscriptionType 107 type: listener.subscriptionType
70 }; 108 };
71 -  
72 - if (listener.subscriptionType === widgetType.timeseries) {  
73 - entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);  
74 - }  
75 if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { 109 if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
76 entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; 110 entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
77 - entityDataSubscriptionOptions.pageLink = datasource.pageLink;  
78 - entityDataSubscriptionOptions.keyFilters = datasource.keyFilters;  
79 - }  
80 - listener.entityDataSubscriptionKey = objectHashCode(entityDataSubscriptionOptions);  
81 - let subscription: EntityDataSubscription;  
82 - if (this.subscriptions[listener.entityDataSubscriptionKey]) {  
83 - subscription = this.subscriptions[listener.entityDataSubscriptionKey];  
84 - subscription.syncListener(listener);  
85 - } else {  
86 - subscription = new EntityDataSubscription(entityDataSubscriptionOptions,  
87 - this.telemetryService, this.utils);  
88 - this.subscriptions[listener.entityDataSubscriptionKey] = subscription;  
89 - subscription.addListener(listener);  
90 - subscription.start();  
91 - }  
92 - }  
93 -  
94 - public unsubscribeFromDatasource(listener: EntityDataListener) {  
95 - if (listener.entityDataSubscriptionKey) {  
96 - const subscription = this.subscriptions[listener.entityDataSubscriptionKey];  
97 - if (subscription) {  
98 - subscription.removeListener(listener);  
99 - if (!subscription.hasListeners()) {  
100 - subscription.unsubscribe();  
101 - delete this.subscriptions[listener.entityDataSubscriptionKey];  
102 - }  
103 - }  
104 - listener.entityDataSubscriptionKey = null; 111 + entityDataSubscriptionOptions.pageLink = pageLink;
  112 + entityDataSubscriptionOptions.keyFilters = keyFilters;
105 } 113 }
  114 + entityDataSubscriptionOptions.isLatestDataSubscription = isLatestDataSubscription;
  115 + return new EntityDataSubscription(entityDataSubscriptionOptions,
  116 + listener, this.telemetryService, this.utils);
106 } 117 }
107 118
108 } 119 }
@@ -98,7 +98,8 @@ export interface IAliasController { @@ -98,7 +98,8 @@ export interface IAliasController {
98 getAliasInfo(aliasId: string): Observable<AliasInfo>; 98 getAliasInfo(aliasId: string): Observable<AliasInfo>;
99 getEntityAliasId(aliasName: string): string; 99 getEntityAliasId(aliasName: string): string;
100 getInstantAliasInfo(aliasId: string): AliasInfo; 100 getInstantAliasInfo(aliasId: string): AliasInfo;
101 - resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>; 101 + resolveSingleEntityInfo(aliasId: string): Observable<EntityInfo>;
  102 + resolveDatasources(datasources: Array<Datasource>, singleEntity?: boolean): Observable<Array<Datasource>>;
102 resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>; 103 resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>;
103 getEntityAliases(): EntityAliases; 104 getEntityAliases(): EntityAliases;
104 updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); 105 updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
@@ -202,8 +203,8 @@ export interface WidgetSubscriptionOptions { @@ -202,8 +203,8 @@ export interface WidgetSubscriptionOptions {
202 alarmsMaxCountLoad?: number; 203 alarmsMaxCountLoad?: number;
203 alarmsFetchSize?: number; 204 alarmsFetchSize?: number;
204 datasources?: Array<Datasource>; 205 datasources?: Array<Datasource>;
205 - keyFilters?: Array<KeyFilter>;  
206 - pageLink?: EntityDataPageLink; 206 + hasDataPageLink?: boolean;
  207 + singleEntity?: boolean;
207 targetDeviceAliasIds?: Array<string>; 208 targetDeviceAliasIds?: Array<string>;
208 targetDeviceIds?: Array<string>; 209 targetDeviceIds?: Array<string>;
209 useDashboardTimewindow?: boolean; 210 useDashboardTimewindow?: boolean;
@@ -264,7 +265,7 @@ export interface IWidgetSubscription { @@ -264,7 +265,7 @@ export interface IWidgetSubscription {
264 265
265 onAliasesChanged(aliasIds: Array<string>): boolean; 266 onAliasesChanged(aliasIds: Array<string>): boolean;
266 267
267 - onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): boolean; 268 + onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void;
268 269
269 updateDataVisibility(index: number): void; 270 updateDataVisibility(index: number): void;
270 271
@@ -278,6 +279,10 @@ export interface IWidgetSubscription { @@ -278,6 +279,10 @@ export interface IWidgetSubscription {
278 279
279 subscribe(): void; 280 subscribe(): void;
280 281
  282 + subscribeForLatestData(datasourceIndex: number,
  283 + pageLink: EntityDataPageLink,
  284 + keyFilters: KeyFilter[]): void;
  285 +
281 isDataResolved(): boolean; 286 isDataResolved(): boolean;
282 287
283 destroy(): void; 288 destroy(): void;
@@ -22,7 +22,6 @@ import { @@ -22,7 +22,6 @@ import {
22 WidgetSubscriptionOptions 22 WidgetSubscriptionOptions
23 } from '@core/api/widget-api.models'; 23 } from '@core/api/widget-api.models';
24 import { 24 import {
25 - DataKey,  
26 DataSet, 25 DataSet,
27 DataSetHolder, 26 DataSetHolder,
28 Datasource, 27 Datasource,
@@ -43,20 +42,18 @@ import { @@ -43,20 +42,18 @@ import {
43 toHistoryTimewindow, 42 toHistoryTimewindow,
44 WidgetTimewindow 43 WidgetTimewindow
45 } from '@app/shared/models/time/time.models'; 44 } from '@app/shared/models/time/time.models';
46 -import { Observable, ReplaySubject, Subject, throwError } from 'rxjs'; 45 +import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
47 import { CancelAnimationFrame } from '@core/services/raf.service'; 46 import { CancelAnimationFrame } from '@core/services/raf.service';
48 import { EntityType } from '@shared/models/entity-type.models'; 47 import { EntityType } from '@shared/models/entity-type.models';
49 import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models'; 48 import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
50 import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils'; 49 import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils';
51 import { AlarmSourceListener } from '@core/http/alarm.service'; 50 import { AlarmSourceListener } from '@core/http/alarm.service';
52 -import { DatasourceListener } from '@core/api/datasource.service';  
53 import { EntityId } from '@app/shared/models/id/entity-id'; 51 import { EntityId } from '@app/shared/models/id/entity-id';
54 -import { DataKeyType } from '@shared/models/telemetry/telemetry.models';  
55 -import { entityFields } from '@shared/models/entity.models';  
56 import * as moment_ from 'moment'; 52 import * as moment_ from 'moment';
57 import { PageData } from '@shared/models/page/page-data'; 53 import { PageData } from '@shared/models/page/page-data';
58 import { EntityDataListener } from '@core/api/entity-data.service'; 54 import { EntityDataListener } from '@core/api/entity-data.service';
59 -import { EntityData, EntityDataPageLink, EntityKeyType } from '@shared/models/query/query.models'; 55 +import { EntityData, EntityDataPageLink, EntityKeyType, KeyFilter } from '@shared/models/query/query.models';
  56 +import { map } from 'rxjs/operators';
60 57
61 const moment = moment_; 58 const moment = moment_;
62 59
@@ -73,12 +70,14 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -73,12 +70,14 @@ export class WidgetSubscription implements IWidgetSubscription {
73 subscriptionTimewindow: SubscriptionTimewindow; 70 subscriptionTimewindow: SubscriptionTimewindow;
74 useDashboardTimewindow: boolean; 71 useDashboardTimewindow: boolean;
75 72
  73 + hasDataPageLink: boolean;
  74 + singleEntity: boolean;
  75 +
76 datasourcePages: PageData<Datasource>[]; 76 datasourcePages: PageData<Datasource>[];
77 dataPages: PageData<Array<DatasourceData>>[]; 77 dataPages: PageData<Array<DatasourceData>>[];
78 entityDataListeners: Array<EntityDataListener>; 78 entityDataListeners: Array<EntityDataListener>;
79 configuredDatasources: Array<Datasource>; 79 configuredDatasources: Array<Datasource>;
80 80
81 - initDataSubscriptionSubject: Subject<void>;  
82 data: Array<DatasourceData>; 81 data: Array<DatasourceData>;
83 datasources: Array<Datasource>; 82 datasources: Array<Datasource>;
84 // datasourceListeners: Array<DatasourceListener>; 83 // datasourceListeners: Array<DatasourceListener>;
@@ -211,6 +210,8 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -211,6 +210,8 @@ export class WidgetSubscription implements IWidgetSubscription {
211 // this.datasources = this.ctx.utils.validateDatasources(options.datasources); 210 // this.datasources = this.ctx.utils.validateDatasources(options.datasources);
212 this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources); 211 this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources);
213 this.entityDataListeners = []; 212 this.entityDataListeners = [];
  213 + this.hasDataPageLink = options.hasDataPageLink;
  214 + this.singleEntity = options.singleEntity;
214 // this.datasourceListeners = []; 215 // this.datasourceListeners = [];
215 this.datasourcePages = []; 216 this.datasourcePages = [];
216 this.datasources = []; 217 this.datasources = [];
@@ -271,11 +272,11 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -271,11 +272,11 @@ export class WidgetSubscription implements IWidgetSubscription {
271 const initRpcSubject = new ReplaySubject(); 272 const initRpcSubject = new ReplaySubject();
272 if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { 273 if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
273 this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; 274 this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
274 - this.ctx.aliasController.getAliasInfo(this.targetDeviceAliasId).subscribe(  
275 - (aliasInfo) => {  
276 - if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType === EntityType.DEVICE) {  
277 - this.targetDeviceId = aliasInfo.currentEntity.id;  
278 - this.targetDeviceName = aliasInfo.currentEntity.name; 275 + this.ctx.aliasController.resolveSingleEntityInfo(this.targetDeviceAliasId).subscribe(
  276 + (entityInfo) => {
  277 + if (entityInfo && entityInfo.entityType === EntityType.DEVICE) {
  278 + this.targetDeviceId = entityInfo.id;
  279 + this.targetDeviceName = entityInfo.name;
279 if (this.targetDeviceId) { 280 if (this.targetDeviceId) {
280 this.rpcEnabled = true; 281 this.rpcEnabled = true;
281 } else { 282 } else {
@@ -348,34 +349,72 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -348,34 +349,72 @@ export class WidgetSubscription implements IWidgetSubscription {
348 } 349 }
349 350
350 private initDataSubscription(): Observable<any> { 351 private initDataSubscription(): Observable<any> {
351 - this.initDataSubscriptionSubject = new ReplaySubject(1); 352 + const initDataSubscriptionSubject = new ReplaySubject(1);
352 this.loadStDiff().subscribe(() => { 353 this.loadStDiff().subscribe(() => {
353 if (!this.ctx.aliasController) { 354 if (!this.ctx.aliasController) {
354 this.hasResolvedData = true; 355 this.hasResolvedData = true;
355 - // this.configureData();  
356 - // initDataSubscriptionSubject.next();  
357 - // initDataSubscriptionSubject.complete();  
358 - this.subscribe(); 356 + this.prepareDataSubscriptions().subscribe(
  357 + () => {
  358 + initDataSubscriptionSubject.next();
  359 + initDataSubscriptionSubject.complete();
  360 + }
  361 + );
359 } else { 362 } else {
360 - this.ctx.aliasController.resolveDatasources(this.configuredDatasources).subscribe( 363 + this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe(
361 (datasources) => { 364 (datasources) => {
362 this.configuredDatasources = datasources; 365 this.configuredDatasources = datasources;
363 - /* if (datasources && datasources.length) {  
364 - this.hasResolvedData = true;  
365 - }*/  
366 - this.subscribe();  
367 - // this.configureData();  
368 - // initDataSubscriptionSubject.next();  
369 - // initDataSubscriptionSubject.complete(); 366 + this.prepareDataSubscriptions().subscribe(
  367 + () => {
  368 + initDataSubscriptionSubject.next();
  369 + initDataSubscriptionSubject.complete();
  370 + }
  371 + );
370 }, 372 },
371 (err) => { 373 (err) => {
372 this.notifyDataLoaded(); 374 this.notifyDataLoaded();
373 - this.initDataSubscriptionSubject.error(err); 375 + initDataSubscriptionSubject.error(err);
374 } 376 }
375 ); 377 );
376 } 378 }
377 }); 379 });
378 - return this.initDataSubscriptionSubject.asObservable(); 380 + return initDataSubscriptionSubject.asObservable();
  381 + }
  382 +
  383 + private prepareDataSubscriptions(): Observable<any> {
  384 + if (this.hasDataPageLink) {
  385 + this.hasResolvedData = true;
  386 + return of(null);
  387 + }
  388 + const resolveResultObservables = this.configuredDatasources.map((datasource, index) => {
  389 + const listener: EntityDataListener = {
  390 + subscriptionType: this.type,
  391 + configDatasource: datasource,
  392 + configDatasourceIndex: index,
  393 + dataLoaded: (pageData, data1, datasourceIndex) => {
  394 + this.dataLoaded(pageData, data1, datasourceIndex, true)
  395 + },
  396 + dataUpdated: this.dataUpdated.bind(this),
  397 + updateRealtimeSubscription: () => {
  398 + this.subscriptionTimewindow = this.updateRealtimeSubscription();
  399 + return this.subscriptionTimewindow;
  400 + },
  401 + setRealtimeSubscription: (subscriptionTimewindow) => {
  402 + this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
  403 + }
  404 + };
  405 + this.entityDataListeners.push(listener);
  406 + return this.ctx.entityDataService.prepareSubscription(listener);
  407 + });
  408 + return forkJoin(resolveResultObservables).pipe(
  409 + map((resolveResults) => {
  410 + resolveResults.forEach((resolveResult) => {
  411 + this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, false);
  412 + });
  413 + this.configureLoadedData();
  414 + this.hasResolvedData = true;
  415 + this.notifyDataLoaded();
  416 + })
  417 + );
379 } 418 }
380 419
381 /* private initDataSubscriptionOld(): Observable<any> { 420 /* private initDataSubscriptionOld(): Observable<any> {
@@ -592,13 +631,12 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -592,13 +631,12 @@ export class WidgetSubscription implements IWidgetSubscription {
592 }); 631 });
593 } 632 }
594 633
595 - onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow): boolean { 634 + onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) {
596 if (this.type === widgetType.timeseries || this.type === widgetType.alarm) { 635 if (this.type === widgetType.timeseries || this.type === widgetType.alarm) {
597 if (this.useDashboardTimewindow) { 636 if (this.useDashboardTimewindow) {
598 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { 637 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
599 - // this.timeWindowConfig = deepClone(newDashboardTimewindow);  
600 - // this.update();  
601 - // TODO: 638 + this.timeWindowConfig = deepClone(newDashboardTimewindow);
  639 + this.update();
602 return true; 640 return true;
603 } 641 }
604 } 642 }
@@ -785,8 +823,12 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -785,8 +823,12 @@ export class WidgetSubscription implements IWidgetSubscription {
785 } 823 }
786 824
787 update() { 825 update() {
788 - this.unsubscribe();  
789 - this.subscribe(); 826 + if (this.type === widgetType.rpc || this.type === widgetType.alarm) {
  827 + this.unsubscribe();
  828 + this.subscribe();
  829 + } else {
  830 + this.dataSubscribe();
  831 + }
790 } 832 }
791 833
792 subscribe(): void { 834 subscribe(): void {
@@ -802,6 +844,29 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -802,6 +844,29 @@ export class WidgetSubscription implements IWidgetSubscription {
802 } 844 }
803 } 845 }
804 846
  847 + subscribeForLatestData(datasourceIndex: number,
  848 + pageLink: EntityDataPageLink,
  849 + keyFilters: KeyFilter[]): void {
  850 + let entityDataListener = this.entityDataListeners[datasourceIndex];
  851 + if (entityDataListener) {
  852 + this.ctx.entityDataService.stopSubscription(entityDataListener);
  853 + }
  854 + const datasource = this.configuredDatasources[datasourceIndex];
  855 + if (datasource) {
  856 + entityDataListener = {
  857 + subscriptionType: this.type,
  858 + configDatasource: datasource,
  859 + configDatasourceIndex: datasourceIndex,
  860 + dataLoaded: (pageData, data1, datasourceIndex1) => {
  861 + this.dataLoaded(pageData, data1, datasourceIndex1, true)
  862 + },
  863 + dataUpdated: this.dataUpdated.bind(this)
  864 + };
  865 + this.entityDataListeners[datasourceIndex] = entityDataListener;
  866 + this.ctx.entityDataService.subscribeForLatestData(entityDataListener, pageLink, keyFilters);
  867 + }
  868 + }
  869 +
805 private doSubscribe() { 870 private doSubscribe() {
806 if (this.type === widgetType.rpc) { 871 if (this.type === widgetType.rpc) {
807 return; 872 return;
@@ -809,6 +874,12 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -809,6 +874,12 @@ export class WidgetSubscription implements IWidgetSubscription {
809 if (this.type === widgetType.alarm) { 874 if (this.type === widgetType.alarm) {
810 this.alarmsSubscribe(); 875 this.alarmsSubscribe();
811 } else { 876 } else {
  877 + this.dataSubscribe();
  878 + }
  879 + }
  880 +
  881 + private dataSubscribe() {
  882 + if (!this.hasDataPageLink) {
812 this.notifyDataLoading(); 883 this.notifyDataLoading();
813 if (this.type === widgetType.timeseries && this.timeWindowConfig) { 884 if (this.type === widgetType.timeseries && this.timeWindowConfig) {
814 this.updateRealtimeSubscription(); 885 this.updateRealtimeSubscription();
@@ -819,62 +890,10 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -819,62 +890,10 @@ export class WidgetSubscription implements IWidgetSubscription {
819 this.onDataUpdated(); 890 this.onDataUpdated();
820 } 891 }
821 } 892 }
822 - // let index = 0;  
823 const forceUpdate = !this.datasources.length; 893 const forceUpdate = !this.datasources.length;
824 - this.configuredDatasources.forEach((datasource, index) => {  
825 - const listener: EntityDataListener = {  
826 - subscriptionType: this.type,  
827 - subscriptionTimewindow: this.subscriptionTimewindow,  
828 - configDatasource: datasource,  
829 - configDatasourceIndex: index,  
830 - dataLoaded: this.dataLoaded.bind(this),  
831 - dataUpdated: this.dataUpdated.bind(this),  
832 - updateRealtimeSubscription: () => {  
833 - this.subscriptionTimewindow = this.updateRealtimeSubscription();  
834 - return this.subscriptionTimewindow;  
835 - },  
836 - setRealtimeSubscription: (subscriptionTimewindow) => {  
837 - this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));  
838 - }  
839 - };  
840 -  
841 - /*if (this.comparisonEnabled && datasource.isAdditional) {  
842 - listener.subscriptionTimewindow = this.timewindowForComparison;  
843 - listener.updateRealtimeSubscription = () => {  
844 - this.subscriptionTimewindow = this.updateSubscriptionForComparison();  
845 - return this.subscriptionTimewindow;  
846 - };  
847 - listener.setRealtimeSubscription = () => {  
848 - this.updateSubscriptionForComparison();  
849 - };  
850 - }*/  
851 -  
852 -/* let entityFieldKey = false;  
853 -  
854 - for (let a = 0; a < datasource.dataKeys.length; a++) {  
855 - if (datasource.dataKeys[a].type !== DataKeyType.entityField) {  
856 - this.data[index + a].data = [];  
857 - } else {  
858 - entityFieldKey = true;  
859 - }  
860 - }  
861 - index += datasource.dataKeys.length;*/  
862 -  
863 - this.entityDataListeners.push(listener);  
864 - // this.datasourceListeners.push(listener);  
865 -  
866 - // if (datasource.dataKeys.length) {  
867 - // this.ctx.datasourceService.subscribeToDatasource(listener);  
868 - // }  
869 -  
870 - this.ctx.entityDataService.subscribeToEntityData(listener);  
871 -  
872 - /* if (datasource.unresolvedStateEntity || entityFieldKey ||  
873 - !datasource.dataKeys.length ||  
874 - (datasource.type === DatasourceType.entity && !datasource.entityId)  
875 - ) {  
876 - forceUpdate = true;  
877 - }*/ 894 + this.entityDataListeners.forEach((listener) => {
  895 + listener.subscriptionTimewindow = this.subscriptionTimewindow;
  896 + this.ctx.entityDataService.startSubscription(listener);
878 }); 897 });
879 if (forceUpdate) { 898 if (forceUpdate) {
880 this.notifyDataLoaded(); 899 this.notifyDataLoaded();
@@ -1000,7 +1019,9 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1000,7 +1019,9 @@ export class WidgetSubscription implements IWidgetSubscription {
1000 this.alarmsUnsubscribe(); 1019 this.alarmsUnsubscribe();
1001 } else { 1020 } else {
1002 this.entityDataListeners.forEach((listener) => { 1021 this.entityDataListeners.forEach((listener) => {
1003 - this.ctx.entityDataService.unsubscribeFromDatasource(listener); 1022 + if (listener != null) {
  1023 + this.ctx.entityDataService.stopSubscription(listener);
  1024 + }
1004 }); 1025 });
1005 this.entityDataListeners.length = 0; 1026 this.entityDataListeners.length = 0;
1006 this.resetData(); 1027 this.resetData();
@@ -1129,7 +1150,9 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1129,7 +1150,9 @@ export class WidgetSubscription implements IWidgetSubscription {
1129 return this.timewindowForComparison; 1150 return this.timewindowForComparison;
1130 } 1151 }
1131 1152
1132 - private dataLoaded(pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) { 1153 + private dataLoaded(pageData: PageData<EntityData>,
  1154 + data: Array<Array<DataSetHolder>>,
  1155 + datasourceIndex: number, isUpdate: boolean) {
1133 const datasource = this.configuredDatasources[datasourceIndex]; 1156 const datasource = this.configuredDatasources[datasourceIndex];
1134 datasource.dataReceived = true; 1157 datasource.dataReceived = true;
1135 const datasources = pageData.data.map((entityData, index) => 1158 const datasources = pageData.data.map((entityData, index) =>
@@ -1152,14 +1175,8 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1152,14 +1175,8 @@ export class WidgetSubscription implements IWidgetSubscription {
1152 totalPages: pageData.totalPages 1175 totalPages: pageData.totalPages
1153 }; 1176 };
1154 this.dataPages[datasourceIndex] = datasourceDataPage; 1177 this.dataPages[datasourceIndex] = datasourceDataPage;
1155 - this.configureLoadedData();  
1156 - const readyCount = this.configuredDatasources.filter(d => d.dataReceived).length;  
1157 - if (this.configuredDatasources.length === readyCount) {  
1158 - this.hasResolvedData = true;  
1159 - this.initDataSubscriptionSubject.next();  
1160 - this.initDataSubscriptionSubject.complete(); 1178 + if (isUpdate) {
1161 this.configureLoadedData(); 1179 this.configureLoadedData();
1162 - this.notifyDataLoaded();  
1163 this.onDataUpdated(true); 1180 this.onDataUpdated(true);
1164 } 1181 }
1165 } 1182 }
@@ -1238,6 +1255,9 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1238,6 +1255,9 @@ export class WidgetSubscription implements IWidgetSubscription {
1238 dataKey, 1255 dataKey,
1239 data: [] 1256 data: []
1240 }; 1257 };
  1258 + if (data && data[keyIndex] && data[keyIndex].data) {
  1259 + datasourceData.data = data[keyIndex].data;
  1260 + }
1241 return datasourceData; 1261 return datasourceData;
1242 }); 1262 });
1243 } 1263 }
@@ -1275,6 +1295,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1275,6 +1295,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1275 const startIndex = configuredDatasource.dataKeyStartIndex; 1295 const startIndex = configuredDatasource.dataKeyStartIndex;
1276 const dataKeysCount = configuredDatasource.dataKeys.length; 1296 const dataKeysCount = configuredDatasource.dataKeys.length;
1277 const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex; 1297 const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex;
  1298 + this.notifyDataLoaded();
1278 let update = true; 1299 let update = true;
1279 let currentData: DataSetHolder; 1300 let currentData: DataSetHolder;
1280 if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) { 1301 if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) {
@@ -54,9 +54,15 @@ import { @@ -54,9 +54,15 @@ import {
54 import { EntityRelationService } from '@core/http/entity-relation.service'; 54 import { EntityRelationService } from '@core/http/entity-relation.service';
55 import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; 55 import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
56 import { Asset, AssetSearchQuery } from '@shared/models/asset.models'; 56 import { Asset, AssetSearchQuery } from '@shared/models/asset.models';
57 -import { Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models'; 57 +import { ClaimResult, Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models';
58 import { EntityViewSearchQuery } from '@shared/models/entity-view.models'; 58 import { EntityViewSearchQuery } from '@shared/models/entity-view.models';
59 import { AttributeService } from '@core/http/attribute.service'; 59 import { AttributeService } from '@core/http/attribute.service';
  60 +import {
  61 + createDefaultEntityDataPageLink,
  62 + EntityData,
  63 + EntityDataQuery,
  64 + EntityFilter, EntityKeyType
  65 +} from '@shared/models/query/query.models';
60 66
61 @Injectable({ 67 @Injectable({
62 providedIn: 'root' 68 providedIn: 'root'
@@ -360,6 +366,54 @@ export class EntityService { @@ -360,6 +366,54 @@ export class EntityService {
360 } 366 }
361 } 367 }
362 368
  369 + public findEntityDataByQuery(query: EntityDataQuery, config?: RequestConfig): Observable<PageData<EntityData>> {
  370 + return this.http.post<PageData<EntityData>>('/api/entitiesQuery/find', query, defaultHttpOptionsFromConfig(config));
  371 + }
  372 +
  373 + private entityDataToEntityInfo(entityData: EntityData): EntityInfo {
  374 + const entityInfo: EntityInfo = {
  375 + id: entityData.entityId.id,
  376 + entityType: entityData.entityId.entityType as EntityType
  377 + };
  378 + if (entityData.latest && entityData.latest[EntityKeyType.ENTITY_FIELD]) {
  379 + const fields = entityData.latest[EntityKeyType.ENTITY_FIELD];
  380 + if (fields.name) {
  381 + entityInfo.name = fields.name.value;
  382 + }
  383 + if (fields.label) {
  384 + entityInfo.label = fields.label.value;
  385 + }
  386 + }
  387 + return entityInfo;
  388 + }
  389 +
  390 + public findSingleEntityInfoByEntityFilter(filter: EntityFilter, config?: RequestConfig): Observable<EntityInfo> {
  391 + const query: EntityDataQuery = {
  392 + entityFilter: filter,
  393 + pageLink: createDefaultEntityDataPageLink(1),
  394 + entityFields: [
  395 + {
  396 + type: EntityKeyType.ENTITY_FIELD,
  397 + key: 'name'
  398 + },
  399 + {
  400 + type: EntityKeyType.ENTITY_FIELD,
  401 + key: 'label'
  402 + }
  403 + ]
  404 + };
  405 + return this.findEntityDataByQuery(query, config).pipe(
  406 + map((data) => {
  407 + if (data.data.length) {
  408 + const entityData = data.data[0];
  409 + return this.entityDataToEntityInfo(entityData);
  410 + } else {
  411 + return null;
  412 + }
  413 + })
  414 + );
  415 + }
  416 +
363 public getAliasFilterTypesByEntityTypes(entityTypes: Array<EntityType | AliasEntityType>): Array<AliasFilterType> { 417 public getAliasFilterTypesByEntityTypes(entityTypes: Array<EntityType | AliasEntityType>): Array<AliasFilterType> {
364 const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]); 418 const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]);
365 if (!entityTypes || !entityTypes.length) { 419 if (!entityTypes || !entityTypes.length) {
@@ -605,7 +659,7 @@ export class EntityService { @@ -605,7 +659,7 @@ export class EntityService {
605 public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> { 659 public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> {
606 const filter = entityAlias.filter; 660 const filter = entityAlias.filter;
607 return this.resolveAliasFilter(filter, stateParams).pipe( 661 return this.resolveAliasFilter(filter, stateParams).pipe(
608 - map((result) => { 662 + mergeMap((result) => {
609 const aliasInfo: AliasInfo = { 663 const aliasInfo: AliasInfo = {
610 alias: entityAlias.alias, 664 alias: entityAlias.alias,
611 entityFilter: result.entityFilter, 665 entityFilter: result.entityFilter,
@@ -615,30 +669,19 @@ export class EntityService { @@ -615,30 +669,19 @@ export class EntityService {
615 }; 669 };
616 aliasInfo.resolvedEntities = result.entities; 670 aliasInfo.resolvedEntities = result.entities;
617 aliasInfo.currentEntity = null; 671 aliasInfo.currentEntity = null;
618 - if (aliasInfo.resolvedEntities.length) {  
619 - aliasInfo.currentEntity = aliasInfo.resolvedEntities[0]; 672 + if (!aliasInfo.resolveMultiple && aliasInfo.entityFilter) {
  673 + return this.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter,
  674 + {ignoreLoading: true, ignoreErrors: true}).pipe(
  675 + map((entity) => {
  676 + aliasInfo.currentEntity = entity;
  677 + return aliasInfo;
  678 + })
  679 + );
620 } 680 }
621 - return aliasInfo; 681 + return of(aliasInfo);
622 }) 682 })
623 ); 683 );
624 } 684 }
625 -/*  
626 - public resolveEntityFilter(filter: EntityAliasFilter, stateParams: StateParams): EntityFilter {  
627 - const stateEntityInfo = this.getStateEntityInfo(filter, stateParams);  
628 - let result: EntityFilter = filter;  
629 - const stateEntityId = stateEntityInfo.entityId;  
630 - if (filter.type === AliasFilterType.stateEntity) {  
631 - result = {  
632 - singleEntity: stateEntityId,  
633 - type: AliasFilterType.singleEntity  
634 - };  
635 - } else if (filter.rootStateEntity) {  
636 - let rootEntityType;  
637 - let rootEntityId;  
638 -  
639 - }  
640 - return result;  
641 - }*/  
642 685
643 public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable<EntityAliasFilterResult> { 686 public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable<EntityAliasFilterResult> {
644 const result: EntityAliasFilterResult = { 687 const result: EntityAliasFilterResult = {
@@ -114,6 +114,17 @@ export class TelemetryWebsocketService implements TelemetryService { @@ -114,6 +114,17 @@ export class TelemetryWebsocketService implements TelemetryService {
114 this.publishCommands(); 114 this.publishCommands();
115 } 115 }
116 116
  117 + public update(subscriber: TelemetrySubscriber) {
  118 + subscriber.subscriptionCommands.forEach(
  119 + (subscriptionCommand) => {
  120 + if (subscriptionCommand.cmdId && subscriptionCommand instanceof EntityDataCmd) {
  121 + this.cmdsWrapper.entityDataCmds.push(subscriptionCommand);
  122 + }
  123 + }
  124 + );
  125 + this.publishCommands();
  126 + }
  127 +
117 public unsubscribe(subscriber: TelemetrySubscriber) { 128 public unsubscribe(subscriber: TelemetrySubscriber) {
118 if (this.isActive) { 129 if (this.isActive) {
119 subscriber.subscriptionCommands.forEach( 130 subscriber.subscriptionCommands.forEach(
@@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
39 </mat-toolbar> 39 </mat-toolbar>
40 <div fxFlex class="table-container"> 40 <div fxFlex class="table-container">
41 <table mat-table [dataSource]="entityDatasource" 41 <table mat-table [dataSource]="entityDatasource"
42 - matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> 42 + matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
43 <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;"> 43 <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
44 <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell> 44 <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
45 <mat-cell *matCellDef="let entity;" 45 <mat-cell *matCellDef="let entity;"
@@ -32,26 +32,23 @@ import { @@ -32,26 +32,23 @@ import {
32 DataKey, 32 DataKey,
33 Datasource, 33 Datasource,
34 DatasourceData, 34 DatasourceData,
35 - DatasourceType,  
36 WidgetActionDescriptor, 35 WidgetActionDescriptor,
37 WidgetConfig 36 WidgetConfig
38 } from '@shared/models/widget.models'; 37 } from '@shared/models/widget.models';
39 import { IWidgetSubscription } from '@core/api/widget-api.models'; 38 import { IWidgetSubscription } from '@core/api/widget-api.models';
40 import { UtilsService } from '@core/services/utils.service'; 39 import { UtilsService } from '@core/services/utils.service';
41 import { TranslateService } from '@ngx-translate/core'; 40 import { TranslateService } from '@ngx-translate/core';
42 -import { deepClone, isDefined, isNumber, createLabelFromDatasource, hashCode } from '@core/utils'; 41 +import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber } from '@core/utils';
43 import cssjs from '@core/css/css'; 42 import cssjs from '@core/css/css';
44 -import { PageLink } from '@shared/models/page/page-link';  
45 -import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';  
46 import { CollectionViewer, DataSource } from '@angular/cdk/collections'; 43 import { CollectionViewer, DataSource } from '@angular/cdk/collections';
47 import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 44 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
48 import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; 45 import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
49 import { emptyPageData, PageData } from '@shared/models/page/page-data'; 46 import { emptyPageData, PageData } from '@shared/models/page/page-data';
50 import { EntityId } from '@shared/models/id/entity-id'; 47 import { EntityId } from '@shared/models/id/entity-id';
51 import { entityTypeTranslations } from '@shared/models/entity-type.models'; 48 import { entityTypeTranslations } from '@shared/models/entity-type.models';
52 -import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; 49 +import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
53 import { MatPaginator } from '@angular/material/paginator'; 50 import { MatPaginator } from '@angular/material/paginator';
54 -import { MatSort } from '@angular/material/sort'; 51 +import { MatSort, SortDirection } from '@angular/material/sort';
55 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 52 import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
56 import { 53 import {
57 CellContentInfo, 54 CellContentInfo,
@@ -59,15 +56,13 @@ import { @@ -59,15 +56,13 @@ import {
59 constructTableCssString, 56 constructTableCssString,
60 DisplayColumn, 57 DisplayColumn,
61 EntityColumn, 58 EntityColumn,
62 - EntityData,  
63 - fromEntityColumnDef, 59 + EntityData, entityDataSortOrderFromString, findColumnByEntityKey, findEntityKeyByColumnDef,
64 getCellContentInfo, 60 getCellContentInfo,
65 getCellStyleInfo, 61 getCellStyleInfo,
66 getColumnWidth, 62 getColumnWidth,
67 getEntityValue, 63 getEntityValue,
68 TableWidgetDataKeySettings, 64 TableWidgetDataKeySettings,
69 TableWidgetSettings, 65 TableWidgetSettings,
70 - toEntityColumnDef,  
71 widthStyle 66 widthStyle
72 } from '@home/components/widget/lib/table-widget.models'; 67 } from '@home/components/widget/lib/table-widget.models';
73 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 68 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@@ -77,6 +72,13 @@ import { @@ -77,6 +72,13 @@ import {
77 DisplayColumnsPanelComponent, 72 DisplayColumnsPanelComponent,
78 DisplayColumnsPanelData 73 DisplayColumnsPanelData
79 } from '@home/components/widget/lib/display-columns-panel.component'; 74 } from '@home/components/widget/lib/display-columns-panel.component';
  75 +import {
  76 + Direction,
  77 + EntityDataPageLink,
  78 + entityDataPageLinkSortDirection,
  79 + EntityKeyType,
  80 + KeyFilter
  81 +} from '@shared/models/query/query.models';
80 82
81 interface EntitiesTableWidgetSettings extends TableWidgetSettings { 83 interface EntitiesTableWidgetSettings extends TableWidgetSettings {
82 entitiesTitle: string; 84 entitiesTitle: string;
@@ -103,7 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -103,7 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
103 105
104 public displayPagination = true; 106 public displayPagination = true;
105 public pageSizeOptions; 107 public pageSizeOptions;
106 - public pageLink: PageLink; 108 + public pageLink: EntityDataPageLink;
107 public sortOrderProperty: string; 109 public sortOrderProperty: string;
108 public textSearchMode = false; 110 public textSearchMode = false;
109 public columns: Array<EntityColumn> = []; 111 public columns: Array<EntityColumn> = [];
@@ -150,8 +152,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -150,8 +152,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
150 private domSanitizer: DomSanitizer) { 152 private domSanitizer: DomSanitizer) {
151 super(store); 153 super(store);
152 154
153 - const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);  
154 - this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); 155 + // const sortOrder: EntityDataSortOrder = sortOrderFromString(this.defaultSortOrder);
  156 + this.pageLink = {
  157 + page: 0,
  158 + pageSize: this.defaultPageSize,
  159 + textSearch: null
  160 + };
  161 + // new PageLink(this.defaultPageSize, 0, null, sortOrder);
155 } 162 }
156 163
157 ngOnInit(): void { 164 ngOnInit(): void {
@@ -191,11 +198,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -191,11 +198,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
191 198
192 public onDataUpdated() { 199 public onDataUpdated() {
193 this.ngZone.run(() => { 200 this.ngZone.run(() => {
194 - this.entityDatasource.updateEntitiesData(this.subscription.data); 201 + this.entityDatasource.dataUpdated(); // .updateEntitiesData(this.subscription.data);
195 this.ctx.detectChanges(); 202 this.ctx.detectChanges();
196 }); 203 });
197 } 204 }
198 205
  206 + public pageLinkSortDirection(): SortDirection {
  207 + return entityDataPageLinkSortDirection(this.pageLink);
  208 + }
  209 +
199 private initializeConfig() { 210 private initializeConfig() {
200 this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; 211 this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
201 212
@@ -256,7 +267,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -256,7 +267,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
256 name: 'entityName', 267 name: 'entityName',
257 label: 'entityName', 268 label: 'entityName',
258 def: 'entityName', 269 def: 'entityName',
259 - title: entityNameColumnTitle 270 + title: entityNameColumnTitle,
  271 + entityKey: {
  272 + key: 'name',
  273 + type: EntityKeyType.ENTITY_FIELD
  274 + }
260 } as EntityColumn 275 } as EntityColumn
261 ); 276 );
262 this.contentsInfo.entityName = { 277 this.contentsInfo.entityName = {
@@ -273,7 +288,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -273,7 +288,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
273 name: 'entityLabel', 288 name: 'entityLabel',
274 label: 'entityLabel', 289 label: 'entityLabel',
275 def: 'entityLabel', 290 def: 'entityLabel',
276 - title: entityLabelColumnTitle 291 + title: entityLabelColumnTitle,
  292 + entityKey: {
  293 + key: 'label',
  294 + type: EntityKeyType.ENTITY_FIELD
  295 + }
277 } as EntityColumn 296 } as EntityColumn
278 ); 297 );
279 this.contentsInfo.entityLabel = { 298 this.contentsInfo.entityLabel = {
@@ -291,6 +310,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -291,6 +310,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
291 label: 'entityType', 310 label: 'entityType',
292 def: 'entityType', 311 def: 'entityType',
293 title: this.translate.instant('entity.entity-type'), 312 title: this.translate.instant('entity.entity-type'),
  313 + entityKey: {
  314 + key: 'entityType',
  315 + type: EntityKeyType.ENTITY_FIELD
  316 + }
294 } as EntityColumn 317 } as EntityColumn
295 ); 318 );
296 this.contentsInfo.entityType = { 319 this.contentsInfo.entityType = {
@@ -309,8 +332,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -309,8 +332,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
309 if (datasource) { 332 if (datasource) {
310 datasource.dataKeys.forEach((entityDataKey) => { 333 datasource.dataKeys.forEach((entityDataKey) => {
311 const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn; 334 const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn;
  335 + dataKey.entityKey = {
  336 + key: dataKey.name,
  337 + type: null
  338 + };
312 if (dataKey.type === DataKeyType.function) { 339 if (dataKey.type === DataKeyType.function) {
313 dataKey.name = dataKey.label; 340 dataKey.name = dataKey.label;
  341 + dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD;
  342 + } else if (dataKey.type === DataKeyType.entityField) {
  343 + dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD;
  344 + } else if (dataKey.type === DataKeyType.attribute) {
  345 + dataKey.entityKey.type = EntityKeyType.ATTRIBUTE;
  346 + } else if (dataKey.type === DataKeyType.timeseries) {
  347 + dataKey.entityKey.type = EntityKeyType.TIME_SERIES;
314 } 348 }
315 dataKeys.push(dataKey); 349 dataKeys.push(dataKey);
316 350
@@ -331,14 +365,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -331,14 +365,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
331 if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { 365 if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
332 this.defaultSortOrder = this.settings.defaultSortOrder; 366 this.defaultSortOrder = this.settings.defaultSortOrder;
333 } 367 }
334 - this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);  
335 - this.sortOrderProperty = toEntityColumnDef(this.pageLink.sortOrder.property, this.columns); 368 +
  369 + this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns);
  370 + let sortColumn: EntityColumn;
  371 + if (this.pageLink.sortOrder) {
  372 + sortColumn = findColumnByEntityKey(this.pageLink.sortOrder.key, this.columns);
  373 + }
  374 + this.sortOrderProperty = sortColumn ? sortColumn.def : null;
336 375
337 if (this.actionCellDescriptors.length) { 376 if (this.actionCellDescriptors.length) {
338 this.displayedColumns.push('actions'); 377 this.displayedColumns.push('actions');
339 } 378 }
340 this.entityDatasource = new EntityDatasource( 379 this.entityDatasource = new EntityDatasource(
341 - this.translate, dataKeys, this.subscription.datasources); 380 + this.translate, dataKeys, this.subscription);
342 } 381 }
343 382
344 private editColumnsToDisplay($event: Event) { 383 private editColumnsToDisplay($event: Event) {
@@ -416,9 +455,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -416,9 +455,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
416 } else { 455 } else {
417 this.pageLink.page = 0; 456 this.pageLink.page = 0;
418 } 457 }
419 - this.pageLink.sortOrder.property = fromEntityColumnDef(this.sort.active, this.columns);  
420 - this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];  
421 - this.entityDatasource.loadEntities(this.pageLink); 458 + this.pageLink.sortOrder = {
  459 + key: findEntityKeyByColumnDef(this.sort.active, this.columns),
  460 + direction: Direction[this.sort.direction.toUpperCase()]
  461 + };
  462 + const keyFilters: KeyFilter[] = null; // TODO:
  463 + this.entityDatasource.loadEntities(this.pageLink, keyFilters);
422 this.ctx.detectChanges(); 464 this.ctx.detectChanges();
423 } 465 }
424 466
@@ -523,18 +565,19 @@ class EntityDatasource implements DataSource<EntityData> { @@ -523,18 +565,19 @@ class EntityDatasource implements DataSource<EntityData> {
523 private entitiesSubject = new BehaviorSubject<EntityData[]>([]); 565 private entitiesSubject = new BehaviorSubject<EntityData[]>([]);
524 private pageDataSubject = new BehaviorSubject<PageData<EntityData>>(emptyPageData<EntityData>()); 566 private pageDataSubject = new BehaviorSubject<PageData<EntityData>>(emptyPageData<EntityData>());
525 567
526 - private allEntities: Array<EntityData> = [];  
527 - private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]);  
528 - private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable(); 568 +// private allEntities: Array<EntityData> = [];
  569 +// private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]);
  570 +// private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable();
529 571
530 private currentEntity: EntityData = null; 572 private currentEntity: EntityData = null;
531 573
532 constructor( 574 constructor(
533 private translate: TranslateService, 575 private translate: TranslateService,
534 private dataKeys: Array<DataKey>, 576 private dataKeys: Array<DataKey>,
535 - datasources: Array<Datasource> 577 + private subscription: IWidgetSubscription
  578 + // datasources: Array<Datasource>
536 ) { 579 ) {
537 - 580 +/*
538 for (const datasource of datasources) { 581 for (const datasource of datasources) {
539 if (datasource.type === DatasourceType.entity && !datasource.entityId) { 582 if (datasource.type === DatasourceType.entity && !datasource.entityId) {
540 continue; 583 continue;
@@ -558,7 +601,7 @@ class EntityDatasource implements DataSource<EntityData> { @@ -558,7 +601,7 @@ class EntityDatasource implements DataSource<EntityData> {
558 }); 601 });
559 this.allEntities.push(entity); 602 this.allEntities.push(entity);
560 } 603 }
561 - this.allEntitiesSubject.next(this.allEntities); 604 + this.allEntitiesSubject.next(this.allEntities);*/
562 } 605 }
563 606
564 connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> { 607 connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> {
@@ -570,18 +613,63 @@ class EntityDatasource implements DataSource<EntityData> { @@ -570,18 +613,63 @@ class EntityDatasource implements DataSource<EntityData> {
570 this.pageDataSubject.complete(); 613 this.pageDataSubject.complete();
571 } 614 }
572 615
573 - loadEntities(pageLink: PageLink) {  
574 - this.fetchEntities(pageLink).pipe( 616 + loadEntities(pageLink: EntityDataPageLink, keyFilters: KeyFilter[]) {
  617 + this.subscription.subscribeForLatestData(0, pageLink, keyFilters);
  618 +/* this.fetchEntities(pageLink).pipe(
575 catchError(() => of(emptyPageData<EntityData>())), 619 catchError(() => of(emptyPageData<EntityData>())),
576 ).subscribe( 620 ).subscribe(
577 (pageData) => { 621 (pageData) => {
578 this.entitiesSubject.next(pageData.data); 622 this.entitiesSubject.next(pageData.data);
579 this.pageDataSubject.next(pageData); 623 this.pageDataSubject.next(pageData);
580 } 624 }
581 - ); 625 + );*/
582 } 626 }
583 627
584 - updateEntitiesData(data: DatasourceData[]) { 628 + dataUpdated() {
  629 + const datasourcesPageData = this.subscription.datasourcePages[0];
  630 + const dataPageData = this.subscription.dataPages[0];
  631 + const entities = new Array<EntityData>();
  632 + datasourcesPageData.data.forEach((datasource, index) => {
  633 + entities.push(this.datasourceToEntityData(datasource, dataPageData.data[index]));
  634 + });
  635 + const entitiesPageData: PageData<EntityData> = {
  636 + data: entities,
  637 + totalPages: datasourcesPageData.totalPages,
  638 + totalElements: datasourcesPageData.totalElements,
  639 + hasNext: datasourcesPageData.hasNext
  640 + };
  641 + this.entitiesSubject.next(entities);
  642 + this.pageDataSubject.next(entitiesPageData);
  643 + }
  644 +
  645 + private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData {
  646 + const entity: EntityData = {
  647 + id: {} as EntityId,
  648 + entityName: datasource.entityName,
  649 + entityLabel: datasource.entityLabel ? datasource.entityLabel : datasource.entityName
  650 + };
  651 + if (datasource.entityId) {
  652 + entity.id.id = datasource.entityId;
  653 + }
  654 + if (datasource.entityType) {
  655 + entity.id.entityType = datasource.entityType;
  656 + entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type);
  657 + } else {
  658 + entity.entityType = '';
  659 + }
  660 + this.dataKeys.forEach((dataKey, index) => {
  661 + const keyData = data[index].data;
  662 + if (keyData && keyData.length && keyData[0].length > 1) {
  663 + const value = keyData[0][1];
  664 + entity[dataKey.label] = value;
  665 + } else {
  666 + entity[dataKey.label] = '';
  667 + }
  668 + });
  669 + return entity;
  670 + }
  671 +
  672 +/* updateEntitiesData(data: DatasourceData[]) {
585 for (let i = 0; i < this.allEntities.length; i++) { 673 for (let i = 0; i < this.allEntities.length; i++) {
586 const entity = this.allEntities[i]; 674 const entity = this.allEntities[i];
587 for (let a = 0; a < this.dataKeys.length; a++) { 675 for (let a = 0; a < this.dataKeys.length; a++) {
@@ -597,7 +685,7 @@ class EntityDatasource implements DataSource<EntityData> { @@ -597,7 +685,7 @@ class EntityDatasource implements DataSource<EntityData> {
597 } 685 }
598 } 686 }
599 this.allEntitiesSubject.next(this.allEntities); 687 this.allEntitiesSubject.next(this.allEntities);
600 - } 688 + }*/
601 689
602 isEmpty(): Observable<boolean> { 690 isEmpty(): Observable<boolean> {
603 return this.entitiesSubject.pipe( 691 return this.entitiesSubject.pipe(
@@ -625,9 +713,9 @@ class EntityDatasource implements DataSource<EntityData> { @@ -625,9 +713,9 @@ class EntityDatasource implements DataSource<EntityData> {
625 (this.currentEntity.id.id === entity.id.id); 713 (this.currentEntity.id.id === entity.id.id);
626 } 714 }
627 715
628 - private fetchEntities(pageLink: PageLink): Observable<PageData<EntityData>> { 716 + /* private fetchEntities(pageLink: PageLink): Observable<PageData<EntityData>> {
629 return this.allEntities$.pipe( 717 return this.allEntities$.pipe(
630 map((data) => pageLink.filterData(data)) 718 map((data) => pageLink.filterData(data))
631 ); 719 );
632 - } 720 + }*/
633 } 721 }
@@ -19,6 +19,7 @@ import { DataKey, WidgetConfig } from '@shared/models/widget.models'; @@ -19,6 +19,7 @@ import { DataKey, WidgetConfig } from '@shared/models/widget.models';
19 import { getDescendantProp, isDefined } from '@core/utils'; 19 import { getDescendantProp, isDefined } from '@core/utils';
20 import { alarmFields, AlarmInfo } from '@shared/models/alarm.models'; 20 import { alarmFields, AlarmInfo } from '@shared/models/alarm.models';
21 import * as tinycolor_ from 'tinycolor2'; 21 import * as tinycolor_ from 'tinycolor2';
  22 +import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models';
22 23
23 const tinycolor = tinycolor_; 24 const tinycolor = tinycolor_;
24 25
@@ -49,6 +50,7 @@ export interface EntityData { @@ -49,6 +50,7 @@ export interface EntityData {
49 export interface EntityColumn extends DataKey { 50 export interface EntityColumn extends DataKey {
50 def: string; 51 def: string;
51 title: string; 52 title: string;
  53 + entityKey?: EntityKey;
52 } 54 }
53 55
54 export interface DisplayColumn { 56 export interface DisplayColumn {
@@ -73,6 +75,58 @@ export interface CellStyleInfo { @@ -73,6 +75,58 @@ export interface CellStyleInfo {
73 cellStyleFunction?: CellStyleFunction; 75 cellStyleFunction?: CellStyleFunction;
74 } 76 }
75 77
  78 +
  79 +export function entityDataSortOrderFromString(strSortOrder: string, columns: EntityColumn[]): EntityDataSortOrder {
  80 + if (!strSortOrder && !strSortOrder.length) {
  81 + return null;
  82 + }
  83 + let property: string;
  84 + let direction = Direction.ASC;
  85 + if (strSortOrder.startsWith('-')) {
  86 + direction = Direction.DESC;
  87 + property = strSortOrder.substring(1);
  88 + } else {
  89 + if (strSortOrder.startsWith('+')) {
  90 + property = strSortOrder.substring(1);
  91 + } else {
  92 + property = strSortOrder;
  93 + }
  94 + }
  95 + if (!property && !property.length) {
  96 + return null;
  97 + }
  98 + const column = findColumnByLabel(property, columns);
  99 + if (column && column.entityKey) {
  100 + return {key: column.entityKey, direction};
  101 + }
  102 + return null;
  103 +}
  104 +
  105 +export function findColumnByEntityKey(key: EntityKey, columns: EntityColumn[]): EntityColumn {
  106 + if (key) {
  107 + return columns.find(theColumn => theColumn.entityKey &&
  108 + theColumn.entityKey.type === key.type && theColumn.entityKey.key === key.key);
  109 + } else {
  110 + return null;
  111 + }
  112 +}
  113 +
  114 +export function findEntityKeyByColumnDef(def: string, columns: EntityColumn[]): EntityKey {
  115 + return findColumnByDef(def, columns).entityKey;
  116 +}
  117 +
  118 +export function findColumn(searchProperty: string, searchValue: string, columns: EntityColumn[]): EntityColumn {
  119 + return columns.find(theColumn => theColumn[searchProperty] === searchValue);
  120 +}
  121 +
  122 +export function findColumnByLabel(label: string, columns: EntityColumn[]): EntityColumn {
  123 + return findColumn('label', label, columns);
  124 +}
  125 +
  126 +export function findColumnByDef(def: string, columns: EntityColumn[]): EntityColumn {
  127 + return findColumn('def', def, columns);
  128 +}
  129 +
76 export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string { 130 export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string {
77 let res = searchValue; 131 let res = searchValue;
78 const column = columns.find(theColumn => theColumn[searchProperty] === searchValue); 132 const column = columns.find(theColumn => theColumn[searchProperty] === searchValue);
@@ -82,6 +136,10 @@ export function findColumnProperty(searchProperty: string, searchValue: string, @@ -82,6 +136,10 @@ export function findColumnProperty(searchProperty: string, searchValue: string,
82 return res; 136 return res;
83 } 137 }
84 138
  139 +export function toEntityKey(def: string, columns: EntityColumn[]): string {
  140 + return findColumnProperty('def', def, 'label', columns);
  141 +}
  142 +
85 export function toEntityColumnDef(label: string, columns: EntityColumn[]): string { 143 export function toEntityColumnDef(label: string, columns: EntityColumn[]): string {
86 return findColumnProperty('label', label, 'def', columns); 144 return findColumnProperty('label', label, 'def', columns);
87 } 145 }
@@ -346,12 +346,19 @@ export class WidgetComponentService { @@ -346,12 +346,19 @@ export class WidgetComponentService {
346 } else { 346 } else {
347 result.typeParameters.useCustomDatasources = false; 347 result.typeParameters.useCustomDatasources = false;
348 } 348 }
  349 + if (isUndefined(result.typeParameters.hasDataPageLink)) {
  350 + result.typeParameters.hasDataPageLink = false;
  351 + }
349 if (isUndefined(result.typeParameters.maxDatasources)) { 352 if (isUndefined(result.typeParameters.maxDatasources)) {
350 result.typeParameters.maxDatasources = -1; 353 result.typeParameters.maxDatasources = -1;
351 } 354 }
352 if (isUndefined(result.typeParameters.maxDataKeys)) { 355 if (isUndefined(result.typeParameters.maxDataKeys)) {
353 result.typeParameters.maxDataKeys = -1; 356 result.typeParameters.maxDataKeys = -1;
354 } 357 }
  358 + if (isUndefined(result.typeParameters.singleEntity)) {
  359 + result.typeParameters.singleEntity = result.typeParameters.maxDatasources === 1 &&
  360 + result.typeParameters.maxDataKeys === 1;
  361 + }
355 if (isUndefined(result.typeParameters.dataKeysOptional)) { 362 if (isUndefined(result.typeParameters.dataKeysOptional)) {
356 result.typeParameters.dataKeysOptional = false; 363 result.typeParameters.dataKeysOptional = false;
357 } 364 }
@@ -620,14 +620,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -620,14 +620,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
620 620
621 this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe( 621 this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe(
622 (dashboardTimewindow) => { 622 (dashboardTimewindow) => {
623 - // TODO:  
624 - let subscriptionChanged = false;  
625 for (const id of Object.keys(this.widgetContext.subscriptions)) { 623 for (const id of Object.keys(this.widgetContext.subscriptions)) {
626 const subscription = this.widgetContext.subscriptions[id]; 624 const subscription = this.widgetContext.subscriptions[id];
627 - subscriptionChanged = subscriptionChanged || subscription.onDashboardTimewindowChanged(dashboardTimewindow);  
628 - }  
629 - if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {  
630 - this.reInit(); 625 + subscription.onDashboardTimewindowChanged(dashboardTimewindow);
631 } 626 }
632 } 627 }
633 )); 628 ));
@@ -845,6 +840,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -845,6 +840,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
845 options = { 840 options = {
846 type: this.widget.type, 841 type: this.widget.type,
847 stateData: this.typeParameters.stateData, 842 stateData: this.typeParameters.stateData,
  843 + hasDataPageLink: this.typeParameters.hasDataPageLink,
  844 + singleEntity: this.typeParameters.singleEntity,
848 comparisonEnabled: comparisonSettings.comparisonEnabled, 845 comparisonEnabled: comparisonSettings.comparisonEnabled,
849 timeForComparison: comparisonSettings.timeForComparison 846 timeForComparison: comparisonSettings.timeForComparison
850 }; 847 };
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 import { AliasFilterType, EntityFilters } from '@shared/models/alias.models'; 17 import { AliasFilterType, EntityFilters } from '@shared/models/alias.models';
18 import { EntityId } from '@shared/models/id/entity-id'; 18 import { EntityId } from '@shared/models/id/entity-id';
  19 +import { SortDirection } from '@angular/material/sort';
19 20
20 export enum EntityKeyType { 21 export enum EntityKeyType {
21 ATTRIBUTE = 'ATTRIBUTE', 22 ATTRIBUTE = 'ATTRIBUTE',
@@ -122,18 +123,30 @@ export interface EntityDataPageLink { @@ -122,18 +123,30 @@ export interface EntityDataPageLink {
122 sortOrder?: EntityDataSortOrder; 123 sortOrder?: EntityDataSortOrder;
123 } 124 }
124 125
125 -export const defaultEntityDataPageLink: EntityDataPageLink = {  
126 - pageSize: 1024,  
127 - page: 0,  
128 - sortOrder: {  
129 - key: {  
130 - type: EntityKeyType.ENTITY_FIELD,  
131 - key: 'createdTime'  
132 - },  
133 - direction: Direction.DESC 126 +export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection {
  127 + if (pageLink.sortOrder) {
  128 + return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection;
  129 + } else {
  130 + return '' as SortDirection;
134 } 131 }
135 } 132 }
136 133
  134 +export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPageLink {
  135 + return {
  136 + pageSize,
  137 + page: 0,
  138 + sortOrder: {
  139 + key: {
  140 + type: EntityKeyType.ENTITY_FIELD,
  141 + key: 'createdTime'
  142 + },
  143 + direction: Direction.DESC
  144 + }
  145 + }
  146 +}
  147 +
  148 +export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1024);
  149 +
137 export interface EntityCountQuery { 150 export interface EntityCountQuery {
138 entityFilter: EntityFilter; 151 entityFilter: EntityFilter;
139 } 152 }
@@ -21,7 +21,7 @@ import { Observable, ReplaySubject, Subject } from 'rxjs'; @@ -21,7 +21,7 @@ import { Observable, ReplaySubject, Subject } from 'rxjs';
21 import { EntityId } from '@shared/models/id/entity-id'; 21 import { EntityId } from '@shared/models/id/entity-id';
22 import { map } from 'rxjs/operators'; 22 import { map } from 'rxjs/operators';
23 import { NgZone } from '@angular/core'; 23 import { NgZone } from '@angular/core';
24 -import { EntityData, EntityDataQuery } from '@shared/models/query/query.models'; 24 +import { EntityData, EntityDataQuery, EntityKey } from '@shared/models/query/query.models';
25 import { PageData } from '@shared/models/page/page-data'; 25 import { PageData } from '@shared/models/page/page-data';
26 26
27 export enum DataKeyType { 27 export enum DataKeyType {
@@ -139,7 +139,7 @@ export interface EntityHistoryCmd { @@ -139,7 +139,7 @@ export interface EntityHistoryCmd {
139 } 139 }
140 140
141 export interface LatestValueCmd { 141 export interface LatestValueCmd {
142 - keys: Array<string>; 142 + keys: Array<EntityKey>;
143 } 143 }
144 144
145 export interface TimeSeriesCmd { 145 export interface TimeSeriesCmd {
@@ -153,7 +153,7 @@ export interface TimeSeriesCmd { @@ -153,7 +153,7 @@ export interface TimeSeriesCmd {
153 153
154 export class EntityDataCmd implements WebsocketCmd { 154 export class EntityDataCmd implements WebsocketCmd {
155 cmdId: number; 155 cmdId: number;
156 - query: EntityDataQuery; 156 + query?: EntityDataQuery;
157 historyCmd?: EntityHistoryCmd; 157 historyCmd?: EntityHistoryCmd;
158 latestCmd?: LatestValueCmd; 158 latestCmd?: LatestValueCmd;
159 tsCmd?: TimeSeriesCmd; 159 tsCmd?: TimeSeriesCmd;
@@ -314,6 +314,7 @@ export class EntityDataUpdate implements EntityDataUpdateMsg { @@ -314,6 +314,7 @@ export class EntityDataUpdate implements EntityDataUpdateMsg {
314 314
315 export interface TelemetryService { 315 export interface TelemetryService {
316 subscribe(subscriber: TelemetrySubscriber); 316 subscribe(subscriber: TelemetrySubscriber);
  317 + update(subscriber: TelemetrySubscriber);
317 unsubscribe(subscriber: TelemetrySubscriber); 318 unsubscribe(subscriber: TelemetrySubscriber);
318 } 319 }
319 320
@@ -360,6 +361,10 @@ export class TelemetrySubscriber { @@ -360,6 +361,10 @@ export class TelemetrySubscriber {
360 this.telemetryService.subscribe(this); 361 this.telemetryService.subscribe(this);
361 } 362 }
362 363
  364 + public update() {
  365 + this.telemetryService.update(this);
  366 + }
  367 +
363 public unsubscribe() { 368 public unsubscribe() {
364 this.telemetryService.unsubscribe(this); 369 this.telemetryService.unsubscribe(this);
365 this.complete(); 370 this.complete();
@@ -150,6 +150,8 @@ export interface WidgetTypeParameters { @@ -150,6 +150,8 @@ export interface WidgetTypeParameters {
150 maxDataKeys?: number; 150 maxDataKeys?: number;
151 dataKeysOptional?: boolean; 151 dataKeysOptional?: boolean;
152 stateData?: boolean; 152 stateData?: boolean;
  153 + hasDataPageLink?: boolean;
  154 + singleEntity?: boolean;
153 } 155 }
154 156
155 export interface WidgetControllerDescriptor { 157 export interface WidgetControllerDescriptor {