Commit 81ccdf7bfafd486ebd00cf619cdb6b5c4818cead

Authored by Igor Kulikov
1 parent 983ba89a

TB-70: Improve dashboards states management.

... ... @@ -26,12 +26,12 @@
26 26 "name": "Entities table",
27 27 "descriptor": {
28 28 "type": "latest",
29   - "sizeX": 10.5,
30   - "sizeY": 6.5,
  29 + "sizeX": 7.5,
  30 + "sizeY": 4.5,
31 31 "resources": [],
32 32 "templateHtml": "<tb-entities-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-entities-table-widget>",
33 33 "templateCss": "",
34   - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1 \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 };\n}\n\nself.onDestroy = function() {\n}\n",
  34 + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\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 };\n}\n\nself.onDestroy = function() {\n}\n",
35 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 \"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 \"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 \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
36 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, filter)\",\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 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\":\"18px\",\"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;\"}]}]}"
... ...
... ... @@ -15,7 +15,7 @@
15 15 "resources": [],
16 16 "templateHtml": "<div style=\"height: 100%;\" id=\"device-terminal\"></div>",
17 17 "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n",
18   - "controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC commands terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = angular.copy(command).trim();\n if (localCommand == 'help') {\n printUsage(this);\n } else {\n var cmdObj = $.terminal.parse_command(localCommand);\n if (cmdObj.args.length > 1) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (cmdObj.args.length && cmdObj.args[0]) {\n params = JSON.parse(cmdObj.args[0]);\n }\n performRpc(this, cmdObj.name, params);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\nfunction performRpc(terminal, method, params) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout).then(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n \nself.onDestroy = function() {\n}\n",
  18 + "controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC commands terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = angular.copy(command).trim();\n if (localCommand == 'help') {\n printUsage(this);\n } else {\n var cmdObj = $.terminal.parse_command(localCommand);\n if (cmdObj.args.length > 1) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (cmdObj.args.length && cmdObj.args[0]) {\n try {\n params = JSON.parse(cmdObj.args[0]);\n } catch (e) {\n params = cmdObj.args[0];\n }\n }\n performRpc(this, cmdObj.name, params);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\nfunction performRpc(terminal, method, params) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout).then(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n \nself.onDestroy = function() {\n}\n",
19 19 "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\"\n ]\n}",
20 20 "dataKeySettingsSchema": "{}\n",
21 21 "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"Device terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
... ...
... ... @@ -53,10 +53,11 @@ export default class AliasController {
53 53 }
54 54
55 55 dashboardStateChanged() {
56   - var newEntityId = this.stateController.getStateParams().entityId;
57 56 var changedAliasIds = [];
58 57 for (var aliasId in this.resolvedAliasesToStateEntities) {
59   - var prevEntityId = this.resolvedAliasesToStateEntities[aliasId];
  58 + var stateEntityInfo = this.resolvedAliasesToStateEntities[aliasId];
  59 + var newEntityId = this.stateController.getEntityId(stateEntityInfo.entityParamName);
  60 + var prevEntityId = stateEntityInfo.entityId;
60 61 if (!angular.equals(newEntityId, prevEntityId)) {
61 62 changedAliasIds.push(aliasId);
62 63 this.setAliasUnresolved(aliasId);
... ... @@ -93,19 +94,26 @@ export default class AliasController {
93 94 this.entityService.resolveAlias(entityAlias, this.stateController.getStateParams()).then(
94 95 function success(aliasInfo) {
95 96 aliasCtrl.resolvedAliases[aliasId] = aliasInfo;
  97 + delete aliasCtrl.resolvedAliasesPromise[aliasId];
96 98 if (aliasInfo.stateEntity) {
  99 + var stateEntityInfo = {
  100 + entityParamName: aliasInfo.entityParamName,
  101 + entityId: aliasCtrl.stateController.getEntityId(aliasInfo.entityParamName)
  102 + };
97 103 aliasCtrl.resolvedAliasesToStateEntities[aliasId] =
98   - aliasCtrl.stateController.getStateParams().entityId;
  104 + stateEntityInfo;
99 105 }
100 106 aliasCtrl.$scope.$broadcast('entityAliasResolved', aliasId);
101 107 deferred.resolve(aliasInfo);
102 108 },
103 109 function fail() {
104 110 deferred.reject();
  111 + delete aliasCtrl.resolvedAliasesPromise[aliasId];
105 112 }
106 113 );
107 114 } else {
108 115 deferred.reject();
  116 + delete aliasCtrl.resolvedAliasesPromise[aliasId];
109 117 }
110 118 return this.resolvedAliasesPromise[aliasId];
111 119 }
... ...
... ... @@ -376,6 +376,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
376 376 var aliasInfo = {
377 377 alias: entityAlias.alias,
378 378 stateEntity: result.stateEntity,
  379 + entityParamName: result.entityParamName,
379 380 resolveMultiple: filter.resolveMultiple
380 381 };
381 382 aliasInfo.resolvedEntities = result.entities;
... ... @@ -392,12 +393,30 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
392 393 return deferred.promise;
393 394 }
394 395
  396 + function getStateEntityId(filter, stateParams) {
  397 + var entityId = null;
  398 + if (stateParams) {
  399 + if (filter.stateEntityParamName && filter.stateEntityParamName.length) {
  400 + if (stateParams[filter.stateEntityParamName]) {
  401 + entityId = stateParams[filter.stateEntityParamName].entityId;
  402 + }
  403 + } else {
  404 + entityId = stateParams.entityId;
  405 + }
  406 + }
  407 + return entityId;
  408 + }
  409 +
395 410 function resolveAliasFilter(filter, stateParams, maxItems) {
396 411 var deferred = $q.defer();
397 412 var result = {
398 413 entities: [],
399 414 stateEntity: false
400 415 };
  416 + if (filter.stateEntityParamName && filter.stateEntityParamName.length) {
  417 + result.entityParamName = filter.stateEntityParamName;
  418 + }
  419 + var stateEntityId = getStateEntityId(filter, stateParams);
401 420 switch (filter.type) {
402 421 case types.aliasFilterType.entityList.value:
403 422 getEntities(filter.entityType, filter.entityList).then(
... ... @@ -431,8 +450,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
431 450 break;
432 451 case types.aliasFilterType.stateEntity.value:
433 452 result.stateEntity = true;
434   - if (stateParams && stateParams.entityId) {
435   - getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then(
  453 + if (stateEntityId) {
  454 + getEntity(stateEntityId.entityType, stateEntityId.id).then(
436 455 function success(entity) {
437 456 result.entities = entitiesToEntitiesInfo([entity]);
438 457 deferred.resolve(result);
... ... @@ -479,9 +498,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
479 498 result.stateEntity = filter.rootStateEntity;
480 499 var rootEntityType;
481 500 var rootEntityId;
482   - if (result.stateEntity && stateParams && stateParams.entityId) {
483   - rootEntityType = stateParams.entityId.entityType;
484   - rootEntityId = stateParams.entityId.id;
  501 + if (result.stateEntity && stateEntityId) {
  502 + rootEntityType = stateEntityId.entityType;
  503 + rootEntityId = stateEntityId.id;
485 504 } else if (!result.stateEntity) {
486 505 rootEntityType = filter.rootEntity.entityType;
487 506 rootEntityId = filter.rootEntity.id;
... ... @@ -520,9 +539,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
520 539 case types.aliasFilterType.assetSearchQuery.value:
521 540 case types.aliasFilterType.deviceSearchQuery.value:
522 541 result.stateEntity = filter.rootStateEntity;
523   - if (result.stateEntity && stateParams && stateParams.entityId) {
524   - rootEntityType = stateParams.entityId.entityType;
525   - rootEntityId = stateParams.entityId.id;
  542 + if (result.stateEntity && stateEntityId) {
  543 + rootEntityType = stateEntityId.entityType;
  544 + rootEntityId = stateEntityId.id;
526 545 } else if (!result.stateEntity) {
527 546 rootEntityType = filter.rootEntity.entityType;
528 547 rootEntityId = filter.rootEntity.id;
... ...
... ... @@ -730,12 +730,15 @@ export default class Subscription {
730 730 index += datasource.dataKeys.length;
731 731
732 732 this.datasourceListeners.push(listener);
733   - this.ctx.datasourceService.subscribeToDatasource(listener);
734   - if (datasource.unresolvedStateEntity) {
  733 +
  734 + if (datasource.dataKeys.length) {
  735 + this.ctx.datasourceService.subscribeToDatasource(listener);
  736 + }
  737 +
  738 + if (datasource.unresolvedStateEntity || !datasource.dataKeys.length) {
735 739 this.notifyDataLoaded();
736 740 this.onDataUpdated();
737 741 }
738   -
739 742 }
740 743 }
741 744 }
... ...
... ... @@ -556,8 +556,9 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
556 556 ' self.typeParameters = function() {\n\n' +
557 557 return {
558 558 useCustomDatasources: false,
559   - maxDatasources: -1 //unlimited
560   - maxDataKeys: -1 //unlimited
  559 + maxDatasources: -1, //unlimited
  560 + maxDataKeys: -1, //unlimited
  561 + dataKeysOptional: false
561 562 };
562 563 ' }\n\n' +
563 564
... ... @@ -625,6 +626,9 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
625 626 if (angular.isUndefined(result.typeParameters.maxDataKeys)) {
626 627 result.typeParameters.maxDataKeys = -1;
627 628 }
  629 + if (angular.isUndefined(result.typeParameters.dataKeysOptional)) {
  630 + result.typeParameters.dataKeysOptional = false;
  631 + }
628 632 if (angular.isFunction(widgetTypeInstance.actionSources)) {
629 633 result.actionSources = widgetTypeInstance.actionSources();
630 634 } else {
... ...
... ... @@ -68,10 +68,14 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
68 68 ngModelCtrl.$setValidity('entityAlias',
69 69 angular.isDefined(value.entityAliasId) &&
70 70 value.entityAliasId != null);
71   - ngModelCtrl.$setValidity('entityKeys',
72   - angular.isDefined(value.dataKeys) &&
73   - value.dataKeys != null &&
74   - value.dataKeys.length > 0);
  71 + if (scope.optDataKeys) {
  72 + ngModelCtrl.$setValidity('entityKeys', true);
  73 + } else {
  74 + ngModelCtrl.$setValidity('entityKeys',
  75 + angular.isDefined(value.dataKeys) &&
  76 + value.dataKeys != null &&
  77 + value.dataKeys.length > 0);
  78 + }
75 79 }
76 80 }
77 81 };
... ... @@ -281,6 +285,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
281 285 scope: {
282 286 widgetType: '=',
283 287 maxDataKeys: '=',
  288 + optDataKeys: '=',
284 289 aliasController: '=',
285 290 datakeySettingsSchema: '=',
286 291 generateDataKey: '&',
... ...
... ... @@ -63,10 +63,14 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
63 63 var dataValid = angular.isDefined(value) && value != null;
64 64 ngModelCtrl.$setValidity('deviceData', dataValid);
65 65 if (dataValid) {
66   - ngModelCtrl.$setValidity('datasourceKeys',
67   - angular.isDefined(value.dataKeys) &&
68   - value.dataKeys != null &&
69   - value.dataKeys.length > 0);
  66 + if (scope.optDataKeys) {
  67 + ngModelCtrl.$setValidity('datasourceKeys', true);
  68 + } else {
  69 + ngModelCtrl.$setValidity('datasourceKeys',
  70 + angular.isDefined(value.dataKeys) &&
  71 + value.dataKeys != null &&
  72 + value.dataKeys.length > 0);
  73 + }
70 74 }
71 75 }
72 76 };
... ... @@ -222,6 +226,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
222 226 scope: {
223 227 widgetType: '=',
224 228 maxDataKeys: '=',
  229 + optDataKeys: '=',
225 230 generateDataKey: '&',
226 231 datakeySettingsSchema: '='
227 232 },
... ...
... ... @@ -83,6 +83,7 @@ function Datasource($compile, $templateCache, utils, types) {
83 83 scope: {
84 84 aliasController: '=',
85 85 maxDataKeys: '=',
  86 + optDataKeys: '=',
86 87 widgetType: '=',
87 88 functionsOnly: '=',
88 89 datakeySettingsSchema: '=',
... ...
... ... @@ -28,6 +28,7 @@
28 28 ng-switch-default
29 29 ng-model="model"
30 30 max-data-keys="maxDataKeys"
  31 + opt-data-keys="optDataKeys"
31 32 datakey-settings-schema="datakeySettingsSchema"
32 33 ng-required="model.type === types.datasourceType.function"
33 34 widget-type="widgetType"
... ... @@ -36,6 +37,7 @@
36 37 <tb-datasource-entity flex
37 38 ng-model="model"
38 39 max-data-keys="maxDataKeys"
  40 + opt-data-keys="optDataKeys"
39 41 datakey-settings-schema="datakeySettingsSchema"
40 42 ng-switch-when="entity"
41 43 ng-required="model.type === types.datasourceType.entity"
... ...
... ... @@ -145,11 +145,13 @@ export default function WidgetActionDialogController($scope, $mdDialog, $filter,
145 145 result.targetDashboardStateId = action.targetDashboardStateId;
146 146 result.openRightLayout = action.openRightLayout;
147 147 result.setEntityId = action.setEntityId;
  148 + result.stateEntityParamName = action.stateEntityParamName;
148 149 break;
149 150 case vm.types.widgetActionTypes.openDashboard.value:
150 151 result.targetDashboardId = action.targetDashboardId;
151 152 result.targetDashboardStateId = action.targetDashboardStateId;
152 153 result.setEntityId = action.setEntityId;
  154 + result.stateEntityParamName = action.stateEntityParamName;
153 155 break;
154 156 case vm.types.widgetActionTypes.custom.value:
155 157 result.customFunction = action.customFunction;
... ...
... ... @@ -104,12 +104,20 @@
104 104 flex aria-label="{{ 'widget-action.open-right-layout' | translate }}"
105 105 ng-model="vm.action.openRightLayout">{{ 'widget-action.open-right-layout' | translate }}
106 106 </md-checkbox>
107   - <md-checkbox ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
108   - vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
109   - vm.action.type == vm.types.widgetActionTypes.openDashboard.value"
110   - flex aria-label="{{ 'widget-action.set-entity-from-widget' | translate }}"
111   - ng-model="vm.action.setEntityId">{{ 'widget-action.set-entity-from-widget' | translate }}
112   - </md-checkbox>
  107 + <div flex layout="column" ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
  108 + vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
  109 + vm.action.type == vm.types.widgetActionTypes.openDashboard.value">
  110 + <md-checkbox flex aria-label="{{ 'widget-action.set-entity-from-widget' | translate }}"
  111 + ng-model="vm.action.setEntityId">{{ 'widget-action.set-entity-from-widget' | translate }}
  112 + </md-checkbox>
  113 + <md-input-container ng-if="vm.action.setEntityId" class="md-block">
  114 + <label translate>alias.state-entity-parameter-name</label>
  115 + <input name="stateEntityParamName"
  116 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  117 + ng-model="vm.action.stateEntityParamName"
  118 + aria-label="{{ 'alias.state-entity-parameter-name' | translate }}">
  119 + </md-input-container>
  120 + </div>
113 121 <tb-js-func ng-if="vm.action.type == vm.types.widgetActionTypes.custom.value"
114 122 ng-model="vm.action.customFunction"
115 123 function-args="{{ ['$event', 'widgetContext', 'entityId'] }}"
... ...
... ... @@ -96,6 +96,7 @@
96 96 <tb-datasource flex ng-model="datasource.value"
97 97 widget-type="widgetType"
98 98 max-data-keys="typeParameters.maxDataKeys"
  99 + opt-data-keys="typeParameters.dataKeysOptional"
99 100 alias-controller="aliasController"
100 101 functions-only="functionsOnly"
101 102 datakey-settings-schema="datakeySettingsSchema"
... ...
... ... @@ -421,23 +421,41 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
421 421 return result;
422 422 }
423 423
  424 + function updateEntityParams(params, targetEntityParamName, targetEntityId, entityName) {
  425 + if (targetEntityId) {
  426 + var targetEntityParams;
  427 + if (targetEntityParamName && targetEntityParamName.length) {
  428 + targetEntityParams = params[targetEntityParamName];
  429 + if (!targetEntityParams) {
  430 + targetEntityParams = {};
  431 + params[targetEntityParamName] = targetEntityParams;
  432 + }
  433 + } else {
  434 + targetEntityParams = params;
  435 + }
  436 + targetEntityParams.entityId = targetEntityId;
  437 + if (entityName) {
  438 + targetEntityParams.entityName = entityName;
  439 + }
  440 + }
  441 + }
  442 +
424 443 function handleWidgetAction($event, descriptor, entityId, entityName) {
425 444 var type = descriptor.type;
  445 + var targetEntityParamName = descriptor.stateEntityParamName;
  446 + var targetEntityId;
  447 + if (descriptor.setEntityId) {
  448 + targetEntityId = entityId;
  449 + }
426 450 switch (type) {
427 451 case types.widgetActionTypes.openDashboardState.value:
428 452 case types.widgetActionTypes.updateDashboardState.value:
429 453 var targetDashboardStateId = descriptor.targetDashboardStateId;
430   - var targetEntityId;
431   - if (descriptor.setEntityId) {
432   - targetEntityId = entityId;
433   - }
434   - var params = {};
435   - if (targetEntityId) {
436   - params.entityId = targetEntityId;
437   - if (entityName) {
438   - params.entityName = entityName;
439   - }
  454 + var params = angular.copy(widgetContext.stateController.getStateParams());
  455 + if (!params) {
  456 + params = {};
440 457 }
  458 + updateEntityParams(params, targetEntityParamName, targetEntityId, entityName);
441 459 if (type == types.widgetActionTypes.openDashboardState.value) {
442 460 widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
443 461 } else {
... ... @@ -447,18 +465,9 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
447 465 case types.widgetActionTypes.openDashboard.value:
448 466 var targetDashboardId = descriptor.targetDashboardId;
449 467 targetDashboardStateId = descriptor.targetDashboardStateId;
450   - targetEntityId;
451   - if (descriptor.setEntityId) {
452   - targetEntityId = entityId;
453   - }
454 468 var stateObject = {};
455 469 stateObject.params = {};
456   - if (targetEntityId) {
457   - stateObject.params.entityId = targetEntityId;
458   - if (entityName) {
459   - stateObject.params.entityName = entityName;
460   - }
461   - }
  470 + updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName);
462 471 if (targetDashboardStateId) {
463 472 stateObject.id = targetDashboardStateId;
464 473 }
... ...
... ... @@ -25,6 +25,8 @@ export default function DashboardSettingsController($scope, $mdDialog, statesCon
25 25 vm.imageAdded = imageAdded;
26 26 vm.clearImage = clearImage;
27 27
  28 + vm.stateControllerIdChanged = stateControllerIdChanged;
  29 +
28 30 vm.settings = settings;
29 31 vm.gridSettings = gridSettings;
30 32 vm.stateControllers = statesControllerService.getStateControllers();
... ... @@ -98,6 +100,12 @@ export default function DashboardSettingsController($scope, $mdDialog, statesCon
98 100 vm.gridSettings.backgroundImageUrl = null;
99 101 }
100 102
  103 + function stateControllerIdChanged() {
  104 + if (vm.settings.stateControllerId != 'default') {
  105 + vm.settings.toolbarAlwaysOpen = true;
  106 + }
  107 + }
  108 +
101 109 function save() {
102 110 $scope.theForm.$setPristine();
103 111 if (vm.gridSettings) {
... ...
... ... @@ -34,7 +34,8 @@
34 34 <div ng-show="vm.settings">
35 35 <md-input-container class="md-block">
36 36 <label translate>dashboard.state-controller</label>
37   - <md-select aria-label="{{ 'dashboard.state-controller' | translate }}" ng-model="vm.settings.stateControllerId">
  37 + <md-select aria-label="{{ 'dashboard.state-controller' | translate }}"
  38 + ng-model="vm.settings.stateControllerId" ng-change="vm.stateControllerIdChanged()">
38 39 <md-option ng-repeat="(stateControllerId, stateController) in vm.stateControllers" ng-value="stateControllerId">
39 40 {{stateControllerId}}
40 41 </md-option>
... ...
... ... @@ -27,6 +27,7 @@ export default function DefaultStateController($scope, $location, $state, $state
27 27 vm.getStateId = getStateId;
28 28 vm.getStateParams = getStateParams;
29 29 vm.getStateParamsByStateId = getStateParamsByStateId;
  30 + vm.getEntityId = getEntityId;
30 31
31 32 vm.getStateName = getStateName;
32 33
... ... @@ -103,6 +104,10 @@ export default function DefaultStateController($scope, $location, $state, $state
103 104 }
104 105 }
105 106
  107 + function getEntityId() {
  108 + return null;
  109 + }
  110 +
106 111 function getStateObjById(id) {
107 112 for (var i=0; i < vm.stateObject.length; i++) {
108 113 if (vm.stateObject[i].id === id) {
... ...
... ... @@ -29,6 +29,7 @@ export default function EntityStateController($scope, $location, $state, $stateP
29 29 vm.getStateId = getStateId;
30 30 vm.getStateParams = getStateParams;
31 31 vm.getStateParamsByStateId = getStateParamsByStateId;
  32 + vm.getEntityId = getEntityId;
32 33
33 34 vm.getStateName = getStateName;
34 35
... ... @@ -111,6 +112,16 @@ export default function EntityStateController($scope, $location, $state, $stateP
111 112 }
112 113 }
113 114
  115 + function getEntityId(entityParamName) {
  116 + var stateParams = getStateParams();
  117 + if (!entityParamName || !entityParamName.length) {
  118 + return stateParams.entityId;
  119 + } else if (stateParams[entityParamName]) {
  120 + return stateParams[entityParamName].entityId;
  121 + }
  122 + return null;
  123 + }
  124 +
114 125 function getStateObjById(id) {
115 126 for (var i=0; i < vm.stateObject.length; i++) {
116 127 if (vm.stateObject[i].id === id) {
... ... @@ -135,15 +146,22 @@ export default function EntityStateController($scope, $location, $state, $stateP
135 146 function resolveEntity(params) {
136 147 var deferred = $q.defer();
137 148 if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
138   - entityService.getEntity(params.entityId.entityType, params.entityId.id, {ignoreLoading: true, ignoreErrors: true}).then(
139   - function success(entity) {
140   - var entityName = entity.name;
141   - deferred.resolve(entityName);
142   - },
143   - function fail() {
144   - deferred.reject();
145   - }
146   - );
  149 + if (params.entityName && params.entityName.length) {
  150 + deferred.resolve(params.entityName);
  151 + } else {
  152 + entityService.getEntity(params.entityId.entityType, params.entityId.id, {
  153 + ignoreLoading: true,
  154 + ignoreErrors: true
  155 + }).then(
  156 + function success(entity) {
  157 + var entityName = entity.name;
  158 + deferred.resolve(entityName);
  159 + },
  160 + function fail() {
  161 + deferred.reject();
  162 + }
  163 + );
  164 + }
147 165 } else {
148 166 deferred.reject();
149 167 }
... ...
... ... @@ -70,6 +70,15 @@ export default function StatesComponent($compile, $templateCache, $controller, s
70 70 return null;
71 71 }
72 72 }
  73 +
  74 + stateController.getEntityId = function(entityParamName) {
  75 + if (scope.statesController) {
  76 + return scope.statesController.getEntityId(entityParamName);
  77 + } else {
  78 + return null;
  79 + }
  80 + }
  81 +
73 82 }
74 83
75 84 scope.$on('$destroy', function callOnDestroyHook() {
... ...
... ... @@ -54,6 +54,7 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc
54 54 filter.entityNameFilter = '';
55 55 break;
56 56 case types.aliasFilterType.stateEntity.value:
  57 + filter.stateEntityParamName = null;
57 58 break;
58 59 case types.aliasFilterType.assetType.value:
59 60 filter.assetType = null;
... ... @@ -67,6 +68,7 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc
67 68 case types.aliasFilterType.assetSearchQuery.value:
68 69 case types.aliasFilterType.deviceSearchQuery.value:
69 70 filter.rootStateEntity = false;
  71 + filter.stateEntityParamName = null;
70 72 filter.rootEntity = null;
71 73 filter.direction = types.entitySearchDirection.from;
72 74 filter.maxLevel = 1;
... ...
... ... @@ -59,6 +59,13 @@
59 59 </md-input-container>
60 60 </section>
61 61 <section layout="column" ng-if="filter.type == types.aliasFilterType.stateEntity.value" id="stateEntityFilter">
  62 + <md-input-container class="md-block">
  63 + <label translate>alias.state-entity-parameter-name</label>
  64 + <input name="stateEntityParamName"
  65 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  66 + ng-model="filter.stateEntityParamName"
  67 + aria-label="{{ 'alias.state-entity-parameter-name' | translate }}">
  68 + </md-input-container>
62 69 </section>
63 70 <section layout="column" ng-if="filter.type == types.aliasFilterType.assetType.value" id="assetTypeFilter">
64 71 <tb-entity-subtype-autocomplete
... ... @@ -97,12 +104,21 @@
97 104 ng-disabled="filter.rootStateEntity"
98 105 ng-model="filter.rootEntity">
99 106 </tb-entity-select>
100   - <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
101   - <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
102   - <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
103   - aria-label="{{ 'alias.root-state-entity' | translate }}">
104   - </md-switch>
105   - </section>
  107 + <div layout="column">
  108 + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
  109 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  110 + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
  111 + aria-label="{{ 'alias.root-state-entity' | translate }}">
  112 + </md-switch>
  113 + </section>
  114 + <md-input-container ng-if="filter.rootStateEntity" class="md-block">
  115 + <label translate>alias.state-entity-parameter-name</label>
  116 + <input name="stateEntityParamName"
  117 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  118 + ng-model="filter.stateEntityParamName"
  119 + aria-label="{{ 'alias.state-entity-parameter-name' | translate }}">
  120 + </md-input-container>
  121 + </div>
106 122 </div>
107 123 <div flex layout="row">
108 124 <md-input-container class="md-block" style="min-width: 100px;">
... ... @@ -139,12 +155,21 @@
139 155 ng-disabled="filter.rootStateEntity"
140 156 ng-model="filter.rootEntity">
141 157 </tb-entity-select>
142   - <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
143   - <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
144   - <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
145   - aria-label="{{ 'alias.root-state-entity' | translate }}">
146   - </md-switch>
147   - </section>
  158 + <div layout="column">
  159 + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
  160 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  161 + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
  162 + aria-label="{{ 'alias.root-state-entity' | translate }}">
  163 + </md-switch>
  164 + </section>
  165 + <md-input-container ng-if="filter.rootStateEntity" class="md-block">
  166 + <label translate>alias.state-entity-parameter-name</label>
  167 + <input name="stateEntityParamName"
  168 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  169 + ng-model="filter.stateEntityParamName"
  170 + aria-label="{{ 'alias.state-entity-parameter-name' | translate }}">
  171 + </md-input-container>
  172 + </div>
148 173 </div>
149 174 <div flex layout="row">
150 175 <md-input-container class="md-block" style="min-width: 100px;">
... ... @@ -189,12 +214,21 @@
189 214 ng-disabled="filter.rootStateEntity"
190 215 ng-model="filter.rootEntity">
191 216 </tb-entity-select>
192   - <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
193   - <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
194   - <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
195   - aria-label="{{ 'alias.root-state-entity' | translate }}">
196   - </md-switch>
197   - </section>
  217 + <div layout="column">
  218 + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
  219 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  220 + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
  221 + aria-label="{{ 'alias.root-state-entity' | translate }}">
  222 + </md-switch>
  223 + </section>
  224 + <md-input-container ng-if="filter.rootStateEntity" class="md-block">
  225 + <label translate>alias.state-entity-parameter-name</label>
  226 + <input name="stateEntityParamName"
  227 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  228 + ng-model="filter.stateEntityParamName"
  229 + aria-label="{{ 'alias.state-entity-parameter-name' | translate }}">
  230 + </md-input-container>
  231 + </div>
198 232 </div>
199 233 <div flex layout="row">
200 234 <md-input-container class="md-block" style="min-width: 100px;">
... ...
... ... @@ -187,6 +187,8 @@ export default angular.module('thingsboard.locale', [])
187 187 "no-entity-filter-specified": "No entity filter specified",
188 188 "root-state-entity": "Use dashboard state entity as root",
189 189 "root-entity": "Root entity",
  190 + "state-entity-parameter-name": "State entity parameter name",
  191 + "default-entity-parameter-name": "By default",
190 192 "max-relation-level": "Max relation level",
191 193 "unlimited-level": "Unlimited level",
192 194 "state-entity": "Dashboard state entity",
... ...
... ... @@ -479,6 +479,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
479 479
480 480 for (var i=0;i<vm.datasources.length;i++) {
481 481 datasource = vm.datasources[i];
  482 + if (datasource.type == types.datasourceType.entity && !datasource.entityId) {
  483 + continue;
  484 + }
482 485 var entity = {
483 486 id: {}
484 487 };
... ...