Showing
18 changed files
with
730 additions
and
413 deletions
... | ... | @@ -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 | 25 | "alias": "html_card", |
42 | 26 | "name": "HTML Card", |
43 | 27 | "descriptor": { |
... | ... | @@ -132,6 +116,22 @@ |
132 | 116 | "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", |
133 | 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 | } |
\ No newline at end of file | ... | ... |
... | ... | @@ -452,7 +452,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { |
452 | 452 | } |
453 | 453 | |
454 | 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 | 458 | private String typeQuery(EntityFilter filter) { | ... | ... |
... | ... | @@ -48,6 +48,7 @@ public class EntityKeyMapping { |
48 | 48 | |
49 | 49 | static { |
50 | 50 | entityFieldColumnMap.put("createdTime", "id"); |
51 | + entityFieldColumnMap.put("entityType", "entity_type"); | |
51 | 52 | entityFieldColumnMap.put("name", "name"); |
52 | 53 | entityFieldColumnMap.put("type", "type"); |
53 | 54 | entityFieldColumnMap.put("label", "label"); | ... | ... |
... | ... | @@ -22,8 +22,8 @@ import { EntityService } from '@core/http/entity.service'; |
22 | 22 | import { UtilsService } from '@core/services/utils.service'; |
23 | 23 | import { AliasFilterType, EntityAliases } from '@shared/models/alias.models'; |
24 | 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 | 28 | export class AliasController implements IAliasController { |
29 | 29 | |
... | ... | @@ -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 | 190 | if (datasource.type === DatasourceType.entity) { |
174 | 191 | if (datasource.entityAliasId) { |
175 | 192 | return this.getAliasInfo(datasource.entityAliasId).pipe( |
... | ... | @@ -200,14 +217,14 @@ export class AliasController implements IAliasController { |
200 | 217 | datasources.push(newDatasource); |
201 | 218 | } |
202 | 219 | return datasources;*/ |
203 | - return [newDatasource]; | |
220 | + return newDatasource; | |
204 | 221 | } else { |
205 | 222 | if (aliasInfo.stateEntity) { |
206 | 223 | newDatasource = deepClone(datasource); |
207 | 224 | newDatasource.unresolvedStateEntity = true; |
208 | - return [newDatasource]; | |
225 | + return newDatasource; | |
209 | 226 | } else { |
210 | - return []; | |
227 | + return null; | |
211 | 228 | // throw new Error('Unable to resolve datasource.'); |
212 | 229 | } |
213 | 230 | } |
... | ... | @@ -232,13 +249,13 @@ export class AliasController implements IAliasController { |
232 | 249 | entityType: entity.entityType |
233 | 250 | } |
234 | 251 | }; |
235 | - return [datasource]; | |
252 | + return datasource; | |
236 | 253 | } else { |
237 | 254 | if (aliasInfo.stateEntity) { |
238 | 255 | datasource.unresolvedStateEntity = true; |
239 | - return [datasource]; | |
256 | + return datasource; | |
240 | 257 | } else { |
241 | - return []; | |
258 | + return null; | |
242 | 259 | // throw new Error('Unable to resolve datasource.'); |
243 | 260 | } |
244 | 261 | } |
... | ... | @@ -248,10 +265,10 @@ export class AliasController implements IAliasController { |
248 | 265 | } else { |
249 | 266 | datasource.aliasName = datasource.entityName; |
250 | 267 | datasource.name = datasource.entityName; |
251 | - return of([datasource]); | |
268 | + return of(datasource); | |
252 | 269 | } |
253 | 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 | 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 | 377 | newDatasources.forEach((datasource) => { |
361 | 378 | observables.push(this.resolveDatasource(datasource)); |
362 | 379 | }); |
363 | 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 | 382 | let functionIndex = 0; |
370 | 383 | result.forEach((datasource) => { |
371 | 384 | if (datasource.type === DatasourceType.function) { |
... | ... | @@ -386,6 +399,9 @@ export class AliasController implements IAliasController { |
386 | 399 | datasource.name = 'Unresolved'; |
387 | 400 | datasource.entityName = 'Unresolved'; |
388 | 401 | } else if (datasource.type === DatasourceType.entity) { |
402 | + if (singleEntity) { | |
403 | + datasource.pageLink = createDefaultEntityDataPageLink(1); | |
404 | + } | |
389 | 405 | if (!datasource.pageLink) { |
390 | 406 | datasource.pageLink = deepClone(defaultEntityDataPageLink); |
391 | 407 | } | ... | ... |
... | ... | @@ -35,19 +35,21 @@ import { |
35 | 35 | TelemetrySubscriber |
36 | 36 | } from '@shared/models/telemetry/telemetry.models'; |
37 | 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 | 39 | import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils'; |
40 | 40 | import { PageData } from '@shared/models/page/page-data'; |
41 | 41 | import { DataAggregator } from '@core/api/data-aggregator'; |
42 | 42 | import { NULL_UUID } from '@shared/models/id/has-uuid'; |
43 | 43 | import { EntityType } from '@shared/models/entity-type.models'; |
44 | 44 | import Timeout = NodeJS.Timeout; |
45 | +import { Observable, of, ReplaySubject, Subject } from 'rxjs'; | |
45 | 46 | |
46 | 47 | export interface EntityDataSubscriptionOptions { |
47 | 48 | datasourceType: DatasourceType; |
48 | 49 | dataKeys: Array<SubscriptionDataKey>; |
49 | 50 | type: widgetType; |
50 | 51 | entityFilter?: EntityFilter; |
52 | + isLatestDataSubscription?: boolean; | |
51 | 53 | pageLink?: EntityDataPageLink; |
52 | 54 | keyFilters?: Array<KeyFilter>; |
53 | 55 | subscriptionTimewindow?: SubscriptionTimewindow; |
... | ... | @@ -59,21 +61,19 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyInd |
59 | 61 | |
60 | 62 | export class EntityDataSubscription { |
61 | 63 | |
62 | - private listeners: Array<EntityDataListener> = []; | |
63 | 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 | 68 | private subscriber: TelemetrySubscriber; |
69 | + private dataCommand: EntityDataCmd; | |
70 | + private subsCommand: EntityDataCmd; | |
72 | 71 | |
73 | 72 | private attrFields: Array<EntityKey>; |
74 | 73 | private tsFields: Array<EntityKey>; |
75 | 74 | private latestValues: Array<EntityKey>; |
76 | 75 | |
76 | + private entityDataResolveSubject: Subject<EntityDataLoadResult>; | |
77 | 77 | private pageData: PageData<EntityData>; |
78 | 78 | private subsTw: SubscriptionTimewindow; |
79 | 79 | private dataAggregators: Array<DataAggregator>; |
... | ... | @@ -87,7 +87,11 @@ export class EntityDataSubscription { |
87 | 87 | private tickElapsed = 0; |
88 | 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 | 95 | private telemetryService: TelemetryService, |
92 | 96 | private utils: UtilsService) { |
93 | 97 | this.initializeSubscription(); |
... | ... | @@ -126,50 +130,6 @@ export class EntityDataSubscription { |
126 | 130 | } |
127 | 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 | 135 | public unsubscribe() { |
... | ... | @@ -192,19 +152,30 @@ export class EntityDataSubscription { |
192 | 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 | 162 | if (this.datasourceType === DatasourceType.entity) { |
198 | 163 | const entityFields: Array<EntityKey> = |
199 | 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 | 167 | if (!entityFields.find(key => key.key === 'name')) { |
203 | 168 | entityFields.push({ |
204 | 169 | type: EntityKeyType.ENTITY_FIELD, |
205 | 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 | 180 | this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( |
210 | 181 | dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) |
... | ... | @@ -217,9 +188,9 @@ export class EntityDataSubscription { |
217 | 188 | this.latestValues = this.attrFields.concat(this.tsFields); |
218 | 189 | |
219 | 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 | 194 | entityFilter: this.entityDataSubscriptionOptions.entityFilter, |
224 | 195 | pageLink: this.entityDataSubscriptionOptions.pageLink, |
225 | 196 | keyFilters: this.entityDataSubscriptionOptions.keyFilters, |
... | ... | @@ -227,72 +198,17 @@ export class EntityDataSubscription { |
227 | 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 | 213 | this.subscriber.entityData$.subscribe( |
298 | 214 | (entityDataUpdate) => { |
... | ... | @@ -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 | 247 | this.subscriber.subscribe(); |
308 | 248 | } else if (this.datasourceType === DatasourceType.function) { |
309 | 249 | const entityData: EntityData = { |
... | ... | @@ -325,29 +265,46 @@ export class EntityDataSubscription { |
325 | 265 | totalPages: 1 |
326 | 266 | }; |
327 | 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 | 297 | if (this.dataAggregators) { |
339 | 298 | this.dataAggregators.forEach((aggregator) => { |
340 | 299 | aggregator.destroy(); |
341 | 300 | }) |
342 | - this.dataAggregators = null; | |
343 | 301 | } |
344 | - this.datasourceData = []; | |
345 | 302 | this.dataAggregators = []; |
346 | - this.entityIdToDataIndex = {}; | |
347 | - let tsKeyNames; | |
303 | + this.resetData(); | |
304 | + | |
348 | 305 | if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
306 | + let tsKeyNames = []; | |
349 | 307 | if (this.datasourceType === DatasourceType.function) { |
350 | - tsKeyNames = []; | |
351 | 308 | for (const key of Object.keys(this.dataKeys)) { |
352 | 309 | const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>; |
353 | 310 | dataKeysList.forEach((subscriptionDataKey) => { |
... | ... | @@ -357,20 +314,85 @@ export class EntityDataSubscription { |
357 | 314 | } else { |
358 | 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 | 318 | if (this.datasourceType === DatasourceType.function) { |
367 | 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 | 321 | } else if (!this.history && tsKeyNames.length) { |
370 | 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 | 396 | for (const key of Object.keys(this.dataKeys)) { |
375 | 397 | const dataKey = this.dataKeys[key]; |
376 | 398 | if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
... | ... | @@ -388,7 +410,23 @@ export class EntityDataSubscription { |
388 | 410 | } |
389 | 411 | } |
390 | 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 | 430 | const data: Array<Array<DataSetHolder>> = []; |
393 | 431 | for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { |
394 | 432 | const entityData = pageData.data[dataIndex]; |
... | ... | @@ -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 | 458 | private onDataUpdate(update: Array<EntityData>) { |
414 | 459 | for (const entityData of update) { |
415 | 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 | 468 | dataIndex, dataKeyIndex, detectChanges); |
425 | - }); | |
426 | 469 | } |
427 | 470 | |
428 | 471 | private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean, |
... | ... | @@ -596,14 +639,10 @@ export class EntityDataSubscription { |
596 | 639 | const value = dataKey.func(time, prevSeries[1]); |
597 | 640 | const series: [number, any] = [time, value]; |
598 | 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 | 648 | private onTick(detectChanges: boolean) { | ... | ... |
... | ... | @@ -24,17 +24,24 @@ import { UtilsService } from '@core/services/utils.service'; |
24 | 24 | import { SubscriptionDataKey } from '@core/api/datasource-subcription'; |
25 | 25 | import { deepClone, objectHashCode } from '@core/utils'; |
26 | 26 | import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription'; |
27 | +import { Observable, of } from 'rxjs'; | |
27 | 28 | |
28 | 29 | export interface EntityDataListener { |
29 | 30 | subscriptionType: widgetType; |
30 | - subscriptionTimewindow: SubscriptionTimewindow; | |
31 | + subscriptionTimewindow?: SubscriptionTimewindow; | |
31 | 32 | configDatasource: Datasource; |
32 | 33 | configDatasourceIndex: number; |
33 | 34 | dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void; |
34 | 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 | 47 | @Injectable({ |
... | ... | @@ -42,16 +49,48 @@ export interface EntityDataListener { |
42 | 49 | }) |
43 | 50 | export class EntityDataService { |
44 | 51 | |
45 | - private subscriptions: {[entityDataSubscriptionKey: string]: EntityDataSubscription} = {}; | |
46 | - | |
47 | 52 | constructor(private telemetryService: TelemetryWebsocketService, |
48 | 53 | private utils: UtilsService) {} |
49 | 54 | |
50 | - public subscribeToEntityData(listener: EntityDataListener) { | |
55 | + public prepareSubscription(listener: EntityDataListener): Observable<EntityDataLoadResult> { | |
51 | 56 | const datasource = listener.configDatasource; |
52 | 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 | 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 | 94 | const subscriptionDataKeys: Array<SubscriptionDataKey> = []; |
56 | 95 | datasource.dataKeys.forEach((dataKey) => { |
57 | 96 | const subscriptionDataKey: SubscriptionDataKey = { |
... | ... | @@ -62,47 +101,19 @@ export class EntityDataService { |
62 | 101 | }; |
63 | 102 | subscriptionDataKeys.push(subscriptionDataKey); |
64 | 103 | }); |
65 | - | |
66 | 104 | const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = { |
67 | 105 | datasourceType: datasource.type, |
68 | 106 | dataKeys: subscriptionDataKeys, |
69 | 107 | type: listener.subscriptionType |
70 | 108 | }; |
71 | - | |
72 | - if (listener.subscriptionType === widgetType.timeseries) { | |
73 | - entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); | |
74 | - } | |
75 | 109 | if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { |
76 | 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 | 98 | getAliasInfo(aliasId: string): Observable<AliasInfo>; |
99 | 99 | getEntityAliasId(aliasName: string): string; |
100 | 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 | 103 | resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>; |
103 | 104 | getEntityAliases(): EntityAliases; |
104 | 105 | updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); |
... | ... | @@ -202,8 +203,8 @@ export interface WidgetSubscriptionOptions { |
202 | 203 | alarmsMaxCountLoad?: number; |
203 | 204 | alarmsFetchSize?: number; |
204 | 205 | datasources?: Array<Datasource>; |
205 | - keyFilters?: Array<KeyFilter>; | |
206 | - pageLink?: EntityDataPageLink; | |
206 | + hasDataPageLink?: boolean; | |
207 | + singleEntity?: boolean; | |
207 | 208 | targetDeviceAliasIds?: Array<string>; |
208 | 209 | targetDeviceIds?: Array<string>; |
209 | 210 | useDashboardTimewindow?: boolean; |
... | ... | @@ -264,7 +265,7 @@ export interface IWidgetSubscription { |
264 | 265 | |
265 | 266 | onAliasesChanged(aliasIds: Array<string>): boolean; |
266 | 267 | |
267 | - onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): boolean; | |
268 | + onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void; | |
268 | 269 | |
269 | 270 | updateDataVisibility(index: number): void; |
270 | 271 | |
... | ... | @@ -278,6 +279,10 @@ export interface IWidgetSubscription { |
278 | 279 | |
279 | 280 | subscribe(): void; |
280 | 281 | |
282 | + subscribeForLatestData(datasourceIndex: number, | |
283 | + pageLink: EntityDataPageLink, | |
284 | + keyFilters: KeyFilter[]): void; | |
285 | + | |
281 | 286 | isDataResolved(): boolean; |
282 | 287 | |
283 | 288 | destroy(): void; | ... | ... |
... | ... | @@ -22,7 +22,6 @@ import { |
22 | 22 | WidgetSubscriptionOptions |
23 | 23 | } from '@core/api/widget-api.models'; |
24 | 24 | import { |
25 | - DataKey, | |
26 | 25 | DataSet, |
27 | 26 | DataSetHolder, |
28 | 27 | Datasource, |
... | ... | @@ -43,20 +42,18 @@ import { |
43 | 42 | toHistoryTimewindow, |
44 | 43 | WidgetTimewindow |
45 | 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 | 46 | import { CancelAnimationFrame } from '@core/services/raf.service'; |
48 | 47 | import { EntityType } from '@shared/models/entity-type.models'; |
49 | 48 | import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models'; |
50 | 49 | import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils'; |
51 | 50 | import { AlarmSourceListener } from '@core/http/alarm.service'; |
52 | -import { DatasourceListener } from '@core/api/datasource.service'; | |
53 | 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 | 52 | import * as moment_ from 'moment'; |
57 | 53 | import { PageData } from '@shared/models/page/page-data'; |
58 | 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 | 58 | const moment = moment_; |
62 | 59 | |
... | ... | @@ -73,12 +70,14 @@ export class WidgetSubscription implements IWidgetSubscription { |
73 | 70 | subscriptionTimewindow: SubscriptionTimewindow; |
74 | 71 | useDashboardTimewindow: boolean; |
75 | 72 | |
73 | + hasDataPageLink: boolean; | |
74 | + singleEntity: boolean; | |
75 | + | |
76 | 76 | datasourcePages: PageData<Datasource>[]; |
77 | 77 | dataPages: PageData<Array<DatasourceData>>[]; |
78 | 78 | entityDataListeners: Array<EntityDataListener>; |
79 | 79 | configuredDatasources: Array<Datasource>; |
80 | 80 | |
81 | - initDataSubscriptionSubject: Subject<void>; | |
82 | 81 | data: Array<DatasourceData>; |
83 | 82 | datasources: Array<Datasource>; |
84 | 83 | // datasourceListeners: Array<DatasourceListener>; |
... | ... | @@ -211,6 +210,8 @@ export class WidgetSubscription implements IWidgetSubscription { |
211 | 210 | // this.datasources = this.ctx.utils.validateDatasources(options.datasources); |
212 | 211 | this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources); |
213 | 212 | this.entityDataListeners = []; |
213 | + this.hasDataPageLink = options.hasDataPageLink; | |
214 | + this.singleEntity = options.singleEntity; | |
214 | 215 | // this.datasourceListeners = []; |
215 | 216 | this.datasourcePages = []; |
216 | 217 | this.datasources = []; |
... | ... | @@ -271,11 +272,11 @@ export class WidgetSubscription implements IWidgetSubscription { |
271 | 272 | const initRpcSubject = new ReplaySubject(); |
272 | 273 | if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { |
273 | 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 | 280 | if (this.targetDeviceId) { |
280 | 281 | this.rpcEnabled = true; |
281 | 282 | } else { |
... | ... | @@ -348,34 +349,72 @@ export class WidgetSubscription implements IWidgetSubscription { |
348 | 349 | } |
349 | 350 | |
350 | 351 | private initDataSubscription(): Observable<any> { |
351 | - this.initDataSubscriptionSubject = new ReplaySubject(1); | |
352 | + const initDataSubscriptionSubject = new ReplaySubject(1); | |
352 | 353 | this.loadStDiff().subscribe(() => { |
353 | 354 | if (!this.ctx.aliasController) { |
354 | 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 | 362 | } else { |
360 | - this.ctx.aliasController.resolveDatasources(this.configuredDatasources).subscribe( | |
363 | + this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe( | |
361 | 364 | (datasources) => { |
362 | 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 | 373 | (err) => { |
372 | 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 | 420 | /* private initDataSubscriptionOld(): Observable<any> { |
... | ... | @@ -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 | 635 | if (this.type === widgetType.timeseries || this.type === widgetType.alarm) { |
597 | 636 | if (this.useDashboardTimewindow) { |
598 | 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 | 640 | return true; |
603 | 641 | } |
604 | 642 | } |
... | ... | @@ -785,8 +823,12 @@ export class WidgetSubscription implements IWidgetSubscription { |
785 | 823 | } |
786 | 824 | |
787 | 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 | 834 | subscribe(): void { |
... | ... | @@ -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 | 870 | private doSubscribe() { |
806 | 871 | if (this.type === widgetType.rpc) { |
807 | 872 | return; |
... | ... | @@ -809,6 +874,12 @@ export class WidgetSubscription implements IWidgetSubscription { |
809 | 874 | if (this.type === widgetType.alarm) { |
810 | 875 | this.alarmsSubscribe(); |
811 | 876 | } else { |
877 | + this.dataSubscribe(); | |
878 | + } | |
879 | + } | |
880 | + | |
881 | + private dataSubscribe() { | |
882 | + if (!this.hasDataPageLink) { | |
812 | 883 | this.notifyDataLoading(); |
813 | 884 | if (this.type === widgetType.timeseries && this.timeWindowConfig) { |
814 | 885 | this.updateRealtimeSubscription(); |
... | ... | @@ -819,62 +890,10 @@ export class WidgetSubscription implements IWidgetSubscription { |
819 | 890 | this.onDataUpdated(); |
820 | 891 | } |
821 | 892 | } |
822 | - // let index = 0; | |
823 | 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 | 898 | if (forceUpdate) { |
880 | 899 | this.notifyDataLoaded(); |
... | ... | @@ -1000,7 +1019,9 @@ export class WidgetSubscription implements IWidgetSubscription { |
1000 | 1019 | this.alarmsUnsubscribe(); |
1001 | 1020 | } else { |
1002 | 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 | 1026 | this.entityDataListeners.length = 0; |
1006 | 1027 | this.resetData(); |
... | ... | @@ -1129,7 +1150,9 @@ export class WidgetSubscription implements IWidgetSubscription { |
1129 | 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 | 1156 | const datasource = this.configuredDatasources[datasourceIndex]; |
1134 | 1157 | datasource.dataReceived = true; |
1135 | 1158 | const datasources = pageData.data.map((entityData, index) => |
... | ... | @@ -1152,14 +1175,8 @@ export class WidgetSubscription implements IWidgetSubscription { |
1152 | 1175 | totalPages: pageData.totalPages |
1153 | 1176 | }; |
1154 | 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 | 1179 | this.configureLoadedData(); |
1162 | - this.notifyDataLoaded(); | |
1163 | 1180 | this.onDataUpdated(true); |
1164 | 1181 | } |
1165 | 1182 | } |
... | ... | @@ -1238,6 +1255,9 @@ export class WidgetSubscription implements IWidgetSubscription { |
1238 | 1255 | dataKey, |
1239 | 1256 | data: [] |
1240 | 1257 | }; |
1258 | + if (data && data[keyIndex] && data[keyIndex].data) { | |
1259 | + datasourceData.data = data[keyIndex].data; | |
1260 | + } | |
1241 | 1261 | return datasourceData; |
1242 | 1262 | }); |
1243 | 1263 | } |
... | ... | @@ -1275,6 +1295,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
1275 | 1295 | const startIndex = configuredDatasource.dataKeyStartIndex; |
1276 | 1296 | const dataKeysCount = configuredDatasource.dataKeys.length; |
1277 | 1297 | const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex; |
1298 | + this.notifyDataLoaded(); | |
1278 | 1299 | let update = true; |
1279 | 1300 | let currentData: DataSetHolder; |
1280 | 1301 | if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) { | ... | ... |
... | ... | @@ -54,9 +54,15 @@ import { |
54 | 54 | import { EntityRelationService } from '@core/http/entity-relation.service'; |
55 | 55 | import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; |
56 | 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 | 58 | import { EntityViewSearchQuery } from '@shared/models/entity-view.models'; |
59 | 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 | 67 | @Injectable({ |
62 | 68 | providedIn: 'root' |
... | ... | @@ -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 | 417 | public getAliasFilterTypesByEntityTypes(entityTypes: Array<EntityType | AliasEntityType>): Array<AliasFilterType> { |
364 | 418 | const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]); |
365 | 419 | if (!entityTypes || !entityTypes.length) { |
... | ... | @@ -605,7 +659,7 @@ export class EntityService { |
605 | 659 | public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> { |
606 | 660 | const filter = entityAlias.filter; |
607 | 661 | return this.resolveAliasFilter(filter, stateParams).pipe( |
608 | - map((result) => { | |
662 | + mergeMap((result) => { | |
609 | 663 | const aliasInfo: AliasInfo = { |
610 | 664 | alias: entityAlias.alias, |
611 | 665 | entityFilter: result.entityFilter, |
... | ... | @@ -615,30 +669,19 @@ export class EntityService { |
615 | 669 | }; |
616 | 670 | aliasInfo.resolvedEntities = result.entities; |
617 | 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 | 686 | public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable<EntityAliasFilterResult> { |
644 | 687 | const result: EntityAliasFilterResult = { | ... | ... |
... | ... | @@ -114,6 +114,17 @@ export class TelemetryWebsocketService implements TelemetryService { |
114 | 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 | 128 | public unsubscribe(subscriber: TelemetrySubscriber) { |
118 | 129 | if (this.isActive) { |
119 | 130 | subscriber.subscriptionCommands.forEach( | ... | ... |
... | ... | @@ -39,7 +39,7 @@ |
39 | 39 | </mat-toolbar> |
40 | 40 | <div fxFlex class="table-container"> |
41 | 41 | <table mat-table [dataSource]="entityDatasource" |
42 | - matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> | |
42 | + matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> | |
43 | 43 | <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;"> |
44 | 44 | <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell> |
45 | 45 | <mat-cell *matCellDef="let entity;" | ... | ... |
... | ... | @@ -32,26 +32,23 @@ import { |
32 | 32 | DataKey, |
33 | 33 | Datasource, |
34 | 34 | DatasourceData, |
35 | - DatasourceType, | |
36 | 35 | WidgetActionDescriptor, |
37 | 36 | WidgetConfig |
38 | 37 | } from '@shared/models/widget.models'; |
39 | 38 | import { IWidgetSubscription } from '@core/api/widget-api.models'; |
40 | 39 | import { UtilsService } from '@core/services/utils.service'; |
41 | 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 | 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 | 43 | import { CollectionViewer, DataSource } from '@angular/cdk/collections'; |
47 | 44 | import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
48 | 45 | import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; |
49 | 46 | import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
50 | 47 | import { EntityId } from '@shared/models/id/entity-id'; |
51 | 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 | 50 | import { MatPaginator } from '@angular/material/paginator'; |
54 | -import { MatSort } from '@angular/material/sort'; | |
51 | +import { MatSort, SortDirection } from '@angular/material/sort'; | |
55 | 52 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; |
56 | 53 | import { |
57 | 54 | CellContentInfo, |
... | ... | @@ -59,15 +56,13 @@ import { |
59 | 56 | constructTableCssString, |
60 | 57 | DisplayColumn, |
61 | 58 | EntityColumn, |
62 | - EntityData, | |
63 | - fromEntityColumnDef, | |
59 | + EntityData, entityDataSortOrderFromString, findColumnByEntityKey, findEntityKeyByColumnDef, | |
64 | 60 | getCellContentInfo, |
65 | 61 | getCellStyleInfo, |
66 | 62 | getColumnWidth, |
67 | 63 | getEntityValue, |
68 | 64 | TableWidgetDataKeySettings, |
69 | 65 | TableWidgetSettings, |
70 | - toEntityColumnDef, | |
71 | 66 | widthStyle |
72 | 67 | } from '@home/components/widget/lib/table-widget.models'; |
73 | 68 | import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
... | ... | @@ -77,6 +72,13 @@ import { |
77 | 72 | DisplayColumnsPanelComponent, |
78 | 73 | DisplayColumnsPanelData |
79 | 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 | 83 | interface EntitiesTableWidgetSettings extends TableWidgetSettings { |
82 | 84 | entitiesTitle: string; |
... | ... | @@ -103,7 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
103 | 105 | |
104 | 106 | public displayPagination = true; |
105 | 107 | public pageSizeOptions; |
106 | - public pageLink: PageLink; | |
108 | + public pageLink: EntityDataPageLink; | |
107 | 109 | public sortOrderProperty: string; |
108 | 110 | public textSearchMode = false; |
109 | 111 | public columns: Array<EntityColumn> = []; |
... | ... | @@ -150,8 +152,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
150 | 152 | private domSanitizer: DomSanitizer) { |
151 | 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 | 164 | ngOnInit(): void { |
... | ... | @@ -191,11 +198,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
191 | 198 | |
192 | 199 | public onDataUpdated() { |
193 | 200 | this.ngZone.run(() => { |
194 | - this.entityDatasource.updateEntitiesData(this.subscription.data); | |
201 | + this.entityDatasource.dataUpdated(); // .updateEntitiesData(this.subscription.data); | |
195 | 202 | this.ctx.detectChanges(); |
196 | 203 | }); |
197 | 204 | } |
198 | 205 | |
206 | + public pageLinkSortDirection(): SortDirection { | |
207 | + return entityDataPageLinkSortDirection(this.pageLink); | |
208 | + } | |
209 | + | |
199 | 210 | private initializeConfig() { |
200 | 211 | this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; |
201 | 212 | |
... | ... | @@ -256,7 +267,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
256 | 267 | name: 'entityName', |
257 | 268 | label: 'entityName', |
258 | 269 | def: 'entityName', |
259 | - title: entityNameColumnTitle | |
270 | + title: entityNameColumnTitle, | |
271 | + entityKey: { | |
272 | + key: 'name', | |
273 | + type: EntityKeyType.ENTITY_FIELD | |
274 | + } | |
260 | 275 | } as EntityColumn |
261 | 276 | ); |
262 | 277 | this.contentsInfo.entityName = { |
... | ... | @@ -273,7 +288,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
273 | 288 | name: 'entityLabel', |
274 | 289 | label: 'entityLabel', |
275 | 290 | def: 'entityLabel', |
276 | - title: entityLabelColumnTitle | |
291 | + title: entityLabelColumnTitle, | |
292 | + entityKey: { | |
293 | + key: 'label', | |
294 | + type: EntityKeyType.ENTITY_FIELD | |
295 | + } | |
277 | 296 | } as EntityColumn |
278 | 297 | ); |
279 | 298 | this.contentsInfo.entityLabel = { |
... | ... | @@ -291,6 +310,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
291 | 310 | label: 'entityType', |
292 | 311 | def: 'entityType', |
293 | 312 | title: this.translate.instant('entity.entity-type'), |
313 | + entityKey: { | |
314 | + key: 'entityType', | |
315 | + type: EntityKeyType.ENTITY_FIELD | |
316 | + } | |
294 | 317 | } as EntityColumn |
295 | 318 | ); |
296 | 319 | this.contentsInfo.entityType = { |
... | ... | @@ -309,8 +332,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
309 | 332 | if (datasource) { |
310 | 333 | datasource.dataKeys.forEach((entityDataKey) => { |
311 | 334 | const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn; |
335 | + dataKey.entityKey = { | |
336 | + key: dataKey.name, | |
337 | + type: null | |
338 | + }; | |
312 | 339 | if (dataKey.type === DataKeyType.function) { |
313 | 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 | 349 | dataKeys.push(dataKey); |
316 | 350 | |
... | ... | @@ -331,14 +365,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
331 | 365 | if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { |
332 | 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 | 376 | if (this.actionCellDescriptors.length) { |
338 | 377 | this.displayedColumns.push('actions'); |
339 | 378 | } |
340 | 379 | this.entityDatasource = new EntityDatasource( |
341 | - this.translate, dataKeys, this.subscription.datasources); | |
380 | + this.translate, dataKeys, this.subscription); | |
342 | 381 | } |
343 | 382 | |
344 | 383 | private editColumnsToDisplay($event: Event) { |
... | ... | @@ -416,9 +455,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
416 | 455 | } else { |
417 | 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 | 464 | this.ctx.detectChanges(); |
423 | 465 | } |
424 | 466 | |
... | ... | @@ -523,18 +565,19 @@ class EntityDatasource implements DataSource<EntityData> { |
523 | 565 | private entitiesSubject = new BehaviorSubject<EntityData[]>([]); |
524 | 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 | 572 | private currentEntity: EntityData = null; |
531 | 573 | |
532 | 574 | constructor( |
533 | 575 | private translate: TranslateService, |
534 | 576 | private dataKeys: Array<DataKey>, |
535 | - datasources: Array<Datasource> | |
577 | + private subscription: IWidgetSubscription | |
578 | + // datasources: Array<Datasource> | |
536 | 579 | ) { |
537 | - | |
580 | +/* | |
538 | 581 | for (const datasource of datasources) { |
539 | 582 | if (datasource.type === DatasourceType.entity && !datasource.entityId) { |
540 | 583 | continue; |
... | ... | @@ -558,7 +601,7 @@ class EntityDatasource implements DataSource<EntityData> { |
558 | 601 | }); |
559 | 602 | this.allEntities.push(entity); |
560 | 603 | } |
561 | - this.allEntitiesSubject.next(this.allEntities); | |
604 | + this.allEntitiesSubject.next(this.allEntities);*/ | |
562 | 605 | } |
563 | 606 | |
564 | 607 | connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> { |
... | ... | @@ -570,18 +613,63 @@ class EntityDatasource implements DataSource<EntityData> { |
570 | 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 | 619 | catchError(() => of(emptyPageData<EntityData>())), |
576 | 620 | ).subscribe( |
577 | 621 | (pageData) => { |
578 | 622 | this.entitiesSubject.next(pageData.data); |
579 | 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 | 673 | for (let i = 0; i < this.allEntities.length; i++) { |
586 | 674 | const entity = this.allEntities[i]; |
587 | 675 | for (let a = 0; a < this.dataKeys.length; a++) { |
... | ... | @@ -597,7 +685,7 @@ class EntityDatasource implements DataSource<EntityData> { |
597 | 685 | } |
598 | 686 | } |
599 | 687 | this.allEntitiesSubject.next(this.allEntities); |
600 | - } | |
688 | + }*/ | |
601 | 689 | |
602 | 690 | isEmpty(): Observable<boolean> { |
603 | 691 | return this.entitiesSubject.pipe( |
... | ... | @@ -625,9 +713,9 @@ class EntityDatasource implements DataSource<EntityData> { |
625 | 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 | 717 | return this.allEntities$.pipe( |
630 | 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 | 19 | import { getDescendantProp, isDefined } from '@core/utils'; |
20 | 20 | import { alarmFields, AlarmInfo } from '@shared/models/alarm.models'; |
21 | 21 | import * as tinycolor_ from 'tinycolor2'; |
22 | +import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models'; | |
22 | 23 | |
23 | 24 | const tinycolor = tinycolor_; |
24 | 25 | |
... | ... | @@ -49,6 +50,7 @@ export interface EntityData { |
49 | 50 | export interface EntityColumn extends DataKey { |
50 | 51 | def: string; |
51 | 52 | title: string; |
53 | + entityKey?: EntityKey; | |
52 | 54 | } |
53 | 55 | |
54 | 56 | export interface DisplayColumn { |
... | ... | @@ -73,6 +75,58 @@ export interface CellStyleInfo { |
73 | 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 | 130 | export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string { |
77 | 131 | let res = searchValue; |
78 | 132 | const column = columns.find(theColumn => theColumn[searchProperty] === searchValue); |
... | ... | @@ -82,6 +136,10 @@ export function findColumnProperty(searchProperty: string, searchValue: string, |
82 | 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 | 143 | export function toEntityColumnDef(label: string, columns: EntityColumn[]): string { |
86 | 144 | return findColumnProperty('label', label, 'def', columns); |
87 | 145 | } | ... | ... |
... | ... | @@ -346,12 +346,19 @@ export class WidgetComponentService { |
346 | 346 | } else { |
347 | 347 | result.typeParameters.useCustomDatasources = false; |
348 | 348 | } |
349 | + if (isUndefined(result.typeParameters.hasDataPageLink)) { | |
350 | + result.typeParameters.hasDataPageLink = false; | |
351 | + } | |
349 | 352 | if (isUndefined(result.typeParameters.maxDatasources)) { |
350 | 353 | result.typeParameters.maxDatasources = -1; |
351 | 354 | } |
352 | 355 | if (isUndefined(result.typeParameters.maxDataKeys)) { |
353 | 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 | 362 | if (isUndefined(result.typeParameters.dataKeysOptional)) { |
356 | 363 | result.typeParameters.dataKeysOptional = false; |
357 | 364 | } | ... | ... |
... | ... | @@ -620,14 +620,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
620 | 620 | |
621 | 621 | this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe( |
622 | 622 | (dashboardTimewindow) => { |
623 | - // TODO: | |
624 | - let subscriptionChanged = false; | |
625 | 623 | for (const id of Object.keys(this.widgetContext.subscriptions)) { |
626 | 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 | 840 | options = { |
846 | 841 | type: this.widget.type, |
847 | 842 | stateData: this.typeParameters.stateData, |
843 | + hasDataPageLink: this.typeParameters.hasDataPageLink, | |
844 | + singleEntity: this.typeParameters.singleEntity, | |
848 | 845 | comparisonEnabled: comparisonSettings.comparisonEnabled, |
849 | 846 | timeForComparison: comparisonSettings.timeForComparison |
850 | 847 | }; | ... | ... |
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | |
17 | 17 | import { AliasFilterType, EntityFilters } from '@shared/models/alias.models'; |
18 | 18 | import { EntityId } from '@shared/models/id/entity-id'; |
19 | +import { SortDirection } from '@angular/material/sort'; | |
19 | 20 | |
20 | 21 | export enum EntityKeyType { |
21 | 22 | ATTRIBUTE = 'ATTRIBUTE', |
... | ... | @@ -122,18 +123,30 @@ export interface EntityDataPageLink { |
122 | 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 | 150 | export interface EntityCountQuery { |
138 | 151 | entityFilter: EntityFilter; |
139 | 152 | } | ... | ... |
... | ... | @@ -21,7 +21,7 @@ import { Observable, ReplaySubject, Subject } from 'rxjs'; |
21 | 21 | import { EntityId } from '@shared/models/id/entity-id'; |
22 | 22 | import { map } from 'rxjs/operators'; |
23 | 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 | 25 | import { PageData } from '@shared/models/page/page-data'; |
26 | 26 | |
27 | 27 | export enum DataKeyType { |
... | ... | @@ -139,7 +139,7 @@ export interface EntityHistoryCmd { |
139 | 139 | } |
140 | 140 | |
141 | 141 | export interface LatestValueCmd { |
142 | - keys: Array<string>; | |
142 | + keys: Array<EntityKey>; | |
143 | 143 | } |
144 | 144 | |
145 | 145 | export interface TimeSeriesCmd { |
... | ... | @@ -153,7 +153,7 @@ export interface TimeSeriesCmd { |
153 | 153 | |
154 | 154 | export class EntityDataCmd implements WebsocketCmd { |
155 | 155 | cmdId: number; |
156 | - query: EntityDataQuery; | |
156 | + query?: EntityDataQuery; | |
157 | 157 | historyCmd?: EntityHistoryCmd; |
158 | 158 | latestCmd?: LatestValueCmd; |
159 | 159 | tsCmd?: TimeSeriesCmd; |
... | ... | @@ -314,6 +314,7 @@ export class EntityDataUpdate implements EntityDataUpdateMsg { |
314 | 314 | |
315 | 315 | export interface TelemetryService { |
316 | 316 | subscribe(subscriber: TelemetrySubscriber); |
317 | + update(subscriber: TelemetrySubscriber); | |
317 | 318 | unsubscribe(subscriber: TelemetrySubscriber); |
318 | 319 | } |
319 | 320 | |
... | ... | @@ -360,6 +361,10 @@ export class TelemetrySubscriber { |
360 | 361 | this.telemetryService.subscribe(this); |
361 | 362 | } |
362 | 363 | |
364 | + public update() { | |
365 | + this.telemetryService.update(this); | |
366 | + } | |
367 | + | |
363 | 368 | public unsubscribe() { |
364 | 369 | this.telemetryService.unsubscribe(this); |
365 | 370 | this.complete(); | ... | ... |
... | ... | @@ -150,6 +150,8 @@ export interface WidgetTypeParameters { |
150 | 150 | maxDataKeys?: number; |
151 | 151 | dataKeysOptional?: boolean; |
152 | 152 | stateData?: boolean; |
153 | + hasDataPageLink?: boolean; | |
154 | + singleEntity?: boolean; | |
153 | 155 | } |
154 | 156 | |
155 | 157 | export interface WidgetControllerDescriptor { | ... | ... |