Commit 92c4d00fbbefd03482a2b684401efa9e514352d3

Authored by Igor Kulikov
1 parent 2eb7949d

UI: Refactor entities hierarchy widget to use new WS data query API.

@@ -54,22 +54,6 @@ @@ -54,22 +54,6 @@
54 } 54 }
55 }, 55 },
56 { 56 {
57 - "alias": "entities_hierarchy",  
58 - "name": "Entities hierarchy",  
59 - "descriptor": {  
60 - "type": "latest",  
61 - "sizeX": 7.5,  
62 - "sizeY": 3.5,  
63 - "resources": [],  
64 - "templateHtml": "<tb-entities-hierarchy-widget \n [ctx]=\"ctx\">\n</tb-entities-hierarchy-widget>",  
65 - "templateCss": "",  
66 - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",  
67 - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",  
68 - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}",  
69 - "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\":{}}"  
70 - }  
71 - },  
72 - {  
73 "alias": "html_value_card", 57 "alias": "html_value_card",
74 "name": "HTML Value Card", 58 "name": "HTML Value Card",
75 "descriptor": { 59 "descriptor": {
@@ -132,6 +116,22 @@ @@ -132,6 +116,22 @@
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}", 116 "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;\"}]}]}" 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\":{\"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 } 118 }
  119 + },
  120 + {
  121 + "alias": "entities_hierarchy",
  122 + "name": "Entities hierarchy",
  123 + "descriptor": {
  124 + "type": "latest",
  125 + "sizeX": 7.5,
  126 + "sizeY": 3.5,
  127 + "resources": [],
  128 + "templateHtml": "<tb-entities-hierarchy-widget \n [ctx]=\"ctx\">\n</tb-entities-hierarchy-widget>",
  129 + "templateCss": "",
  130 + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
  131 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
  132 + "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: \\\"FROM\\\",\\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 + }
135 } 135 }
136 ] 136 ]
137 } 137 }
@@ -8997,10 +8997,10 @@ @@ -8997,10 +8997,10 @@
8997 "integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw==" 8997 "integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw=="
8998 }, 8998 },
8999 "ngx-flowchart": { 8999 "ngx-flowchart": {
9000 - "version": "git://github.com/thingsboard/ngx-flowchart.git#a4157b0eef2eb3646ef920447c7b06b39d54f87f", 9000 + "version": "git://github.com/thingsboard/ngx-flowchart.git#7a02f4748b5e7821a883c903107af5f20415d026",
9001 "from": "git://github.com/thingsboard/ngx-flowchart.git#master", 9001 "from": "git://github.com/thingsboard/ngx-flowchart.git#master",
9002 "requires": { 9002 "requires": {
9003 - "tslib": "^1.10.0" 9003 + "tslib": "^1.13.0"
9004 }, 9004 },
9005 "dependencies": { 9005 "dependencies": {
9006 "tslib": { 9006 "tslib": {
@@ -20,7 +20,7 @@ import { Datasource, DatasourceType } from '@app/shared/models/widget.models'; @@ -20,7 +20,7 @@ import { Datasource, DatasourceType } from '@app/shared/models/widget.models';
20 import { deepClone, isEqual } from '@core/utils'; 20 import { deepClone, isEqual } from '@core/utils';
21 import { EntityService } from '@core/http/entity.service'; 21 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 { EntityAliases } from '@shared/models/alias.models'; 23 +import { AliasFilterType, EntityAliases, SingleEntityFilter } from '@shared/models/alias.models';
24 import { EntityInfo } from '@shared/models/entity.models'; 24 import { EntityInfo } from '@shared/models/entity.models';
25 import { map, mergeMap } from 'rxjs/operators'; 25 import { map, mergeMap } from 'rxjs/operators';
26 import { 26 import {
@@ -282,6 +282,15 @@ export class AliasController implements IAliasController { @@ -282,6 +282,15 @@ export class AliasController implements IAliasController {
282 } 282 }
283 }) 283 })
284 ); 284 );
  285 + } else if (newDatasource.entityId && !newDatasource.entityFilter) {
  286 + newDatasource.entityFilter = {
  287 + singleEntity: {
  288 + id: newDatasource.entityId,
  289 + entityType: newDatasource.entityType,
  290 + },
  291 + type: AliasFilterType.singleEntity
  292 + } as SingleEntityFilter;
  293 + return of(newDatasource);
285 } else { 294 } else {
286 newDatasource.aliasName = newDatasource.entityName; 295 newDatasource.aliasName = newDatasource.entityName;
287 newDatasource.name = newDatasource.entityName; 296 newDatasource.name = newDatasource.entityName;
@@ -23,19 +23,18 @@ import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@share @@ -23,19 +23,18 @@ import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@share
23 import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; 23 import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
24 import { UtilsService } from '@core/services/utils.service'; 24 import { UtilsService } from '@core/services/utils.service';
25 import cssjs from '@core/css/css'; 25 import cssjs from '@core/css/css';
26 -import { forkJoin, fromEvent, Observable, of } from 'rxjs';  
27 -import { catchError, debounceTime, distinctUntilChanged, map, mergeMap, tap } from 'rxjs/operators'; 26 +import { fromEvent } from 'rxjs';
  27 +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
28 import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; 28 import { constructTableCssString } from '@home/components/widget/lib/table-widget.models';
29 import { Overlay } from '@angular/cdk/overlay'; 29 import { Overlay } from '@angular/cdk/overlay';
30 import { 30 import {
31 LoadNodesCallback, 31 LoadNodesCallback,
32 NavTreeEditCallbacks, 32 NavTreeEditCallbacks,
  33 + NodesCallback,
33 NodeSearchCallback, 34 NodeSearchCallback,
34 NodeSelectedCallback, 35 NodeSelectedCallback,
35 NodesInsertedCallback 36 NodesInsertedCallback
36 } from '@shared/components/nav-tree.component'; 37 } from '@shared/components/nav-tree.component';
37 -import { BaseData } from '@shared/models/base-data';  
38 -import { EntityId } from '@shared/models/id/entity-id';  
39 import { EntityType } from '@shared/models/entity-type.models'; 38 import { EntityType } from '@shared/models/entity-type.models';
40 import { deepClone, hashCode } from '@core/utils'; 39 import { deepClone, hashCode } from '@core/utils';
41 import { 40 import {
@@ -58,10 +57,9 @@ import { @@ -58,10 +57,9 @@ import {
58 NodesSortFunction, 57 NodesSortFunction,
59 NodeTextFunction 58 NodeTextFunction
60 } from '@home/components/widget/lib/entities-hierarchy-widget.models'; 59 } from '@home/components/widget/lib/entities-hierarchy-widget.models';
61 -import { EntityService } from '@core/http/entity.service';  
62 -import { EntityRelationsQuery, EntitySearchDirection } from '@shared/models/relation.models';  
63 -import { EntityRelationService } from '@core/http/entity-relation.service';  
64 -import { ActionNotificationShow } from '@core/notification/notification.actions'; 60 +import { EntityRelationsQuery } from '@shared/models/relation.models';
  61 +import { AliasFilterType, RelationsQueryFilter } from '@shared/models/alias.models';
  62 +import { EntityFilter } from '@shared/models/query/query.models';
65 63
66 @Component({ 64 @Component({
67 selector: 'tb-entities-hierarchy-widget', 65 selector: 'tb-entities-hierarchy-widget',
@@ -86,6 +84,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -86,6 +84,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
86 private widgetConfig: WidgetConfig; 84 private widgetConfig: WidgetConfig;
87 private subscription: IWidgetSubscription; 85 private subscription: IWidgetSubscription;
88 private datasources: Array<HierarchyNodeDatasource>; 86 private datasources: Array<HierarchyNodeDatasource>;
  87 + private data: Array<Array<DatasourceData>>;
89 88
90 private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; 89 private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {};
91 private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; 90 private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {};
@@ -108,14 +107,11 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -108,14 +107,11 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
108 } 107 }
109 }; 108 };
110 109
111 -  
112 constructor(protected store: Store<AppState>, 110 constructor(protected store: Store<AppState>,
113 private elementRef: ElementRef, 111 private elementRef: ElementRef,
114 private overlay: Overlay, 112 private overlay: Overlay,
115 private viewContainerRef: ViewContainerRef, 113 private viewContainerRef: ViewContainerRef,
116 - private utils: UtilsService,  
117 - private entityService: EntityService,  
118 - private entityRelationService: EntityRelationService) { 114 + private utils: UtilsService) {
119 super(store); 115 super(store);
120 } 116 }
121 117
@@ -125,6 +121,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -125,6 +121,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
125 this.widgetConfig = this.ctx.widgetConfig; 121 this.widgetConfig = this.ctx.widgetConfig;
126 this.subscription = this.ctx.defaultSubscription; 122 this.subscription = this.ctx.defaultSubscription;
127 this.datasources = this.subscription.datasources as Array<HierarchyNodeDatasource>; 123 this.datasources = this.subscription.datasources as Array<HierarchyNodeDatasource>;
  124 + this.data = this.subscription.dataPages[0].data;
128 this.initializeConfig(); 125 this.initializeConfig();
129 this.ctx.updateWidgetParams(); 126 this.ctx.updateWidgetParams();
130 } 127 }
@@ -254,33 +251,15 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -254,33 +251,15 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
254 251
255 public loadNodes: LoadNodesCallback = (node, cb) => { 252 public loadNodes: LoadNodesCallback = (node, cb) => {
256 if (node.id === '#') { 253 if (node.id === '#') {
257 - const tasks: Observable<HierarchyNavTreeNode>[] = [];  
258 - this.datasources.forEach((datasource) => {  
259 - tasks.push(this.datasourceToNode(datasource));  
260 - });  
261 - forkJoin(tasks).subscribe((nodes) => {  
262 - cb(this.prepareNodes(nodes));  
263 - this.updateNodeData(this.subscription.data); 254 + const childNodes: HierarchyNavTreeNode[] = [];
  255 + this.datasources.forEach((childDatasource, index) => {
  256 + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, this.data[index]));
264 }); 257 });
  258 + cb(this.prepareNodes(childNodes));
265 } else { 259 } else {
266 if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { 260 if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') {
267 - const relationQuery = this.prepareNodeRelationQuery(node.data.nodeCtx);  
268 - this.entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).subscribe(  
269 - (entityRelations) => {  
270 - if (entityRelations.length) {  
271 - const tasks: Observable<HierarchyNavTreeNode>[] = [];  
272 - entityRelations.forEach((relation) => {  
273 - const targetId = relationQuery.parameters.direction === EntitySearchDirection.FROM ? relation.to : relation.from;  
274 - tasks.push(this.entityIdToNode(targetId.entityType as EntityType, targetId.id, node.data.datasource, node.data.nodeCtx));  
275 - });  
276 - forkJoin(tasks).subscribe((nodes) => {  
277 - cb(this.prepareNodes(nodes));  
278 - });  
279 - } else {  
280 - cb([]);  
281 - }  
282 - },  
283 - (error) => { 261 + this.loadChildren(node, node.data.datasource, cb);
  262 + /* (error) => { // TODO:
284 let errorText = 'Failed to get relations!'; 263 let errorText = 'Failed to get relations!';
285 if (error && error.status === 400) { 264 if (error && error.status === 400) {
286 errorText = 'Invalid relations query returned by \'Node relations query function\'! Please check widget configuration!'; 265 errorText = 'Invalid relations query returned by \'Node relations query function\'! Please check widget configuration!';
@@ -288,6 +267,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -288,6 +267,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
288 this.showError(errorText); 267 this.showError(errorText);
289 } 268 }
290 ); 269 );
  270 + */
291 } else { 271 } else {
292 cb([]); 272 cb([]);
293 } 273 }
@@ -313,7 +293,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -313,7 +293,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
313 } 293 }
314 } 294 }
315 295
316 - public onNodesInserted: NodesInsertedCallback = (nodes, parent) => { 296 + public onNodesInserted: NodesInsertedCallback = (nodes) => {
317 if (nodes) { 297 if (nodes) {
318 nodes.forEach((nodeId) => { 298 nodes.forEach((nodeId) => {
319 const task = this.pendingUpdateNodeTasks[nodeId]; 299 const task = this.pendingUpdateNodeTasks[nodeId];
@@ -355,17 +335,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -355,17 +335,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
355 } 335 }
356 } 336 }
357 337
358 - private showError(errorText: string) {  
359 - this.store.dispatch(new ActionNotificationShow(  
360 - {  
361 - message: errorText,  
362 - type: 'error',  
363 - target: this.toastTargetId,  
364 - verticalPosition: 'bottom',  
365 - horizontalPosition: 'left'  
366 - }));  
367 - }  
368 -  
369 private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { 338 private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] {
370 nodes = nodes.filter((node) => node !== null); 339 nodes = nodes.filter((node) => node !== null);
371 nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); 340 nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx));
@@ -399,85 +368,87 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -399,85 +368,87 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
399 } 368 }
400 } 369 }
401 370
402 - private datasourceToNode(datasource: HierarchyNodeDatasource, parentNodeCtx?: HierarchyNodeContext): Observable<HierarchyNavTreeNode> {  
403 - return this.resolveEntity(datasource).pipe(  
404 - map(entity => {  
405 - if (entity !== null) {  
406 - const node: HierarchyNavTreeNode = {  
407 - id: (++this.nodeIdCounter) + ''  
408 - };  
409 - this.nodesMap[node.id] = node;  
410 - datasource.nodeId = node.id;  
411 - node.icon = false;  
412 - const nodeCtx: HierarchyNodeContext = {  
413 - parentNodeCtx,  
414 - entity,  
415 - data: {}  
416 - };  
417 - nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1;  
418 - node.data = {  
419 - datasource,  
420 - nodeCtx  
421 - };  
422 - node.state = {  
423 - disabled: this.nodeDisabledFunction(node.data.nodeCtx),  
424 - opened: this.nodeOpenedFunction(node.data.nodeCtx)  
425 - };  
426 - node.text = this.prepareNodeText(node);  
427 - node.children = this.nodeHasChildrenFunction(node.data.nodeCtx);  
428 - return node;  
429 - } else {  
430 - return null;  
431 - }  
432 - })  
433 - ); 371 + private datasourceToNode(datasource: HierarchyNodeDatasource,
  372 + data: DatasourceData[],
  373 + parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode {
  374 + const node: HierarchyNavTreeNode = {
  375 + id: (++this.nodeIdCounter) + ''
  376 + };
  377 + this.nodesMap[node.id] = node;
  378 + datasource.nodeId = node.id;
  379 + node.icon = false;
  380 + const nodeCtx: HierarchyNodeContext = {
  381 + parentNodeCtx,
  382 + entity: {
  383 + id: {
  384 + id: datasource.entityId,
  385 + entityType: datasource.entityType
  386 + },
  387 + name: datasource.entityName,
  388 + label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName
  389 + },
  390 + data: {}
  391 + };
  392 + datasource.dataKeys.forEach((dataKey, index) => {
  393 + const keyData = data[index].data;
  394 + if (keyData && keyData.length && keyData[0].length > 1) {
  395 + nodeCtx.data[dataKey.label] = keyData[0][1];
  396 + } else {
  397 + nodeCtx.data[dataKey.label] = '';
  398 + }
  399 + });
  400 + nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1;
  401 + node.data = {
  402 + datasource,
  403 + nodeCtx
  404 + };
  405 + node.state = {
  406 + disabled: this.nodeDisabledFunction(node.data.nodeCtx),
  407 + opened: this.nodeOpenedFunction(node.data.nodeCtx)
  408 + };
  409 + node.text = this.prepareNodeText(node);
  410 + node.children = this.nodeHasChildrenFunction(node.data.nodeCtx);
  411 + return node;
434 } 412 }
435 413
436 - private entityIdToNode(entityType: EntityType, entityId: string,  
437 - parentDatasource: HierarchyNodeDatasource,  
438 - parentNodeCtx: HierarchyNodeContext): Observable<HierarchyNavTreeNode> {  
439 - const datasource = {  
440 - dataKeys: parentDatasource.dataKeys, 414 + private loadChildren(parentNode: HierarchyNavTreeNode, datasource: HierarchyNodeDatasource, childrenNodesLoadCb: NodesCallback) {
  415 + const nodeCtx = parentNode.data.nodeCtx;
  416 + nodeCtx.childrenNodesLoaded = false;
  417 + const entityFilter = this.prepareNodeRelationsQueryFilter(nodeCtx);
  418 + const childrenDatasource = {
  419 + dataKeys: datasource.dataKeys,
441 type: DatasourceType.entity, 420 type: DatasourceType.entity,
442 - entityType,  
443 - entityId 421 + entityFilter
444 } as HierarchyNodeDatasource; 422 } as HierarchyNodeDatasource;
445 - return this.datasourceToNode(datasource, parentNodeCtx).pipe(  
446 - mergeMap((node) => {  
447 - if (node != null) {  
448 - const subscriptionOptions: WidgetSubscriptionOptions = {  
449 - type: widgetType.latest,  
450 - datasources: [datasource],  
451 - callbacks: {  
452 - onDataUpdated: subscription => {  
453 - this.updateNodeData(subscription.data);  
454 - }  
455 - }  
456 - };  
457 - return this.ctx.subscriptionApi.  
458 - createSubscription(subscriptionOptions, true).pipe(  
459 - map(() => node));  
460 - } else {  
461 - return of(node);  
462 - }  
463 - })  
464 - );  
465 - }  
466 -  
467 - private resolveEntity(datasource: HierarchyNodeDatasource): Observable<BaseData<EntityId>> {  
468 - if (datasource.type === DatasourceType.function) {  
469 - const entity = {  
470 - id: {  
471 - entityType: 'function' 423 + const subscriptionOptions: WidgetSubscriptionOptions = {
  424 + type: widgetType.latest,
  425 + datasources: [childrenDatasource],
  426 + callbacks: {
  427 + onSubscriptionMessage: (subscription, message) => {
  428 + this.ctx.showToast(message.severity, message.message, undefined,
  429 + 'bottom', 'left', this.toastTargetId);
472 }, 430 },
473 - name: datasource.name  
474 - };  
475 - return of(entity as BaseData<EntityId>);  
476 - } else {  
477 - return this.entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).pipe(  
478 - catchError(err => of(null))  
479 - );  
480 - } 431 + onInitialPageDataChanged: (subscription) => {
  432 + this.ctx.subscriptionApi.removeSubscription(subscription.id);
  433 + this.nodeEditCallbacks.refreshNode(parentNode.id);
  434 + },
  435 + onDataUpdated: subscription => {
  436 + if (nodeCtx.childrenNodesLoaded) {
  437 + this.updateNodeData(subscription.data);
  438 + } else {
  439 + const datasourcesPageData = subscription.datasourcePages[0];
  440 + const dataPageData = subscription.dataPages[0];
  441 + const childNodes: HierarchyNavTreeNode[] = [];
  442 + datasourcesPageData.data.forEach((childDatasource, index) => {
  443 + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index]));
  444 + });
  445 + nodeCtx.childrenNodesLoaded = true;
  446 + childrenNodesLoadCb(this.prepareNodes(childNodes));
  447 + }
  448 + }
  449 + }
  450 + };
  451 + this.ctx.subscriptionApi.createSubscription(subscriptionOptions, true);
481 } 452 }
482 453
483 private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { 454 private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery {
@@ -487,4 +458,19 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -487,4 +458,19 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
487 } 458 }
488 return relationQuery as EntityRelationsQuery; 459 return relationQuery as EntityRelationsQuery;
489 } 460 }
  461 +
  462 + private prepareNodeRelationsQueryFilter(nodeCtx: HierarchyNodeContext): EntityFilter {
  463 + const relationQuery = this.prepareNodeRelationQuery(nodeCtx);
  464 + return {
  465 + rootEntity: {
  466 + id: relationQuery.parameters.rootId,
  467 + entityType: relationQuery.parameters.rootType
  468 + },
  469 + direction: relationQuery.parameters.direction,
  470 + filters: relationQuery.filters,
  471 + maxLevel: relationQuery.parameters.maxLevel,
  472 + fetchLastLevelOnly: relationQuery.parameters.fetchLastLevelOnly,
  473 + type: AliasFilterType.relationsQuery
  474 + } as RelationsQueryFilter;
  475 + }
490 } 476 }
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 import { BaseData } from '@shared/models/base-data'; 17 import { BaseData } from '@shared/models/base-data';
18 import { EntityId } from '@shared/models/id/entity-id'; 18 import { EntityId } from '@shared/models/id/entity-id';
19 -import { NavTreeNode } from '@shared/components/nav-tree.component'; 19 +import { NavTreeNode, NodesCallback } from '@shared/components/nav-tree.component';
20 import { Datasource } from '@shared/models/widget.models'; 20 import { Datasource } from '@shared/models/widget.models';
21 import { isDefined, isUndefined } from '@core/utils'; 21 import { isDefined, isUndefined } from '@core/utils';
22 import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; 22 import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models';
@@ -35,6 +35,7 @@ export interface EntitiesHierarchyWidgetSettings { @@ -35,6 +35,7 @@ export interface EntitiesHierarchyWidgetSettings {
35 export interface HierarchyNodeContext { 35 export interface HierarchyNodeContext {
36 parentNodeCtx?: HierarchyNodeContext; 36 parentNodeCtx?: HierarchyNodeContext;
37 entity: BaseData<EntityId>; 37 entity: BaseData<EntityId>;
  38 + childrenNodesLoaded?: boolean;
38 level?: number; 39 level?: number;
39 data: {[key: string]: any}; 40 data: {[key: string]: any};
40 } 41 }