Showing
18 changed files
with
730 additions
and
413 deletions
@@ -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 { |