Commit f37ebb66aa39283a16fd80fae3bdb015292bfd8f

Authored by Igor Kulikov
1 parent 2aaa51fe

UI: Entities data query

... ... @@ -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 {
... ...