Browse Source

Merge branch 'develop/3.0' of https://github.com/thingsboard/thingsboard into map/3.0

pull/2535/head
Artem Halushko 7 years ago
parent
commit
7412f5fbdf
  1. 14
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 2
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  3. 121
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  4. 10
      application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java
  5. 18
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  6. 33
      application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java
  7. 6
      application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
  10. 253
      application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java
  11. 159
      application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java
  12. 12
      application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java
  13. 99
      application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java
  14. 7
      application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java
  15. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java
  16. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java
  17. 2
      ui-ngx/src/app/core/http/device.service.ts
  18. 2
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  19. 18
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  20. 126
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  21. 75
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
  22. 483
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  23. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  24. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  25. 45
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  26. 57
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html
  27. 63
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts
  28. 96
      ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts
  29. 6
      ui-ngx/src/app/shared/components/json-content.component.html
  30. 8
      ui-ngx/src/app/shared/components/json-content.component.ts
  31. 10
      ui-ngx/src/app/shared/components/json-object-edit.component.html
  32. 19
      ui-ngx/src/app/shared/components/json-object-edit.component.scss
  33. 16
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  34. 6
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  35. 16
      ui-ngx/src/app/shared/components/value-input.component.html
  36. 34
      ui-ngx/src/app/shared/components/value-input.component.ts
  37. 10
      ui-ngx/src/app/shared/models/constants.ts
  38. 30
      ui-ngx/src/app/shared/pipe/tbJson.pipe.ts
  39. 13
      ui-ngx/src/app/shared/shared.module.ts
  40. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  41. 4
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  42. 4
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  43. 4
      ui/src/app/api/widget.service.js
  44. 5
      ui/src/app/common/types.constant.js
  45. 32
      ui/src/app/components/json-content.directive.js
  46. 2
      ui/src/app/components/json-content.scss
  47. 8
      ui/src/app/components/json-content.tpl.html
  48. 10
      ui/src/app/components/json-object-edit.directive.js
  49. 13
      ui/src/app/components/json-object-edit.scss
  50. 8
      ui/src/app/components/json-object-edit.tpl.html
  51. 45
      ui/src/app/components/tb-json-to-string.directive.js
  52. 38
      ui/src/app/entity/attribute/add-attribute-dialog.controller.js
  53. 34
      ui/src/app/entity/attribute/add-attribute-dialog.tpl.html
  54. 1
      ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
  55. 20
      ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
  56. 35
      ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js
  57. 24
      ui/src/app/entity/attribute/attribute-dialog-edit-json.scss
  58. 54
      ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html
  59. 143
      ui/src/app/entity/attribute/attribute-table.directive.js
  60. 8
      ui/src/app/entity/attribute/attribute-table.scss
  61. 4
      ui/src/app/entity/attribute/attribute-table.tpl.html
  62. 58
      ui/src/app/entity/attribute/edit-attribute-value.controller.js
  63. 30
      ui/src/app/entity/attribute/edit-attribute-value.tpl.html
  64. 2
      ui/src/app/locale/locale.constant-el_GR.json
  65. 8
      ui/src/app/locale/locale.constant-en_US.json
  66. 4
      ui/src/app/locale/locale.constant-ru_RU.json
  67. 4
      ui/src/app/locale/locale.constant-uk_UA.json

14
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -317,9 +317,9 @@
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3.5, "sizeY": 3.5,
"resources": [], "resources": [],
"templateHtml": "<tb-multiple-input-widget \n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-multiple-input-widget>", "templateHtml": "<tb-multiple-input-widget \n [ctx]=\"ctx\">\n</tb-multiple-input-widget>",
"templateCss": "", "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}",
"controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n\r\nself.onResize = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-resize', self.ctx.$scope.formId);\r\n}\r\n", "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified (only if action buttons are visible)\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n \"updateAllValues\",\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n \"fieldsInRow\"\n ]\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified (only if action buttons are visible)\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n \"updateAllValues\",\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n \"fieldsInRow\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n \"required\",\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"step\",\n \"requiredErrorMessage\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n \"required\",\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"step\",\n \"requiredErrorMessage\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@ -349,9 +349,9 @@
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 4.5, "sizeY": 4.5,
"resources": [], "resources": [],
"templateHtml": "<form name=\"claimDeviceForm\" class=\"claim-form\" ng-submit=\"claim()\">\n <fieldset ng-disabled=\"$root.loading || loading\">\n <md-input-container ng-class=\"{'show-label': showLabel}\" class=\"md-block\">\n <label>{{deviceLabel}}</label>\n <input ng-model=\"deviceObj.deviceName\" \n name=\"deviceName\" \n required\n >\n <div ng-messages=\"claimDeviceForm.deviceName.$error\">\n <div ng-message=\"required\">{{requiredErrorDevice}}</div>\n </div>\n </md-input-container>\n <md-input-container ng-if=\"secretKeyField\" class=\"md-block\" ng-class=\"{'show-label': showLabel}\">\n <label>{{secretKeyLabel}}</label>\n <input name=\"deviceSecret\" ng-model=\"deviceObj.deviceSecret\" required>\n <div ng-messages=\"claimDeviceForm.deviceSecret.$error\">\n <div ng-message=\"required\">{{requiredErrorSecretKey}}</div>\n </div>\n </md-input-container>\n </fieldset>\n <div class=\"md-block\" layout=\"row\" layout-align=\"end center\">\n <md-button type=\"submit\" ng-disabled=\"loading || claimDeviceForm.$invalid || !claimDeviceForm.$dirty\" class=\"md-raised md-primary\">{{labelClaimButon}}</md-button>\n </div>\n</form>", "templateHtml": "<form *ngIf=\"claimDeviceFormGroup\" #claimDeviceForm=\"ngForm\" [formGroup]=\"claimDeviceFormGroup\"\n tb-toast toastTarget=\"{{ toastTargetId }}\"\n class=\"claim-form\" (ngSubmit)=\"claim(claimDeviceForm)\">\n <fieldset [disabled]=\"(isLoading$ | async) || loading\">\n <mat-form-field class=\"mat-block\">\n <mat-label *ngIf=\"showLabel\">{{deviceLabel}}</mat-label>\n <input matInput formControlName=\"deviceName\" required>\n <mat-error *ngIf=\"claimDeviceFormGroup.get('deviceName').hasError('required')\">\n {{requiredErrorDevice}}\n </mat-error>\n </mat-form-field>\n <mat-form-field *ngIf=\"secretKeyField\" class=\"mat-block\">\n <mat-label *ngIf=\"showLabel\">{{secretKeyLabel}}</mat-label>\n <input matInput formControlName=\"deviceSecret\" required>\n <mat-error *ngIf=\"claimDeviceFormGroup.get('deviceSecret').hasError('required')\">\n {{requiredErrorSecretKey}}\n </mat-error>\n </mat-form-field>\n </fieldset>\n <div class=\"mat-block\" fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\" [disabled]=\"(isLoading$ | async) || claimDeviceForm.invalid || !claimDeviceForm.dirty\">\n {{labelClaimButon}}\n </button>\n </div>\n</form>\n",
"templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}", "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n",
"controllerScript": "let $scope;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let $q = $injector.get('$q');\n let $http = $injector.get('$http');\n let toast = $scope.$injector.get('toast');\n let utils = $scope.$injector.get('utils');\n let $translate = $scope.$injector.get('$translate');\n let $rootScope = $scope.$injector.get('$rootScope');\n let settings = self.ctx.settings || {};\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n $scope.deviceObj = {};\n \n const config = {\n ignoreErrors: true \n };\n \n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claim = () => {\n $scope.loading = true;\n claimDevice($scope.deviceObj.deviceName, $scope.deviceObj.deviceSecret, config).then(\n (data) => {\n successClaim();\n },\n (error) => {\n $scope.loading = false;\n if(error.status == 404) {\n toast.showError(deviceNotFound, angular.element('.claim-form'),'bottom left');\n } else if(error.status == 400) {\n toast.showError(failedClaimDevice, angular.element('.claim-form'),'bottom left');\n }\n }\n );\n }\n \n function claimDevice(deviceName, deviceSecret, config) {\n let deferred = $q.defer();\n let url = \"/api/customer/device/\" + deviceName + \"/claim\";\n let obj = deviceSecret ? { secretKey: deviceSecret } : {};\n $http.post(url, obj, config).then(\n (payload) => {\n deferred.resolve(payload.data); \n },\n (error) => {\n deferred.reject(error);\n }\n );\n return deferred.promise;\n }\n \n function updateAliasData() {\n var aliasIds = [];\n for (var id in self.ctx.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n var tasks = [];\n aliasIds.forEach(function(aliasId) {\n self.ctx.aliasController.setAliasUnresolved(aliasId);\n tasks.push(self.ctx.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(function() {\n $rootScope.$broadcast('widgetForceReInit');\n });\n }\n \n function successClaim() {\n resetForm();\n $scope.claimDeviceForm.$setPristine();\n $scope.claimDeviceForm.$setUntouched();\n $scope.loading = false;\n toast.showSuccess(successfulClaim, 2000);\n updateAliasData();\n }\n \n function resetForm() {\n $scope.deviceObj = {};\n }\n}\n\n", "controllerScript": "let $scope;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n}\n\nfunction init() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n let $translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n let deviceService = $scope.$injector.get(self.ctx.servicesMap.get('deviceService'));\n let settings = self.ctx.settings || {};\n \n $scope.toastTargetId = 'device-claiming-widget' + utils.guid();\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n\n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claimDeviceFormGroup = $scope.fb.group(\n {deviceName: ['', [$scope.validators.required]]}\n );\n if ($scope.secretKeyField) {\n $scope.claimDeviceFormGroup.addControl('deviceSecret', $scope.fb.control('', [$scope.validators.required]));\n }\n \n $scope.claim = function(claimDeviceForm) {\n $scope.loading = true;\n\n let deviceName = $scope.claimDeviceFormGroup.get('deviceName').value;\n let claimRequest = {};\n if ($scope.secretKeyField) {\n claimRequest.secretKey = $scope.claimDeviceFormGroup.get('deviceSecret').value;\n }\n deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(\n function (data) {\n successClaim(claimDeviceForm);\n self.ctx.detectChanges();\n },\n function (error) {\n $scope.loading = false;\n if(error.status == 404) {\n $scope.showErrorToast(deviceNotFound, 'bottom', 'left', $scope.toastTargetId);\n } else {\n let errorMessage = failedClaimDevice;\n if (error.status !== 400) {\n if (error.error && error.error.message) {\n errorMessage = error.error.message;\n }\n }\n $scope.showErrorToast(errorMessage, 'bottom', 'left', $scope.toastTargetId);\n } \n self.ctx.detectChanges();\n }\n );\n }\n\n function successClaim(claimDeviceForm) {\n let deviceObj = {\n deviceName: ''\n };\n if ($scope.secretKeyField) {\n deviceObj.deviceSecret = '';\n } \n claimDeviceForm.resetForm(); \n $scope.claimDeviceFormGroup.reset(deviceObj);\n $scope.loading = false;\n $scope.showSuccessToast(successfulClaim, 2000, 'bottom', 'left', $scope.toastTargetId);\n self.ctx.updateAliases();\n }\n \n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\n }]\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\n }]\n}",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"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;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"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;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@ -406,4 +406,4 @@
} }
} }
] ]
} }

2
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -451,7 +451,7 @@ public class DeviceController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('CUSTOMER_USER')") @PreAuthorize("hasAuthority('CUSTOMER_USER')")
@RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.POST) @RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> claimDevice(@PathVariable(DEVICE_NAME) String deviceName, public DeferredResult<ResponseEntity> claimDevice(@PathVariable(DEVICE_NAME) String deviceName,

121
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.service.install.DatabaseEntitiesUpgradeService;
import org.thingsboard.server.service.install.EntityDatabaseSchemaService; import org.thingsboard.server.service.install.EntityDatabaseSchemaService;
import org.thingsboard.server.service.install.SystemDataLoaderService; import org.thingsboard.server.service.install.SystemDataLoaderService;
import org.thingsboard.server.service.install.TsDatabaseSchemaService; import org.thingsboard.server.service.install.TsDatabaseSchemaService;
import org.thingsboard.server.service.install.migrate.EntitiesMigrateService;
import org.thingsboard.server.service.install.update.DataUpdateService; import org.thingsboard.server.service.install.update.DataUpdateService;
@Service @Service
@ -68,99 +69,95 @@ public class ThingsboardInstallService {
@Autowired @Autowired
private DataUpdateService dataUpdateService; private DataUpdateService dataUpdateService;
@Autowired(required = false)
private EntitiesMigrateService entitiesMigrateService;
public void performInstall() { public void performInstall() {
try { try {
if (isUpgrade) { if (isUpgrade) {
log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion); log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion);
switch (upgradeFromVersion) { if ("2.5.0-cassandra".equals(upgradeFromVersion)) {
case "1.2.3": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion log.info("Migrating ThingsBoard entities data from cassandra to SQL database ...");
log.info("Upgrading ThingsBoard from version 1.2.3 to 1.3.0 ..."); entitiesMigrateService.migrate();
log.info("Updating system data...");
databaseEntitiesUpgradeService.upgradeDatabase("1.2.3"); systemDataLoaderService.updateSystemWidgets();
} else {
switch (upgradeFromVersion) {
case "1.2.3": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion
log.info("Upgrading ThingsBoard from version 1.2.3 to 1.3.0 ...");
case "1.3.0": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion databaseEntitiesUpgradeService.upgradeDatabase("1.2.3");
log.info("Upgrading ThingsBoard from version 1.3.0 to 1.3.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("1.3.0"); case "1.3.0": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion
log.info("Upgrading ThingsBoard from version 1.3.0 to 1.3.1 ...");
case "1.3.1": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion databaseEntitiesUpgradeService.upgradeDatabase("1.3.0");
log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("1.3.1"); case "1.3.1": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion
log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
case "1.4.0": databaseEntitiesUpgradeService.upgradeDatabase("1.3.1");
log.info("Upgrading ThingsBoard from version 1.4.0 to 2.0.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("1.4.0"); case "1.4.0":
log.info("Upgrading ThingsBoard from version 1.4.0 to 2.0.0 ...");
dataUpdateService.updateData("1.4.0"); databaseEntitiesUpgradeService.upgradeDatabase("1.4.0");
case "2.0.0": dataUpdateService.updateData("1.4.0");
log.info("Upgrading ThingsBoard from version 2.0.0 to 2.1.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.0.0"); case "2.0.0":
log.info("Upgrading ThingsBoard from version 2.0.0 to 2.1.1 ...");
case "2.1.1": databaseEntitiesUpgradeService.upgradeDatabase("2.0.0");
log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.1.1"); case "2.1.1":
case "2.1.3": log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ...");
log.info("Upgrading ThingsBoard from version 2.1.3 to 2.2.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.1.3"); databaseEntitiesUpgradeService.upgradeDatabase("2.1.1");
case "2.1.3":
log.info("Upgrading ThingsBoard from version 2.1.3 to 2.2.0 ...");
case "2.3.0": databaseEntitiesUpgradeService.upgradeDatabase("2.1.3");
log.info("Upgrading ThingsBoard from version 2.3.0 to 2.3.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.3.0"); case "2.3.0":
log.info("Upgrading ThingsBoard from version 2.3.0 to 2.3.1 ...");
case "2.3.1": databaseEntitiesUpgradeService.upgradeDatabase("2.3.0");
log.info("Upgrading ThingsBoard from version 2.3.1 to 2.4.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.3.1"); case "2.3.1":
log.info("Upgrading ThingsBoard from version 2.3.1 to 2.4.0 ...");
case "2.4.0": databaseEntitiesUpgradeService.upgradeDatabase("2.3.1");
log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ...");
case "2.4.1": case "2.4.0":
log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ..."); log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.4.1"); case "2.4.1":
case "2.4.2": log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ...");
log.info("Upgrading ThingsBoard from version 2.4.2 to 2.4.3 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.4.2"); databaseEntitiesUpgradeService.upgradeDatabase("2.4.1");
case "2.4.2":
log.info("Upgrading ThingsBoard from version 2.4.2 to 2.4.3 ...");
case "2.4.3": databaseEntitiesUpgradeService.upgradeDatabase("2.4.2");
log.info("Upgrading ThingsBoard from version 2.4.3 to 2.5 ...");
if (databaseTsUpgradeService != null) { case "2.4.3":
databaseTsUpgradeService.upgradeDatabase("2.4.3"); log.info("Upgrading ThingsBoard from version 2.4.3 to 2.5 ...");
}
databaseEntitiesUpgradeService.upgradeDatabase("2.4.3");
log.info("Updating system data..."); if (databaseTsUpgradeService != null) {
databaseTsUpgradeService.upgradeDatabase("2.4.3");
}
databaseEntitiesUpgradeService.upgradeDatabase("2.4.3");
systemDataLoaderService.deleteSystemWidgetBundle("charts"); log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("cards");
systemDataLoaderService.deleteSystemWidgetBundle("maps");
systemDataLoaderService.deleteSystemWidgetBundle("analogue_gauges");
systemDataLoaderService.deleteSystemWidgetBundle("digital_gauges");
systemDataLoaderService.deleteSystemWidgetBundle("gpio_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("alarm_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("control_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("maps_v2");
systemDataLoaderService.deleteSystemWidgetBundle("gateway_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("input_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("date");
systemDataLoaderService.deleteSystemWidgetBundle("entity_admin_widgets");
systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.updateSystemWidgets();
break; break;
default: default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
}
} }
log.info("Upgrade finished successfully!"); log.info("Upgrade finished successfully!");

10
application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java

@ -25,6 +25,7 @@ import java.nio.file.Path;
import java.sql.CallableStatement; import java.sql.CallableStatement;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Types; import java.sql.Types;
@Slf4j @Slf4j
@ -93,6 +94,15 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
try { try {
CallableStatement callableStatement = conn.prepareCall("{" + query + "}"); CallableStatement callableStatement = conn.prepareCall("{" + query + "}");
callableStatement.execute(); callableStatement.execute();
SQLWarning warnings = callableStatement.getWarnings();
if (warnings != null) {
log.info("{}", warnings.getMessage());
SQLWarning nextWarning = warnings.getNextWarning();
while (nextWarning != null) {
log.info("{}", nextWarning.getMessage());
nextWarning = nextWarning.getNextWarning();
}
}
callableStatement.close(); callableStatement.close();
log.info(SUCCESSFULLY_EXECUTED_FUNCTION, query.replace(CALL_REGEX, "")); log.info(SUCCESSFULLY_EXECUTED_FUNCTION, query.replace(CALL_REGEX, ""));
Thread.sleep(2000); Thread.sleep(2000);

18
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -168,6 +168,24 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
installScripts.loadSystemWidgets(); installScripts.loadSystemWidgets();
} }
@Override
public void updateSystemWidgets() throws Exception {
this.deleteSystemWidgetBundle("charts");
this.deleteSystemWidgetBundle("cards");
this.deleteSystemWidgetBundle("maps");
this.deleteSystemWidgetBundle("analogue_gauges");
this.deleteSystemWidgetBundle("digital_gauges");
this.deleteSystemWidgetBundle("gpio_widgets");
this.deleteSystemWidgetBundle("alarm_widgets");
this.deleteSystemWidgetBundle("control_widgets");
this.deleteSystemWidgetBundle("maps_v2");
this.deleteSystemWidgetBundle("gateway_widgets");
this.deleteSystemWidgetBundle("input_widgets");
this.deleteSystemWidgetBundle("date");
this.deleteSystemWidgetBundle("entity_admin_widgets");
installScripts.loadSystemWidgets();
}
private User createUser(Authority authority, private User createUser(Authority authority,
TenantId tenantId, TenantId tenantId,
CustomerId customerId, CustomerId customerId,

33
application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.HsqlDao;
import org.thingsboard.server.dao.util.SqlDao;
@Service
@HsqlDao
@SqlDao
@Profile("install")
public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
implements EntityDatabaseSchemaService {
protected HsqlEntityDatabaseSchemaService() {
super("schema-entities-hsql.sql", "schema-entities-idx.sql");
}
}

6
application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java → application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java

@ -17,14 +17,16 @@ package org.thingsboard.server.service.install;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
@Service @Service
@SqlDao @SqlDao
@PsqlDao
@Profile("install") @Profile("install")
public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService public class PsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
implements EntityDatabaseSchemaService { implements EntityDatabaseSchemaService {
public SqlEntityDatabaseSchemaService() { public PsqlEntityDatabaseSchemaService() {
super("schema-entities.sql", "schema-entities-idx.sql"); super("schema-entities.sql", "schema-entities-idx.sql");
} }
} }

2
application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java

@ -23,6 +23,8 @@ public interface SystemDataLoaderService {
void loadSystemWidgets() throws Exception; void loadSystemWidgets() throws Exception;
void updateSystemWidgets() throws Exception;
void loadDemoData() throws Exception; void loadDemoData() throws Exception;
void deleteSystemWidgetBundle(String bundleAlias) throws Exception; void deleteSystemWidgetBundle(String bundleAlias) throws Exception;

2
application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java

@ -104,7 +104,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
executeDropStatement(conn, DROP_FUNCTION_INSERT_INTO_TENANT_TS_KV); executeDropStatement(conn, DROP_FUNCTION_INSERT_INTO_TENANT_TS_KV);
executeDropStatement(conn, DROP_FUNCTION_INSERT_INTO_TS_KV_LATEST); executeDropStatement(conn, DROP_FUNCTION_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE tenant_ts_kv ADD COLUMN json_v json;");
executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN json_v json;");
log.info("schema timeseries updated!"); log.info("schema timeseries updated!");

253
application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java

@ -0,0 +1,253 @@
package org.thingsboard.server.service.install.migrate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.util.SqlDao;
import org.thingsboard.server.service.install.EntityDatabaseSchemaService;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Arrays;
import java.util.List;
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.bigintColumn;
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.booleanColumn;
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.doubleColumn;
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.enumToIntColumn;
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.idColumn;
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.stringColumn;
@Service
@Profile("install")
@SqlDao
@Slf4j
public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateService {
@Autowired
private EntityDatabaseSchemaService entityDatabaseSchemaService;
@Autowired
protected CassandraCluster cluster;
@Value("${spring.datasource.url}")
protected String dbUrl;
@Value("${spring.datasource.username}")
protected String dbUserName;
@Value("${spring.datasource.password}")
protected String dbPassword;
@Override
public void migrate() throws Exception {
log.info("Performing migration of entities data from cassandra to SQL database ...");
entityDatabaseSchemaService.createDatabaseSchema();
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
conn.setAutoCommit(true);
for (CassandraToSqlTable table: tables) {
table.migrateToSql(cluster.getSession(), conn);
}
} catch (Exception e) {
log.error("Unexpected error during ThingsBoard entities data migration!", e);
throw e;
}
}
private static List<CassandraToSqlTable> tables = Arrays.asList(
new CassandraToSqlTable("admin_settings",
idColumn("id"),
stringColumn("key"),
stringColumn("json_value")),
new CassandraToSqlTable("alarm",
idColumn("id"),
idColumn("tenant_id"),
stringColumn("type"),
idColumn("originator_id"),
enumToIntColumn("originator_type", EntityType.class),
stringColumn("severity"),
stringColumn("status"),
bigintColumn("start_ts"),
bigintColumn("end_ts"),
bigintColumn("ack_ts"),
bigintColumn("clear_ts"),
stringColumn("details", "additional_info"),
booleanColumn("propagate"),
stringColumn("propagate_relation_types")),
new CassandraToSqlTable("asset",
idColumn("id"),
idColumn("tenant_id"),
idColumn("customer_id"),
stringColumn("name"),
stringColumn("type"),
stringColumn("label"),
stringColumn("search_text"),
stringColumn("additional_info")),
new CassandraToSqlTable("audit_log_by_tenant_id", "audit_log",
idColumn("id"),
idColumn("tenant_id"),
idColumn("customer_id"),
idColumn("entity_id"),
stringColumn("entity_type"),
stringColumn("entity_name"),
idColumn("user_id"),
stringColumn("user_name"),
stringColumn("action_type"),
stringColumn("action_data"),
stringColumn("action_status"),
stringColumn("action_failure_details")),
new CassandraToSqlTable("attributes_kv_cf", "attribute_kv",
idColumn("entity_id"),
stringColumn("entity_type"),
stringColumn("attribute_type"),
stringColumn("attribute_key"),
booleanColumn("bool_v"),
stringColumn("str_v"),
bigintColumn("long_v"),
doubleColumn("dbl_v"),
stringColumn("json_v"),
bigintColumn("last_update_ts")),
new CassandraToSqlTable("component_descriptor",
idColumn("id"),
stringColumn("type"),
stringColumn("scope"),
stringColumn("name"),
stringColumn("search_text"),
stringColumn("clazz"),
stringColumn("configuration_descriptor"),
stringColumn("actions")),
new CassandraToSqlTable("customer",
idColumn("id"),
idColumn("tenant_id"),
stringColumn("title"),
stringColumn("search_text"),
stringColumn("country"),
stringColumn("state"),
stringColumn("city"),
stringColumn("address"),
stringColumn("address2"),
stringColumn("zip"),
stringColumn("phone"),
stringColumn("email"),
stringColumn("additional_info")),
new CassandraToSqlTable("dashboard",
idColumn("id"),
idColumn("tenant_id"),
stringColumn("title"),
stringColumn("search_text"),
stringColumn("assigned_customers"),
stringColumn("configuration")),
new CassandraToSqlTable("device",
idColumn("id"),
idColumn("tenant_id"),
idColumn("customer_id"),
stringColumn("name"),
stringColumn("type"),
stringColumn("label"),
stringColumn("search_text"),
stringColumn("additional_info")),
new CassandraToSqlTable("device_credentials",
idColumn("id"),
idColumn("device_id"),
stringColumn("credentials_type"),
stringColumn("credentials_id"),
stringColumn("credentials_value")),
new CassandraToSqlTable("event",
idColumn("id"),
idColumn("tenant_id"),
idColumn("entity_id"),
stringColumn("entity_type"),
stringColumn("event_type"),
stringColumn("event_uid"),
stringColumn("body")),
new CassandraToSqlTable("relation",
idColumn("from_id"),
stringColumn("from_type"),
idColumn("to_id"),
stringColumn("to_type"),
stringColumn("relation_type_group"),
stringColumn("relation_type"),
stringColumn("additional_info")),
new CassandraToSqlTable("user", "tb_user",
idColumn("id"),
idColumn("tenant_id"),
idColumn("customer_id"),
stringColumn("email"),
stringColumn("search_text"),
stringColumn("authority"),
stringColumn("first_name"),
stringColumn("last_name"),
stringColumn("additional_info")),
new CassandraToSqlTable("tenant",
idColumn("id"),
stringColumn("title"),
stringColumn("search_text"),
stringColumn("region"),
stringColumn("country"),
stringColumn("state"),
stringColumn("city"),
stringColumn("address"),
stringColumn("address2"),
stringColumn("zip"),
stringColumn("phone"),
stringColumn("email"),
stringColumn("additional_info")),
new CassandraToSqlTable("user_credentials",
idColumn("id"),
idColumn("user_id"),
booleanColumn("enabled"),
stringColumn("password"),
stringColumn("activate_token"),
stringColumn("reset_token")),
new CassandraToSqlTable("widget_type",
idColumn("id"),
idColumn("tenant_id"),
stringColumn("bundle_alias"),
stringColumn("alias"),
stringColumn("name"),
stringColumn("descriptor")),
new CassandraToSqlTable("widgets_bundle",
idColumn("id"),
idColumn("tenant_id"),
stringColumn("alias"),
stringColumn("title"),
stringColumn("search_text")),
new CassandraToSqlTable("rule_chain",
idColumn("id"),
idColumn("tenant_id"),
stringColumn("name"),
stringColumn("search_text"),
idColumn("first_rule_node_id"),
booleanColumn("root"),
booleanColumn("debug_mode"),
stringColumn("configuration"),
stringColumn("additional_info")),
new CassandraToSqlTable("rule_node",
idColumn("id"),
idColumn("rule_chain_id"),
stringColumn("type"),
stringColumn("name"),
booleanColumn("debug_mode"),
stringColumn("search_text"),
stringColumn("configuration"),
stringColumn("additional_info")),
new CassandraToSqlTable("entity_view",
idColumn("id"),
idColumn("tenant_id"),
idColumn("customer_id"),
idColumn("entity_id"),
stringColumn("entity_type"),
stringColumn("name"),
stringColumn("type"),
stringColumn("keys"),
bigintColumn("start_ts"),
bigintColumn("end_ts"),
stringColumn("search_text"),
stringColumn("additional_info"))
);
}

159
application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java

@ -0,0 +1,159 @@
package org.thingsboard.server.service.install.migrate;
import com.datastax.driver.core.Row;
import lombok.Data;
import org.thingsboard.server.common.data.UUIDConverter;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
@Data
public class CassandraToSqlColumn {
private String cassandraColumnName;
private String sqlColumnName;
private CassandraToSqlColumnType type;
private int sqlType;
private Class<? extends Enum> enumClass;
public static CassandraToSqlColumn idColumn(String name) {
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ID);
}
public static CassandraToSqlColumn stringColumn(String name) {
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.STRING);
}
public static CassandraToSqlColumn stringColumn(String cassandraColumnName, String sqlColumnName) {
return new CassandraToSqlColumn(cassandraColumnName, sqlColumnName);
}
public static CassandraToSqlColumn bigintColumn(String name) {
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BIGINT);
}
public static CassandraToSqlColumn doubleColumn(String name) {
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.DOUBLE);
}
public static CassandraToSqlColumn booleanColumn(String name) {
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BOOLEAN);
}
public static CassandraToSqlColumn enumToIntColumn(String name, Class<? extends Enum> enumClass) {
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ENUM_TO_INT, enumClass);
}
public CassandraToSqlColumn(String columnName) {
this(columnName, columnName, CassandraToSqlColumnType.STRING, null);
}
public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type) {
this(columnName, columnName, type, null);
}
public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type, Class<? extends Enum> enumClass) {
this(columnName, columnName, type, enumClass);
}
public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName) {
this(cassandraColumnName, sqlColumnName, CassandraToSqlColumnType.STRING, null);
}
public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName, CassandraToSqlColumnType type,
Class<? extends Enum> enumClass) {
this.cassandraColumnName = cassandraColumnName;
this.sqlColumnName = sqlColumnName;
this.type = type;
this.enumClass = enumClass;
switch (this.type) {
case ID:
case STRING:
this.sqlType = Types.VARCHAR;
break;
case DOUBLE:
this.sqlType = Types.DOUBLE;
break;
case INTEGER:
case ENUM_TO_INT:
this.sqlType = Types.INTEGER;
break;
case FLOAT:
this.sqlType = Types.FLOAT;
break;
case BIGINT:
this.sqlType = Types.BIGINT;
break;
case BOOLEAN:
this.sqlType = Types.BOOLEAN;
break;
}
}
public void prepareColumnValue(Row row, PreparedStatement sqlInsertStatement, int index) throws SQLException {
String value = this.getColumnValue(row, index);
this.setColumnValue(sqlInsertStatement, index, value);
}
private String getColumnValue(Row row, int index) {
if (row.isNull(index)) {
return null;
} else {
switch (this.type) {
case ID:
return UUIDConverter.fromTimeUUID(row.getUUID(index));
case DOUBLE:
return Double.toString(row.getDouble(index));
case INTEGER:
return Integer.toString(row.getInt(index));
case FLOAT:
return Float.toString(row.getFloat(index));
case BIGINT:
return Long.toString(row.getLong(index));
case BOOLEAN:
return Boolean.toString(row.getBool(index));
case STRING:
case ENUM_TO_INT:
default:
return row.getString(index);
}
}
}
private void setColumnValue(PreparedStatement sqlInsertStatement, int index, String value) throws SQLException {
if (value == null) {
sqlInsertStatement.setNull(index, this.sqlType);
} else {
switch (this.type) {
case DOUBLE:
sqlInsertStatement.setDouble(index, Double.parseDouble(value));
break;
case INTEGER:
sqlInsertStatement.setInt(index, Integer.parseInt(value));
break;
case FLOAT:
sqlInsertStatement.setFloat(index, Float.parseFloat(value));
break;
case BIGINT:
sqlInsertStatement.setLong(index, Long.parseLong(value));
break;
case BOOLEAN:
sqlInsertStatement.setBoolean(index, Boolean.parseBoolean(value));
break;
case ENUM_TO_INT:
Enum enumVal = Enum.valueOf(this.enumClass, value);
int intValue = enumVal.ordinal();
sqlInsertStatement.setInt(index, intValue);
break;
case STRING:
case ID:
default:
sqlInsertStatement.setString(index, value);
break;
}
}
}
}

12
application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java

@ -0,0 +1,12 @@
package org.thingsboard.server.service.install.migrate;
public enum CassandraToSqlColumnType {
ID,
DOUBLE,
INTEGER,
FLOAT,
BIGINT,
BOOLEAN,
STRING,
ENUM_TO_INT
}

99
application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java

@ -0,0 +1,99 @@
package org.thingsboard.server.service.install.migrate;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.SimpleStatement;
import com.datastax.driver.core.Statement;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
@Data
@Slf4j
public class CassandraToSqlTable {
private String cassandraCf;
private String sqlTableName;
private List<CassandraToSqlColumn> columns;
public CassandraToSqlTable(String tableName, CassandraToSqlColumn... columns) {
this(tableName, tableName, columns);
}
public CassandraToSqlTable(String cassandraCf, String sqlTableName, CassandraToSqlColumn... columns) {
this.cassandraCf = cassandraCf;
this.sqlTableName = sqlTableName;
this.columns = Arrays.asList(columns);
}
public void migrateToSql(Session session, Connection conn) throws SQLException {
log.info("Migrating data from cassandra '{}' Column Family to '{}' SQL table...", this.cassandraCf, this.sqlTableName);
PreparedStatement sqlInsertStatement = createSqlInsertStatement(conn);
Statement cassandraSelectStatement = createCassandraSelectStatement();
cassandraSelectStatement.setFetchSize(100);
ResultSet rs = session.execute(cassandraSelectStatement);
Iterator<Row> iter = rs.iterator();
int rowCounter = 0;
while (iter.hasNext()) {
Row row = iter.next();
if (row != null) {
this.migrateRowToSql(row, sqlInsertStatement);
rowCounter++;
if (rowCounter % 100 == 0) {
sqlInsertStatement.executeBatch();
log.info("{} records migrated so far...", rowCounter);
}
}
}
if (rowCounter % 100 > 0) {
sqlInsertStatement.executeBatch();
}
sqlInsertStatement.close();
log.info("{} total records migrated.", rowCounter);
log.info("Finished migration data from cassandra '{}' Column Family to '{}' SQL table.", this.cassandraCf, this.sqlTableName);
}
private void migrateRowToSql(Row row, PreparedStatement sqlInsertStatement) throws SQLException {
for (int i=0; i<this.columns.size();i++) {
CassandraToSqlColumn column = this.columns.get(i);
column.prepareColumnValue(row, sqlInsertStatement, i);
}
sqlInsertStatement.addBatch();
}
private Statement createCassandraSelectStatement() {
StringBuilder selectStatementBuilder = new StringBuilder();
selectStatementBuilder.append("SELECT ");
for (CassandraToSqlColumn column : columns) {
selectStatementBuilder.append(column.getCassandraColumnName()).append(",");
}
selectStatementBuilder.deleteCharAt(selectStatementBuilder.length() - 1);
selectStatementBuilder.append(" FROM ").append(cassandraCf);
return new SimpleStatement(selectStatementBuilder.toString());
}
private PreparedStatement createSqlInsertStatement(Connection conn) throws SQLException {
StringBuilder insertStatementBuilder = new StringBuilder();
insertStatementBuilder.append("INSERT INTO ").append(this.sqlTableName).append(" (");
for (CassandraToSqlColumn column : columns) {
insertStatementBuilder.append(column.getSqlColumnName()).append(",");
}
insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
insertStatementBuilder.append(") VALUES (");
for (CassandraToSqlColumn ignored : columns) {
insertStatementBuilder.append("?").append(",");
}
insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
insertStatementBuilder.append(")");
return conn.prepareStatement(insertStatementBuilder.toString());
}
}

7
application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java

@ -0,0 +1,7 @@
package org.thingsboard.server.service.install.migrate;
public interface EntitiesMigrateService {
void migrate() throws Exception;
}

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java

@ -53,7 +53,7 @@ public class TimescaleInsertTsRepository extends AbstractInsertRepository implem
if (tsKvEntity.getBooleanValue() != null) { if (tsKvEntity.getBooleanValue() != null) {
ps.setBoolean(5, tsKvEntity.getBooleanValue()); ps.setBoolean(5, tsKvEntity.getBooleanValue());
ps.setBoolean(9, tsKvEntity.getBooleanValue()); ps.setBoolean(10, tsKvEntity.getBooleanValue());
} else { } else {
ps.setNull(5, Types.BOOLEAN); ps.setNull(5, Types.BOOLEAN);
ps.setNull(10, Types.BOOLEAN); ps.setNull(10, Types.BOOLEAN);

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java

@ -40,9 +40,9 @@ public class AggregationRepository {
public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType "; public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType ";
public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, MAX(tskv.json_v) AS jsonValue, 'MAX' AS aggType "; public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType ";
public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, MIN(tskv.json_v) AS jsonValue,'MIN' AS aggType "; public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType ";
public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType "; public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType ";

2
ui-ngx/src/app/core/http/device.service.ts

@ -135,7 +135,7 @@ export class DeviceService {
public claimDevice(deviceName: string, claimRequest: ClaimRequest, public claimDevice(deviceName: string, claimRequest: ClaimRequest,
config?: RequestConfig): Observable<ClaimResult> { config?: RequestConfig): Observable<ClaimResult> {
return this.http.post<ClaimResult>(`api/customer/device/${deviceName}/claim`, claimRequest, defaultHttpOptionsFromConfig(config)); return this.http.post<ClaimResult>(`/api/customer/device/${deviceName}/claim`, claimRequest, defaultHttpOptionsFromConfig(config));
} }
public unclaimDevice(deviceName: string, config?: RequestConfig) { public unclaimDevice(deviceName: string, config?: RequestConfig) {

2
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -168,7 +168,7 @@
class="tb-value-cell" class="tb-value-cell"
(click)="editAttribute($event, attribute)"> (click)="editAttribute($event, attribute)">
<div fxLayout="row"> <div fxLayout="row">
<span fxFlex>{{attribute.value}}</span> <span fxFlex>{{attribute.value | tbJson}}</span>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"> <span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</span> </span>

18
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -75,31 +75,21 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
verticalPosition: NotificationVerticalPosition = 'bottom', verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left', horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) { target?: string) {
this.showToast('success', message, duration, verticalPosition, horizontalPosition, target); this.ctx.showSuccessToast(message, duration, verticalPosition, horizontalPosition, target);
} }
showErrorToast(message: string, showErrorToast(message: string,
verticalPosition: NotificationVerticalPosition = 'bottom', verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left', horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) { target?: string) {
this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target); this.ctx.showErrorToast(message, verticalPosition, horizontalPosition, target);
} }
showToast(type: NotificationType, message: string, duration: number = 1000, showToast(type: NotificationType, message: string, duration: number,
verticalPosition: NotificationVerticalPosition = 'bottom', verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left', horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) { target?: string) {
this.store.dispatch(new ActionNotificationShow( this.ctx.showToast(type, message, duration, verticalPosition, horizontalPosition, target);
{
message,
type,
duration,
verticalPosition,
horizontalPosition,
target,
panelClass: this.ctx.widgetNamespace,
forceDismiss: true
}));
} }
} }

126
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -0,0 +1,126 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #formContainer class="tb-multiple-input"
#multipleInputForm="ngForm"
[formGroup]="multipleInputFormGroup"
tb-toast toastTarget="{{ toastTargetId }}"
(ngSubmit)="save()" novalidate autocomplete="off">
<div style="padding: 0 8px;" *ngIf="entityDetected && isAllParametersValid">
<fieldset *ngFor="let source of sources" [ngClass]="{'fields-group': settings.showGroupTitle}">
<legend class="group-title" *ngIf="settings.showGroupTitle">{{ getGroupTitle(source.datasource) }}
</legend>
<div fxLayout="row" class="layout-wrap"
[ngClass]="{'vertical-alignment': isVerticalAlignment || changeAlignment}">
<div *ngFor="let key of visibleKeys(source)"
[ngStyle]="{width: (isVerticalAlignment || changeAlignment) ? '100%' : inputWidthSettings}">
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'string'">
<mat-form-field class="mat-block">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="text"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{key.settings.requiredErrorMessage}}
</mat-error>
</mat-form-field>
</div>
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'double' ||
key.settings.dataKeyValueType === 'integer'">
<mat-form-field class="mat-block">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="number"
step="{{key.settings.step}}"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{key.settings.requiredErrorMessage}}
</mat-error>
</mat-form-field>
</div>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'">
<mat-checkbox formControlName="{{key.formId}}"
(change)="inputChanged(source, key)">
{{key.label}}
</mat-checkbox>
</div>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'">
<mat-slide-toggle formControlName="{{key.formId}}"
(change)="inputChanged(source, key)">
{{key.label}}
</mat-slide-toggle>
</div>
<div class="input-field mat-block date-time-input" *ngIf="(key.settings.dataKeyValueType === 'dateTime') ||
(key.settings.dataKeyValueType === 'date') ||
(key.settings.dataKeyValueType === 'time')" fxLayout="column">
<div fxLayout="row" [ngClass]="{'vertically-aligned': smallWidthContainer}" fxLayoutGap="16px">
<mat-form-field>
<mat-placeholder>{{key.label}}</mat-placeholder>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}"
openOnFocus="true"></mat-datetimepicker>
<input matInput formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
[matDatetimepicker]="datePicker"
(focus)="key.isFocused = true;"
(blur)="key.isFocused = false;"
(dateChange)="inputChanged(source, key)">
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{key.settings.requiredErrorMessage}}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</div>
</fieldset>
<div class="mat-padding" fxLayout="row" fxLayoutAlign="end center"
*ngIf="entityDetected && settings.showActionButtons">
<button mat-button color="primary" type="button"
(click)="discardAll()" style="max-height: 50px; margin-right:20px;"
[disabled]="!multipleInputForm.dirty">
{{ 'action.undo' | translate }}
</button>
<button mat-button mat-raised-button color="primary" type="submit"
style="max-height: 50px; margin-right:20px;"
[disabled]="!multipleInputForm.dirty || multipleInputForm.invalid">
{{ 'action.save' | translate }}
</button>
</div>
</div>
<div class="tb-multiple-input__errors" fxLayout="column" fxLayoutAlign="center center" style="height: 100%;"
*ngIf="!entityDetected || !isAllParametersValid">
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" [fxHide]="entityDetected">
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
</div>
<div style="text-align: center; font-size: 18px; color: #a0a0a0;"
[fxShow]="entityDetected && !isAllParametersValid">
{{ 'widgets.input-widgets.not-allowed-entity' | translate }}
</div>
</div>
</form>

75
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss

@ -0,0 +1,75 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-multiple-input {
height: 100%;
overflow-x: hidden;
overflow-y: auto;
.fields-group {
padding: 0 8px;
margin: 10px 0;
border: 1px groove rgba(0, 0, 0, .25);
legend {
color: rgba(0, 0, 0, .7);
}
}
.input-field {
padding-right: 10px;
mat-form-field {
margin-bottom: 5px;
}
}
mat-checkbox,
mat-slide-toggle {
display: block;
margin-top: 20px;
margin-bottom: 16px;
white-space: normal;
}
.date-time-input {
mat-form-field {
width: 100%;
margin: 2px 0;
}
}
.vertical-alignment {
flex-direction: column;
mat-checkbox,
mat-slide-toggle {
margin-top: 18px;
}
mat-slide-toggle {
display: flex;
justify-content: space-between;
}
}
.vertically-aligned {
flex-direction: column;
}
}
}

483
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -0,0 +1,483 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { isDefined, isEqual, isUndefined } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models';
import * as _moment from 'moment';
import { FormBuilder, FormGroup, NgForm, ValidatorFn, Validators } from '@angular/forms';
import { RequestConfig } from '@core/http/http-utils';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { forkJoin, Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
type FieldAlignment = 'row' | 'column';
type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries';
type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' |
'booleanCheckbox' | 'booleanSwitch' |
'dateTime' | 'date' | 'time';
type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly';
interface MultipleInputWidgetSettings {
widgetTitle: string;
showActionButtons: boolean;
updateAllValues: boolean;
showResultMessage: boolean;
showGroupTitle: boolean;
groupTitle: string;
fieldsAlignment: FieldAlignment;
fieldsInRow: number;
attributesShared?: boolean;
}
interface MultipleInputWidgetDataKeySettings {
dataKeyType: MultipleInputWidgetDataKeyType;
dataKeyValueType: MultipleInputWidgetDataKeyValueType;
required: boolean;
isEditable: MultipleInputWidgetDataKeyEditableType;
disabledOnDataKey: string;
dataKeyHidden: boolean;
step: number;
requiredErrorMessage: string;
icon: string;
inputTypeNumber?: boolean;
readOnly?: boolean;
disabledOnCondition?: boolean;
}
interface MultipleInputWidgetDataKey extends DataKey {
formId?: string;
settings: MultipleInputWidgetDataKeySettings;
isFocused: boolean;
value?: any;
}
interface MultipleInputWidgetSource {
datasource: Datasource;
keys: MultipleInputWidgetDataKey[];
}
@Component({
selector: 'tb-multiple-input-widget ',
templateUrl: './multiple-input-widget.component.html',
styleUrls: ['./multiple-input-widget.component.scss']
})
export class MultipleInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
@ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>;
@ViewChild('multipleInputForm', {static: true}) multipleInputForm: NgForm;
@Input()
ctx: WidgetContext;
private formResizeListener: any;
private settings: MultipleInputWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private datasources: Array<Datasource>;
private sources: Array<MultipleInputWidgetSource> = [];
isVerticalAlignment: boolean;
inputWidthSettings: string;
changeAlignment: boolean;
smallWidthContainer: boolean;
entityDetected = false;
isAllParametersValid = true;
multipleInputFormGroup: FormGroup;
toastTargetId = 'multiple-input-widget' + this.utils.guid();
constructor(protected store: Store<AppState>,
private elementRef: ElementRef,
private ngZone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
private fb: FormBuilder,
private attributeService: AttributeService,
private translate: TranslateService) {
super(store);
}
ngOnInit(): void {
this.ctx.$scope.multipleInputWidget = this;
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.datasources = this.subscription.datasources;
this.initializeConfig();
this.updateDatasources();
this.buildForm();
this.ctx.updateWidgetParams();
this.formResizeListener = this.resize.bind(this);
// @ts-ignore
addResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
}
ngOnDestroy(): void {
if (this.formResizeListener) {
// @ts-ignore
removeResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
}
}
private initializeConfig() {
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
}
this.settings.groupTitle = this.settings.groupTitle || '${entityName}';
// For backward compatibility
if (isUndefined(this.settings.showActionButtons)) {
this.settings.showActionButtons = true;
}
if (isUndefined(this.settings.fieldsAlignment)) {
this.settings.fieldsAlignment = 'row';
}
if (isUndefined(this.settings.fieldsInRow)) {
this.settings.fieldsInRow = 2;
}
// For backward compatibility
this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row');
if (!this.isVerticalAlignment && this.settings.fieldsInRow) {
this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%';
}
this.updateWidgetDisplaying();
}
private updateDatasources() {
if (this.datasources && this.datasources.length) {
this.entityDetected = true;
let keyIndex = 0;
this.datasources.forEach((datasource) => {
const source: MultipleInputWidgetSource = {
datasource,
keys: []
};
if (datasource.type === DatasourceType.entity) {
datasource.dataKeys.forEach((dataKey: MultipleInputWidgetDataKey) => {
if ((datasource.entityType !== EntityType.DEVICE) && (dataKey.settings.dataKeyType === 'shared')) {
this.isAllParametersValid = false;
}
if (dataKey.units) {
dataKey.label += ' (' + dataKey.units + ')';
}
dataKey.formId = (++keyIndex)+'';
dataKey.isFocused = false;
// For backward compatibility
if (isUndefined(dataKey.settings.dataKeyType)) {
if (this.settings.attributesShared) {
dataKey.settings.dataKeyType = 'shared';
} else {
dataKey.settings.dataKeyType = 'server';
}
}
if (isUndefined(dataKey.settings.dataKeyValueType)) {
if (dataKey.settings.inputTypeNumber) {
dataKey.settings.dataKeyValueType = 'double';
} else {
dataKey.settings.dataKeyValueType = 'string';
}
}
if (isUndefined(dataKey.settings.isEditable)) {
if (dataKey.settings.readOnly) {
dataKey.settings.isEditable = 'readonly';
} else {
dataKey.settings.isEditable = 'editable';
}
}
// For backward compatibility
source.keys.push(dataKey);
});
} else {
this.entityDetected = false;
}
this.sources.push(source);
});
}
}
private buildForm() {
this.multipleInputFormGroup = this.fb.group({});
this.sources.forEach((source) => {
for (const key of this.visibleKeys(source)) {
const validators: ValidatorFn[] = [];
if (key.settings.required) {
validators.push(Validators.required);
}
if (key.settings.dataKeyValueType === 'integer') {
validators.push(Validators.pattern(/^-?[0-9]+$/));
}
const formControl = this.fb.control(
{ value: key.value,
disabled: key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition},
validators
);
this.multipleInputFormGroup.addControl(key.formId, formControl);
}
});
}
private updateWidgetData(data: Array<DatasourceData>) {
let dataIndex = 0;
this.sources.forEach((source) => {
source.keys.forEach((key) => {
const keyData = data[dataIndex].data;
if (keyData && keyData.length) {
let value;
switch (key.settings.dataKeyValueType) {
case 'dateTime':
case 'date':
value = _moment(keyData[0][1]).toDate();
break;
case 'time':
value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate();
break;
case 'booleanCheckbox':
case 'booleanSwitch':
value = (keyData[0][1] === 'true');
break;
default:
value = keyData[0][1];
}
key.value = value;
}
if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) {
const conditions = data.filter((item) => {
return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey;
});
if (conditions && conditions.length) {
if (conditions[0].data.length) {
if (conditions[0].data[0][1] === 'false') {
key.settings.disabledOnCondition = true;
} else {
key.settings.disabledOnCondition = !conditions[0].data[0][1];
}
}
}
}
if (!key.settings.dataKeyHidden) {
if (key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition) {
this.multipleInputFormGroup.get(key.formId).disable({emitEvent: false});
} else {
this.multipleInputFormGroup.get(key.formId).enable({emitEvent: false});
}
const dirty = this.multipleInputFormGroup.get(key.formId).dirty;
if (!key.isFocused && !dirty) {
this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false});
}
}
dataIndex++;
});
});
}
private updateWidgetDisplaying() {
this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620);
this.smallWidthContainer = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 420);
}
public onDataUpdated() {
this.ngZone.run(() => {
this.updateWidgetData(this.subscription.data);
this.ctx.detectChanges();
});
}
private resize() {
this.ngZone.run(() => {
this.updateWidgetDisplaying();
this.ctx.detectChanges();
});
}
public getGroupTitle(datasource: Datasource): string {
return this.utils.createLabelFromDatasource(datasource, this.settings.groupTitle);
}
public visibleKeys(source: MultipleInputWidgetSource): MultipleInputWidgetDataKey[] {
return source.keys.filter(key => !key.settings.dataKeyHidden);
}
public datePickerType(keyType: MultipleInputWidgetDataKeyValueType): string {
switch (keyType) {
case 'dateTime':
return 'datetime';
case 'date':
return 'date';
case 'time':
return 'time';
}
}
public focusInputElement($event: Event) {
($event.target as HTMLInputElement).select();
}
public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) {
if (!this.settings.showActionButtons) {
const currentValue = this.multipleInputFormGroup.get(key.formId).value;
if (!key.settings.required || (key.settings.required && isDefined(currentValue))) {
const dataToSave: MultipleInputWidgetSource = {
datasource: source.datasource,
keys: [key]
};
this.save(dataToSave);
}
}
}
public save(dataToSave?: MultipleInputWidgetSource) {
const config: RequestConfig = {
ignoreLoading: !this.settings.showActionButtons
};
let data: Array<MultipleInputWidgetSource>;
if (dataToSave) {
data = [dataToSave];
} else {
data = this.sources;
}
const tasks: Observable<any>[] = [];
data.forEach((toSave) => {
const serverAttributes: AttributeData[] = [];
const sharedAttributes: AttributeData[] = [];
const telemetry: AttributeData[] = [];
for (const key of this.visibleKeys(toSave)) {
const currentValue = this.multipleInputFormGroup.get(key.formId).value;
if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) {
const attribute: AttributeData = {
key: key.name,
value: null
};
if (currentValue) {
switch (key.settings.dataKeyValueType) {
case 'dateTime':
case 'date':
attribute.value = currentValue.getTime();
break;
case 'time':
attribute.value = currentValue.getTime() - _moment().startOf('day').valueOf();
break;
default:
attribute.value = currentValue;
}
} else {
if (currentValue === '') {
attribute.value = null;
} else {
attribute.value = currentValue;
}
}
switch (key.settings.dataKeyType) {
case 'shared':
sharedAttributes.push(attribute);
break;
case 'timeseries':
telemetry.push(attribute);
break;
default:
serverAttributes.push(attribute);
}
}
}
const entityId: EntityId = {
entityType: toSave.datasource.entityType,
id: toSave.datasource.entityId
};
if (serverAttributes.length) {
tasks.push(this.attributeService.saveEntityAttributes(
entityId,
AttributeScope.SERVER_SCOPE,
serverAttributes,
config
));
}
if (sharedAttributes.length) {
tasks.push(this.attributeService.saveEntityAttributes(
entityId,
AttributeScope.SHARED_SCOPE,
sharedAttributes,
config
));
}
if (telemetry.length) {
tasks.push(this.attributeService.saveEntityTimeseries(
entityId,
LatestTelemetry.LATEST_TELEMETRY,
telemetry,
config
));
}
});
if (tasks.length) {
forkJoin(tasks).subscribe(
() => {
this.multipleInputForm.resetForm();
this.multipleInputFormGroup.markAsPristine();
if (this.settings.showResultMessage) {
this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'),
1000, 'bottom', 'left', this.toastTargetId);
}
},
() => {
if (this.settings.showResultMessage) {
this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'),
'bottom', 'left', this.toastTargetId);
}
});
} else {
this.multipleInputForm.resetForm();
this.multipleInputFormGroup.markAsPristine();
}
}
public discardAll() {
this.multipleInputForm.resetForm();
this.sources.forEach((source) => {
for (const key of this.visibleKeys(source)) {
this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false});
}
});
this.multipleInputFormGroup.markAsPristine();
}
}

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -30,6 +30,7 @@ import {
DateRangeNavigatorPanelComponent, DateRangeNavigatorPanelComponent,
DateRangeNavigatorWidgetComponent DateRangeNavigatorWidgetComponent
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component'; } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -41,7 +42,8 @@ import {
TimeseriesTableWidgetComponent, TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent, EntitiesHierarchyWidgetComponent,
DateRangeNavigatorWidgetComponent, DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent DateRangeNavigatorPanelComponent,
MultipleInputWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -55,7 +57,8 @@ import {
TimeseriesTableWidgetComponent, TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent, EntitiesHierarchyWidgetComponent,
RpcWidgetsModule, RpcWidgetsModule,
DateRangeNavigatorWidgetComponent DateRangeNavigatorWidgetComponent,
MultipleInputWidgetComponent
], ],
providers: [ providers: [
CustomDialogService CustomDialogService

1
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -260,6 +260,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext = this.dashboardWidget.widgetContext; this.widgetContext = this.dashboardWidget.widgetContext;
this.widgetContext.changeDetector = this.cd; this.widgetContext.changeDetector = this.cd;
this.widgetContext.ngZone = this.ngZone; this.widgetContext.ngZone = this.ngZone;
this.widgetContext.store = this.store;
this.widgetContext.servicesMap = ServicesMap; this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.isEdit = this.isEdit; this.widgetContext.isEdit = this.isEdit;
this.widgetContext.isMobile = this.isMobile; this.widgetContext.isMobile = this.isMobile;

45
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -52,6 +52,14 @@ import { CustomDialogService } from '@home/components/widget/dialog/custom-dialo
import { isDefined, formatValue } from '@core/utils'; import { isDefined, formatValue } from '@core/utils';
import { forkJoin, Observable, of, ReplaySubject } from 'rxjs'; import { forkJoin, Observable, of, ReplaySubject } from 'rxjs';
import { WidgetSubscription } from '@core/api/widget-subscription'; import { WidgetSubscription } from '@core/api/widget-subscription';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
NotificationHorizontalPosition,
NotificationType,
NotificationVerticalPosition
} from '@core/notification/notification.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -152,8 +160,8 @@ export class WidgetContext {
formatValue formatValue
}; };
$container: JQuery<any>; $container: JQuery<HTMLElement>;
$containerParent: JQuery<any>; $containerParent: JQuery<HTMLElement>;
width: number; width: number;
height: number; height: number;
$scope: IDynamicWidgetComponent; $scope: IDynamicWidgetComponent;
@ -184,11 +192,44 @@ export class WidgetContext {
ngZone?: NgZone; ngZone?: NgZone;
store?: Store<AppState>;
rxjs = { rxjs = {
forkJoin, forkJoin,
of of
}; };
showSuccessToast(message: string, duration: number = 1000,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
this.showToast('success', message, duration, verticalPosition, horizontalPosition, target);
}
showErrorToast(message: string,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target);
}
showToast(type: NotificationType, message: string, duration: number,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
this.store.dispatch(new ActionNotificationShow(
{
message,
type,
duration,
verticalPosition,
horizontalPosition,
target,
panelClass: this.widgetNamespace,
forceDismiss: true
}));
}
detectChanges(updateWidgetParams: boolean = false) { detectChanges(updateWidgetParams: boolean = false) {
if (!this.destroyed) { if (!this.destroyed) {
if (updateWidgetParams) { if (updateWidgetParams) {

57
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="jsonFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ (this.data.title ? this.data.title : 'details.edit-json') | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<tb-json-object-edit
formControlName="json"
label="{{ 'value.json-value' | translate }}"
validateContent="true"
[required]="true"
[fillHeight]="false">
</tb-json-object-edit>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || jsonFormGroup.invalid || !jsonFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

63
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts

@ -0,0 +1,63 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {Component, Inject, OnInit} from "@angular/core";
import {DialogComponent} from "@shared/components/dialog.component";
import {Store} from "@ngrx/store";
import {AppState} from "@core/core.state";
import {Router} from "@angular/router";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {FormBuilder, FormGroup} from "@angular/forms";
export interface JsonObjectEdittDialogData {
jsonValue: Object;
title?: string;
}
@Component({
selector: 'tb-object-edit-dialog',
templateUrl: './json-object-edit-dialog.component.html',
styleUrls: []
})
export class JsonObjectEditDialogComponent extends DialogComponent<JsonObjectEditDialogComponent, Object>
implements OnInit {
jsonFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: JsonObjectEdittDialogData,
public dialogRef: MatDialogRef<JsonObjectEditDialogComponent, Object>,
public fb: FormBuilder) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.jsonFormGroup = this.fb.group({
json: [this.data.jsonValue, []]
});
}
cancel(): void {
this.dialogRef.close(undefined);
}
add(): void {
this.dialogRef.close(this.jsonFormGroup.get('json').value);
}
}

96
ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts

@ -0,0 +1,96 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf} from "@angular/core";
import {
ControlValueAccessor,
FormControl, FormGroupDirective,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, NgForm,
ValidationErrors,
Validator
} from "@angular/forms";
import {ErrorStateMatcher} from "@angular/material/core";
@Directive({
selector: '[tb-json-to-string]',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TbJsonToStringDirective),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TbJsonToStringDirective),
multi: true,
},
{
provide: ErrorStateMatcher,
useExisting: TbJsonToStringDirective
}]
})
export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher {
private propagateChange = null;
private parseError: boolean;
private data: any;
@HostListener('input', ['$event.target.value']) input(newValue: any): void {
try {
this.data = JSON.parse(newValue);
this.parseError = false;
} catch (e) {
this.parseError = true;
}
this.propagateChange(this.data);
}
constructor(private render: Renderer2,
private element: ElementRef,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.parseError);
return originalErrorState || customErrorState;
}
validate(c: FormControl): ValidationErrors {
return (!this.parseError) ? null : {
invalidJSON: {
valid: false
}
};
}
writeValue(obj: any): void {
if (obj) {
this.data = obj;
this.parseError = false;
this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj));
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
}

6
ui-ngx/src/app/shared/components/json-content.component.html

@ -22,9 +22,13 @@
<label class="tb-title no-padding">{{ label }}</label> <label class="tb-title no-padding">{{ label }}</label>
<span fxFlex></span> <span fxFlex></span>
<button type="button" <button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()"> mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJSON()">
{{'js-func.tidy' | translate }} {{'js-func.tidy' | translate }}
</button> </button>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="minifyJSON()">
{{'js-func.mini' | translate }}
</button>
<button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen" <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen"
class="tb-mat-32" class="tb-mat-32"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"

8
ui-ngx/src/app/shared/components/json-content.component.ts

@ -257,12 +257,18 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
} }
} }
beautifyJson() { beautifyJSON() {
const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60}); const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60});
this.jsonEditor.setValue(res ? res : '', -1); this.jsonEditor.setValue(res ? res : '', -1);
this.updateView(); this.updateView();
} }
minifyJSON() {
const res = JSON.stringify(this.contentBody);
this.jsonEditor.setValue(res ? res : '', -1);
this.updateView();
}
onFullscreen() { onFullscreen() {
if (this.jsonEditor) { if (this.jsonEditor) {
setTimeout(() => { setTimeout(() => {

10
ui-ngx/src/app/shared/components/json-object-edit.component.html

@ -18,12 +18,20 @@
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}" <div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center"> <div fxLayout="row" fxLayoutAlign="start center" class="tb-json-object-toolbar">
<label class="tb-title no-padding" <label class="tb-title no-padding"
ng-class="{'tb-required': required, ng-class="{'tb-required': required,
'tb-readonly': readonly, 'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label> 'tb-error': !objectValid}">{{ label }}</label>
<span fxFlex></span> <span fxFlex></span>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJSON()">
{{'js-func.tidy' | translate }}
</button>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="minifyJSON()">
{{'js-func.mini' | translate }}
</button>
<button mat-button mat-icon-button (click)="fullscreen = !fullscreen" <button mat-button mat-icon-button (click)="fullscreen = !fullscreen"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"> matTooltipPosition="above">

19
ui-ngx/src/app/shared/components/json-object-edit.component.scss

@ -21,6 +21,25 @@
} }
} }
.tb-json-object-toolbar {
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
}
}
}
.tb-json-object-panel { .tb-json-object-panel {
height: 100%; height: 100%;
margin-left: 15px; margin-left: 15px;

16
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -195,6 +195,22 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
} }
} }
beautifyJSON() {
const res = JSON.stringify(this.modelValue, null, 2);
if (this.jsonEditor) {
this.jsonEditor.setValue(res ? res : '', -1);
}
this.updateView();
}
minifyJSON() {
const res = JSON.stringify(this.modelValue);
if (this.jsonEditor) {
this.jsonEditor.setValue(res ? res : '', -1);
}
this.updateView();
}
writeValue(value: any): void { writeValue(value: any): void {
this.modelValue = value; this.modelValue = value;
this.contentValue = ''; this.contentValue = '';

6
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -27,7 +27,7 @@ import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-
import { import {
Aggregation, Aggregation,
aggregationTranslations, aggregationTranslations,
AggregationType, AggregationType, DAY,
HistoryWindow, HistoryWindow,
HistoryWindowType, HistoryWindowType,
IntervalWindow, IntervalWindow,
@ -205,9 +205,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
const timewindowFormValue = this.timewindowForm.getRawValue(); const timewindowFormValue = this.timewindowForm.getRawValue();
if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
return timewindowFormValue.history.timewindowMs; return timewindowFormValue.history.timewindowMs;
} else { } else if (timewindowFormValue.history.fixedTimewindow) {
return timewindowFormValue.history.fixedTimewindow.endTimeMs - return timewindowFormValue.history.fixedTimewindow.endTimeMs -
timewindowFormValue.history.fixedTimewindow.startTimeMs; timewindowFormValue.history.fixedTimewindow.startTimeMs;
} else {
return DAY;
} }
} }

16
ui-ngx/src/app/shared/components/value-input.component.html

@ -59,5 +59,21 @@
{{ (modelValue ? 'value.true' : 'value.false') | translate }} {{ (modelValue ? 'value.true' : 'value.false') | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
<div fxLayout="row" fxLayoutAlign="center" fxFlex="60" *ngIf="valueType === valueTypeEnum.JSON" class="mat-block">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>value.json-value</mat-label>
<input [disabled]="disabled" matInput tb-json-to-string required name="value" #value="ngModel"
[(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
<button matSuffix mat-button mat-icon-button (click)="openEditJSONDialog($event)">
<mat-icon>open_in_new</mat-icon>
</button>
<mat-error *ngIf="value.hasError('required')">
{{ (requiredText ? requiredText : 'value.json-value-required') | translate }}
</mat-error>
<mat-error *ngIf="value.hasError('invalidJSON')">
{{ 'value.json-value-invalid' | translate }}
</mat-error>
</mat-form-field>
</div>
</section> </section>
</form> </form>

34
ui-ngx/src/app/shared/components/value-input.component.ts

@ -17,6 +17,12 @@
import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms';
import { ValueType, valueTypesMap } from '@shared/models/constants'; import { ValueType, valueTypesMap } from '@shared/models/constants';
import { isObject } from "@core/utils";
import { MatDialog } from "@angular/material/dialog";
import {
JsonObjectEditDialogComponent,
JsonObjectEdittDialogData
} from "@shared/components/dialog/json-object-edit-dialog.component";
@Component({ @Component({
selector: 'tb-value-input', selector: 'tb-value-input',
@ -50,13 +56,35 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
private propagateChange = null; private propagateChange = null;
constructor() { constructor(
public dialog: MatDialog,
) {
} }
ngOnInit(): void { ngOnInit(): void {
} }
openEditJSONDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEdittDialogData, Object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: this.modelValue
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.modelValue = res;
this.inputForm.control.patchValue({'value': this.modelValue});
}
}
);
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -78,6 +106,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
} else { } else {
this.valueType = ValueType.DOUBLE; this.valueType = ValueType.DOUBLE;
} }
} else if (isObject(this.modelValue)) {
this.valueType = ValueType.JSON;
} else { } else {
this.valueType = ValueType.STRING; this.valueType = ValueType.STRING;
} }
@ -94,6 +124,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
onValueTypeChanged() { onValueTypeChanged() {
if (this.valueType === ValueType.BOOLEAN) { if (this.valueType === ValueType.BOOLEAN) {
this.modelValue = false; this.modelValue = false;
} if (this.valueType === ValueType.JSON) {
this.modelValue = {};
} else { } else {
this.modelValue = null; this.modelValue = null;
} }

10
ui-ngx/src/app/shared/models/constants.ts

@ -121,7 +121,8 @@ export enum ValueType {
STRING = 'STRING', STRING = 'STRING',
INTEGER = 'INTEGER', INTEGER = 'INTEGER',
DOUBLE = 'DOUBLE', DOUBLE = 'DOUBLE',
BOOLEAN = 'BOOLEAN' BOOLEAN = 'BOOLEAN',
JSON = 'JSON'
} }
export const valueTypesMap = new Map<ValueType, ValueTypeData>( export const valueTypesMap = new Map<ValueType, ValueTypeData>(
@ -153,6 +154,13 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>(
name: 'value.boolean', name: 'value.boolean',
icon: 'mdi:checkbox-marked-outline' icon: 'mdi:checkbox-marked-outline'
} }
],
[
ValueType.JSON,
{
name: 'value.json',
icon: 'mdi:json'
}
] ]
] ]
); );

30
ui-ngx/src/app/shared/pipe/tbJson.pipe.ts

@ -0,0 +1,30 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {Pipe, PipeTransform} from '@angular/core';
import {isObject, isNumber} from "@core/utils";
@Pipe({name: 'tbJson'})
export class TbJsonPipe implements PipeTransform {
transform(value: any): string {
if (isObject(value)) {
return JSON.stringify(value);
} else if (isNumber(value)) {
return value.toString();
}
return value;
}
}

13
ui-ngx/src/app/shared/shared.module.ts

@ -104,6 +104,7 @@ import { TbErrorComponent } from '@shared/components/tb-error.component';
import { EntityTypeListComponent } from '@shared/components/entity/entity-type-list.component'; import { EntityTypeListComponent } from '@shared/components/entity/entity-type-list.component';
import { EntitySubTypeListComponent } from '@shared/components/entity/entity-subtype-list.component'; import { EntitySubTypeListComponent } from '@shared/components/entity/entity-subtype-list.component';
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { TbJsonPipe } from "@shared/pipe/tbJson.pipe";
import { ColorPickerDialogComponent } from '@shared/components/dialog/color-picker-dialog.component'; import { ColorPickerDialogComponent } from '@shared/components/dialog/color-picker-dialog.component';
import { MatChipDraggableDirective } from '@shared/components/mat-chip-draggable.directive'; import { MatChipDraggableDirective } from '@shared/components/mat-chip-draggable.directive';
import { ColorInputComponent } from '@shared/components/color-input.component'; import { ColorInputComponent } from '@shared/components/color-input.component';
@ -124,6 +125,8 @@ import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
import { TbHotkeysDirective } from '@shared/components/hotkeys.directive'; import { TbHotkeysDirective } from '@shared/components/hotkeys.directive';
import { NavTreeComponent } from '@shared/components/nav-tree.component'; import { NavTreeComponent } from '@shared/components/nav-tree.component';
import { LedLightComponent } from '@shared/components/led-light.component'; import { LedLightComponent } from '@shared/components/led-light.component';
import { TbJsonToStringDirective } from "@shared/components/directives/tb-json-to-string.directive";
import { JsonObjectEditDialogComponent } from "@shared/components/dialog/json-object-edit-dialog.component";
@NgModule({ @NgModule({
providers: [ providers: [
@ -132,6 +135,7 @@ import { LedLightComponent } from '@shared/components/led-light.component';
EnumToArrayPipe, EnumToArrayPipe,
HighlightPipe, HighlightPipe,
TruncatePipe, TruncatePipe,
TbJsonPipe,
{ {
provide: FlowInjectionToken, provide: FlowInjectionToken,
useValue: Flow useValue: Flow
@ -202,7 +206,10 @@ import { LedLightComponent } from '@shared/components/led-light.component';
EnumToArrayPipe, EnumToArrayPipe,
HighlightPipe, HighlightPipe,
TruncatePipe, TruncatePipe,
KeyboardShortcutPipe TbJsonPipe,
KeyboardShortcutPipe,
TbJsonToStringDirective,
JsonObjectEditDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -357,8 +364,10 @@ import { LedLightComponent } from '@shared/components/led-light.component';
EnumToArrayPipe, EnumToArrayPipe,
HighlightPipe, HighlightPipe,
TruncatePipe, TruncatePipe,
TbJsonPipe,
KeyboardShortcutPipe, KeyboardShortcutPipe,
TranslateModule TranslateModule,
JsonObjectEditDialogComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

10
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -626,6 +626,7 @@
"details": { "details": {
"details": "Details", "details": "Details",
"edit-mode": "Edit mode", "edit-mode": "Edit mode",
"edit-json": "Edit JSON",
"toggle-edit-mode": "Toggle edit mode" "toggle-edit-mode": "Toggle edit mode"
}, },
"device": { "device": {
@ -1298,7 +1299,8 @@
"js-func": { "js-func": {
"no-return-error": "Function must return value!", "no-return-error": "Function must return value!",
"return-type-mismatch": "Function must return value of '{{type}}' type!", "return-type-mismatch": "Function must return value of '{{type}}' type!",
"tidy": "Tidy" "tidy": "Tidy",
"mini": "Mini"
}, },
"key-val": { "key-val": {
"key": "Key", "key": "Key",
@ -1635,7 +1637,11 @@
"boolean-value": "Boolean value", "boolean-value": "Boolean value",
"false": "False", "false": "False",
"true": "True", "true": "True",
"long": "Long" "long": "Long",
"json": "JSON",
"json-value": "JSON value",
"json-value-invalid": "JSON value has an invalid format",
"json-value-required": "JSON value is required."
}, },
"widget": { "widget": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",

4
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

@ -607,6 +607,7 @@
}, },
"details": { "details": {
"edit-mode": "Режим редактирования", "edit-mode": "Режим редактирования",
"edit-json": "Редактировать JSON",
"toggle-edit-mode": "Режим редактирования" "toggle-edit-mode": "Режим редактирования"
}, },
"device": { "device": {
@ -1191,8 +1192,7 @@
}, },
"js-func": { "js-func": {
"no-return-error": "Функция должна возвращать значение!", "no-return-error": "Функция должна возвращать значение!",
"return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!", "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!"
"tidy": "Tidy"
}, },
"key-val": { "key-val": {
"key": "Ключ", "key": "Ключ",

4
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -724,6 +724,7 @@
"details": { "details": {
"details": "Деталі", "details": "Деталі",
"edit-mode": "Режим редагування", "edit-mode": "Режим редагування",
"edit-json": "Редагувати JSON",
"toggle-edit-mode": "Перемкнути режим редагування" "toggle-edit-mode": "Перемкнути режим редагування"
}, },
"device": { "device": {
@ -1606,8 +1607,7 @@
}, },
"js-func": { "js-func": {
"no-return-error": "Функція повинна повертати значення!", "no-return-error": "Функція повинна повертати значення!",
"return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!", "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!"
"tidy": "Tidy"
}, },
"key-val": { "key-val": {
"key": "Ключ", "key": "Ключ",

4
ui/src/app/api/widget.service.js

@ -29,6 +29,8 @@ import thingsboardWebCameraInputWidget from '../widget/lib/web-camera-input-widg
import thingsboardRpcWidgets from '../widget/lib/rpc'; import thingsboardRpcWidgets from '../widget/lib/rpc';
import thingsboardJsonToString from '../components/tb-json-to-string.directive';
import TbFlot from '../widget/lib/flot-widget'; import TbFlot from '../widget/lib/flot-widget';
import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge'; import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge';
@ -52,7 +54,7 @@ export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsbo
thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes,
thingsboardUtils, TripAnimationWidget]) thingsboardUtils, thingsboardJsonToString, TripAnimationWidget])
.factory('widgetService', WidgetService) .factory('widgetService', WidgetService)
.name; .name;

5
ui/src/app/common/types.constant.js

@ -881,6 +881,11 @@ export default angular.module('thingsboard.types', [])
value: "boolean", value: "boolean",
name: "value.boolean", name: "value.boolean",
icon: "mdi:checkbox-marked-outline" icon: "mdi:checkbox-marked-outline"
},
json: {
value: "json",
name: "value.json",
icon: "mdi:json"
} }
}, },
widgetType: { widgetType: {

32
ui/src/app/components/json-content.directive.js

@ -57,9 +57,12 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
updateEditorSize(); updateEditorSize();
}; };
scope.beautifyJson = function () { scope.beautifyJSON = function () {
var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60}); scope.contentBody = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
scope.contentBody = res; };
scope.minifyJSON = function () {
scope.contentBody = angular.toJson(angular.fromJson(scope.contentBody));
}; };
function updateEditorSize() { function updateEditorSize() {
@ -116,7 +119,7 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
scope.$watch('contentBody', function (newContent, oldContent) { scope.$watch('contentBody', function (newContent, oldContent) {
ngModelCtrl.$setViewValue(scope.contentBody); ngModelCtrl.$setViewValue(scope.contentBody);
if (!angular.equals(newContent, oldContent)) { if (!angular.equals(newContent, oldContent)) {
scope.contentValid = true; scope.contentValid = scope.validate();
} }
scope.updateValidity(); scope.updateValidity();
}); });
@ -139,15 +142,17 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
} }
return true; return true;
} catch (e) { } catch (e) {
var details = utils.parseException(e); if (!scope.hideErrorToast) {
var errorInfo = 'Error:'; var details = utils.parseException(e);
if (details.name) { var errorInfo = 'Error:';
errorInfo += ' ' + details.name + ':'; if (details.name) {
} errorInfo += ' ' + details.name + ':';
if (details.message) { }
errorInfo += ' ' + details.message; if (details.message) {
errorInfo += ' ' + details.message;
}
scope.showError(errorInfo);
} }
scope.showError(errorInfo);
return false; return false;
} }
}; };
@ -169,7 +174,7 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
}); });
$compile(element.contents())(scope); $compile(element.contents())(scope);
} };
return { return {
restrict: "E", restrict: "E",
@ -177,6 +182,7 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
scope: { scope: {
contentType: '=', contentType: '=',
validateContent: '=?', validateContent: '=?',
hideErrorToast: '=?',
readonly:'=ngReadonly', readonly:'=ngReadonly',
fillHeight:'=?' fillHeight:'=?'
}, },

2
ui/src/app/components/json-content.scss

@ -27,7 +27,7 @@ tb-json-content {
min-height: 15px; min-height: 15px;
padding: 4px; padding: 4px;
margin: 0 5px 0 0; margin: 0 5px 0 0;
font-size: .8rem; font-size: 12px;
line-height: 15px; line-height: 15px;
color: #7b7b7b; color: #7b7b7b;
background: rgba(220, 220, 220, .35); background: rgba(220, 220, 220, .35);

8
ui/src/app/components/json-content.tpl.html

@ -17,11 +17,15 @@
--> -->
<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column"> <div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
<div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar"> <div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar">
<label class="tb-title no-padding">{{ label }}</label> <label class="tb-title no-padding"
ng-class="{'tb-error': !contentValid}">{{ label }}</label>
<span flex></span> <span flex></span>
<md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJson()">{{ <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJSON()">{{
'js-func.tidy' | translate }} 'js-func.tidy' | translate }}
</md-button> </md-button>
<md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.mini' | translate }}" ng-click="minifyJSON()">{{
'js-func.mini' | translate }}
</md-button>
<md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button> <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
</div> </div>
<div flex id="tb-json-panel" class="tb-json-content-panel" layout="column"> <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">

10
ui/src/app/components/json-object-edit.directive.js

@ -50,6 +50,14 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
updateEditorSize(); updateEditorSize();
}; };
scope.beautifyJSON = function () {
scope.contentBody = angular.toJson(scope.object, 4);
};
scope.minifyJSON = function () {
scope.contentBody = angular.toJson(scope.object);
};
function updateEditorSize() { function updateEditorSize() {
if (scope.json_editor) { if (scope.json_editor) {
scope.json_editor.resize(); scope.json_editor.resize();
@ -169,7 +177,7 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
}); });
$compile(element.contents())(scope); $compile(element.contents())(scope);
} };
return { return {
restrict: "E", restrict: "E",

13
ui/src/app/components/json-object-edit.scss

@ -21,6 +21,19 @@ tb-json-object-edit {
} }
} }
.tb-json-object-edit-toolbar {
.md-button.tidy {
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0 5px 0 0;
font-size: 12px;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
}
}
.tb-json-object-panel { .tb-json-object-panel {
height: 100%; height: 100%;
margin-left: 15px; margin-left: 15px;

8
ui/src/app/components/json-object-edit.tpl.html

@ -16,12 +16,18 @@
--> -->
<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column"> <div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
<div layout="row" layout-align="start center"> <div layout="row" layout-align="start center" class="tb-json-object-edit-toolbar">
<label class="tb-title no-padding" <label class="tb-title no-padding"
ng-class="{'tb-required': required, ng-class="{'tb-required': required,
'tb-readonly': readonly, 'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label> 'tb-error': !objectValid}">{{ label }}</label>
<span flex></span> <span flex></span>
<md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJSON()">
{{'js-func.tidy' | translate }}
</md-button>
<md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.mini' | translate }}" ng-click="minifyJSON()">
{{'js-func.mini' | translate }}
</md-button>
<md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button> <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
</div> </div>
<div flex id="tb-json-panel" class="tb-json-object-panel" layout="column"> <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">

45
ui/src/app/components/tb-json-to-string.directive.js

@ -0,0 +1,45 @@
/*
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default angular.module('tbJsonToString', [])
.directive('tbJsonToString', InputJson)
.name;
function InputJson() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ngModelCtrl) {
function into(input) {
try {
ngModelCtrl.$setValidity('invalidJSON', true);
return angular.fromJson(input);
} catch (e) {
ngModelCtrl.$setValidity('invalidJSON', false);
}
}
function out(data) {
try {
ngModelCtrl.$setValidity('invalidJSON', true);
return angular.toJson(data);
} catch (e) {
ngModelCtrl.$setValidity('invalidJSON', false);
}
}
ngModelCtrl.$parsers.push(into);
ngModelCtrl.$formatters.push(out);
}
};
}

38
ui/src/app/entity/attribute/add-attribute-dialog.controller.js

@ -13,10 +13,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/* eslint-disable import/no-unresolved, import/default */
import attributeDialogEditJsonTemplate from './attribute-dialog-edit-json.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import AttributeDialogEditJsonController from './attribute-dialog-edit-json.controller';
/*@ngInject*/ /*@ngInject*/
export default function AddAttributeDialogController($scope, $mdDialog, types, attributeService, entityType, entityId, attributeScope) { export default function AddAttributeDialogController($scope, $mdDialog, types, attributeService, entityType, entityId, attributeScope) {
var vm = this; let vm = this;
vm.attribute = {}; vm.attribute = {};
@ -40,11 +48,37 @@ export default function AddAttributeDialogController($scope, $mdDialog, types, a
); );
} }
$scope.$watch('vm.valueType', function() { $scope.$watch('vm.valueType', function () {
if (vm.valueType === types.valueType.boolean) { if (vm.valueType === types.valueType.boolean) {
vm.attribute.value = false; vm.attribute.value = false;
} else if (vm.valueType === types.valueType.json) {
vm.attribute.value = {};
} else { } else {
vm.attribute.value = null; vm.attribute.value = null;
} }
}); });
vm.addJSON = ($event) => {
showJsonDialog($event, vm.attribute.value, false).then((response) => {
vm.attribute.value = response;
})
};
function showJsonDialog($event, jsonValue, readOnly) {
if ($event) {
$event.stopPropagation();
}
return $mdDialog.show({
controller: AttributeDialogEditJsonController,
controllerAs: 'vm',
templateUrl: attributeDialogEditJsonTemplate,
locals: {
jsonValue: jsonValue,
readOnly: readOnly
},
targetEvent: $event,
fullscreen: true,
multiple: true,
});
}
} }

34
ui/src/app/entity/attribute/add-attribute-dialog.tpl.html

@ -26,7 +26,8 @@
</md-button> </md-button>
</div> </div>
</md-toolbar> </md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear> <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span> <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content> <md-dialog-content>
<div class="md-dialog-content"> <div class="md-dialog-content">
@ -58,7 +59,8 @@
</md-input-container> </md-input-container>
<md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block"> <md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block">
<label translate>value.integer-value</label> <label translate>value.integer-value</label>
<input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attribute.value"> <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/"
ng-model="vm.attribute.value">
<div ng-messages="theForm.value.$error"> <div ng-messages="theForm.value.$error">
<div translate ng-message="required">attribute.value-required</div> <div translate ng-message="required">attribute.value-required</div>
<div translate ng-message="pattern">value.invalid-integer-value</div> <div translate ng-message="pattern">value.invalid-integer-value</div>
@ -71,11 +73,31 @@
<div translate ng-message="required">attribute.value-required</div> <div translate ng-message="required">attribute.value-required</div>
</div> </div>
</md-input-container> </md-input-container>
<div layout="column" layout-align="center" flex="60" ng-if="vm.valueType===vm.valueTypes.boolean"> <div layout="column" layout-align="center" flex="60"
<md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0px;"> ng-if="vm.valueType===vm.valueTypes.boolean">
<md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0;">
{{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }} {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }}
</md-checkbox> </md-checkbox>
</div> </div>
<div layout="row" layout-align="center" flex="60"
ng-if="vm.valueType===vm.valueTypes.json" class="md-block">
<md-input-container flex class="md-block">
<label translate>value.json-value</label>
<input required tb-json-to-string name="value" ng-model="vm.attribute.value">
<div ng-messages="theForm.value.$error">
<div translate ng-message="required">attribute.value-required</div>
</div>
</md-input-container>
<md-button class="md-icon-button"
ng-click="vm.addJSON($event)"
style="margin: 18px 0;"
aria-label="{{ 'action.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.edit' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="open_in_new"></ng-md-icon>
</md-button>
</div>
</section> </section>
</fieldset> </fieldset>
</md-content> </md-content>
@ -87,8 +109,8 @@
class="md-raised md-primary"> class="md-raised md-primary">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</md-button> </md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">
translate }} {{ 'action.cancel' | translate }}
</md-button> </md-button>
</md-dialog-actions> </md-dialog-actions>
</form> </form>

1
ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js

@ -151,5 +151,4 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
} }
); );
} }
} }

20
ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html

@ -26,7 +26,8 @@
</md-button> </md-button>
</div> </div>
</md-toolbar> </md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear> <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span> <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content> <md-dialog-content>
<div class="md-dialog-content"> <div class="md-dialog-content">
@ -37,10 +38,10 @@
<section flex layout="column" style="width: 300px;"> <section flex layout="column" style="width: 300px;">
<span translate style="padding-bottom: 10px;">dashboard.select-existing</span> <span translate style="padding-bottom: 10px;">dashboard.select-existing</span>
<tb-dashboard-autocomplete the-form="theForm" <tb-dashboard-autocomplete the-form="theForm"
ng-disabled="$root.loading || vm.addToDashboardType != 0" ng-disabled="$root.loading || vm.addToDashboardType != 0"
tb-required="vm.addToDashboardType === 0" tb-required="vm.addToDashboardType === 0"
ng-model="vm.dashboardId" ng-model="vm.dashboardId"
select-first-dashboard="false"> select-first-dashboard="false">
</tb-dashboard-autocomplete> </tb-dashboard-autocomplete>
</section> </section>
</md-radio-button> </md-radio-button>
@ -49,7 +50,8 @@
<span translate>dashboard.create-new</span> <span translate>dashboard.create-new</span>
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>dashboard.new-dashboard-title</label> <label translate>dashboard.new-dashboard-title</label>
<input ng-required="vm.addToDashboardType === 1" name="title" ng-model="vm.newDashboard.title"> <input ng-required="vm.addToDashboardType === 1" name="title"
ng-model="vm.newDashboard.title">
<div ng-messages="theForm.title.$error"> <div ng-messages="theForm.title.$error">
<div translate ng-message="required">dashboard.title-required</div> <div translate ng-message="required">dashboard.title-required</div>
</div> </div>
@ -66,15 +68,15 @@
<md-checkbox <md-checkbox
ng-model="vm.openDashboard" ng-model="vm.openDashboard"
aria-label="{{ 'dashboard.open-dashboard' | translate }}" aria-label="{{ 'dashboard.open-dashboard' | translate }}"
style="margin-bottom: 0px; padding-right: 20px;"> style="margin-bottom: 0; padding-right: 20px;">
{{ 'dashboard.open-dashboard' | translate }} {{ 'dashboard.open-dashboard' | translate }}
</md-checkbox> </md-checkbox>
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit" <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary"> class="md-raised md-primary">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</md-button> </md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">
translate }} {{ 'action.cancel' | translate }}
</md-button> </md-button>
</md-dialog-actions> </md-dialog-actions>
</form> </form>

35
ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js

@ -0,0 +1,35 @@
/*
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-enable import/no-unresolved, import/default */
import './attribute-dialog-edit-json.scss';
/*@ngInject*/
export default function AttributeDialogEditJsonController($mdDialog, types, jsonValue, readOnly) {
let vm = this;
vm.json = angular.toJson(jsonValue, 4);
vm.readOnly = readOnly;
vm.contentType = types.contentType.JSON.value;
vm.save = () => {
$mdDialog.hide(angular.fromJson(vm.json));
};
vm.cancel = () => {
$mdDialog.cancel();
};
}

24
ui/src/app/entity/attribute/attribute-dialog-edit-json.scss

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.attribute-edit-json-dialog{
min-width: 400px;
}
@media (max-width: 425px){
.attribute-edit-json-dialog {
min-width: 200px;
}
}

54
ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog class="attribute-edit-json-dialog">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2>{{ 'details.edit-json' | translate }}</h2>
<span flex></span>
<md-button class="md-icon-button"
ng-click="vm.cancel()"
aria-label="{{'action.close'|translate}}">
<ng-md-icon icon="close" aria-label="Close"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content>
<div class="md-dialog-content">
<tb-json-content
ng-model="vm.json"
label="{{ 'value.json-value' | translate }}"
content-type="vm.contentType"
validate-content="true"
hide-error-toast="true"
fill-height="false"
ng-readonly="vm.readOnly">
</tb-json-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row" layout-align="end center" class="action-buttons">
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{'action.save'|translate}}
</md-button>
<md-button id="cancel-btn" ng-disabled="$root.loading" ng-click="vm.cancel()">
{{'action.cancel'|translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

143
ui/src/app/entity/attribute/attribute-table.directive.js

@ -39,7 +39,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
element.html(template); element.html(template);
var getAttributeScopeByValue = function(attributeScopeValue) { var getAttributeScopeByValue = function (attributeScopeValue) {
if (scope.types.latestTelemetry.value === attributeScopeValue) { if (scope.types.latestTelemetry.value === attributeScopeValue) {
return scope.types.latestTelemetry; return scope.types.latestTelemetry;
} }
@ -48,7 +48,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
return scope.attributeScopes[attrScope]; return scope.attributeScopes[attrScope];
} }
} }
} };
scope.types = types; scope.types = types;
@ -87,14 +87,14 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
search: null search: null
}; };
scope.$watch("entityId", function(newVal) { scope.$watch("entityId", function (newVal) {
if (newVal) { if (newVal) {
scope.resetFilter(); scope.resetFilter();
scope.getEntityAttributes(false, true); scope.getEntityAttributes(false, true);
} }
}); });
scope.$watch("attributeScope", function(newVal, prevVal) { scope.$watch("attributeScope", function (newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) { if (newVal && !angular.equals(newVal, prevVal)) {
scope.mode = 'default'; scope.mode = 'default';
scope.query.search = null; scope.query.search = null;
@ -103,30 +103,30 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
} }
}); });
scope.resetFilter = function() { scope.resetFilter = function () {
scope.mode = 'default'; scope.mode = 'default';
scope.query.search = null; scope.query.search = null;
scope.selectedAttributes = []; scope.selectedAttributes = [];
scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope); scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
} };
scope.enterFilterMode = function(event) { scope.enterFilterMode = function (event) {
let $button = angular.element(event.currentTarget); let $button = angular.element(event.currentTarget);
let $toolbarsContainer = $button.closest('.toolbarsContainer'); let $toolbarsContainer = $button.closest('.toolbarsContainer');
scope.query.search = ''; scope.query.search = '';
$timeout(()=>{ $timeout(() => {
$toolbarsContainer.find('.searchInput').focus(); $toolbarsContainer.find('.searchInput').focus();
}) })
} };
scope.exitFilterMode = function() { scope.exitFilterMode = function () {
scope.query.search = null; scope.query.search = null;
scope.getEntityAttributes(); scope.getEntityAttributes();
} };
scope.$watch("query.search", function(newVal, prevVal) { scope.$watch("query.search", function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && scope.query.search != null) { if (!angular.equals(newVal, prevVal) && scope.query.search != null) {
scope.getEntityAttributes(); scope.getEntityAttributes();
} }
@ -142,15 +142,15 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
} }
} }
scope.onReorder = function() { scope.onReorder = function () {
scope.getEntityAttributes(false, false); scope.getEntityAttributes(false, false);
} };
scope.onPaginate = function() { scope.onPaginate = function () {
scope.getEntityAttributes(false, false); scope.getEntityAttributes(false, false);
} };
scope.getEntityAttributes = function(forceUpdate, reset) { scope.getEntityAttributes = function (forceUpdate, reset) {
if (scope.attributesDeferred) { if (scope.attributesDeferred) {
scope.attributesDeferred.resolve(); scope.attributesDeferred.resolve();
} }
@ -163,7 +163,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
} }
scope.checkSubscription(); scope.checkSubscription();
scope.attributesDeferred = attributeService.getEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.attributesDeferred = attributeService.getEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value,
scope.query, function(attributes, update, apply) { scope.query, function (attributes, update, apply) {
success(attributes, update || forceUpdate, apply); success(attributes, update || forceUpdate, apply);
} }
); );
@ -176,9 +176,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
}); });
deferred.resolve(); deferred.resolve();
} }
} };
scope.checkSubscription = function() { scope.checkSubscription = function () {
var newSubscriptionId = null; var newSubscriptionId = null;
if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') { if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') {
newSubscriptionId = attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value); newSubscriptionId = attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value);
@ -187,36 +187,38 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
} }
scope.subscriptionId = newSubscriptionId; scope.subscriptionId = newSubscriptionId;
} };
scope.$on('$destroy', function() { scope.$on('$destroy', function () {
if (scope.subscriptionId) { if (scope.subscriptionId) {
attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
} }
}); });
scope.editAttribute = function($event, attribute) { scope.editAttribute = function ($event, attribute) {
if (!scope.attributeScope.clientSide) { if (!scope.attributeScope.clientSide) {
$event.stopPropagation(); $event.stopPropagation();
$mdEditDialog.show({ $mdEditDialog.show({
controller: EditAttributeValueController, controller: EditAttributeValueController,
templateUrl: editAttributeValueTemplate, templateUrl: editAttributeValueTemplate,
locals: {attributeValue: attribute.value, locals: {
save: function (model) { attributeValue: attribute.value,
var updatedAttribute = angular.copy(attribute); save: function (model) {
updatedAttribute.value = model.value; var updatedAttribute = angular.copy(attribute);
attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then( updatedAttribute.value = model.value;
function success() { attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then(
scope.getEntityAttributes(); function success() {
} scope.getEntityAttributes();
); }
}}, );
}
},
targetEvent: $event targetEvent: $event
}); });
} }
} };
scope.addAttribute = function($event) { scope.addAttribute = function ($event) {
if (!scope.attributeScope.clientSide) { if (!scope.attributeScope.clientSide) {
$event.stopPropagation(); $event.stopPropagation();
$mdDialog.show({ $mdDialog.show({
@ -224,16 +226,20 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: addAttributeDialogTemplate, templateUrl: addAttributeDialogTemplate,
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
locals: {entityType: scope.entityType, entityId: scope.entityId, attributeScope: scope.attributeScope.value}, locals: {
entityType: scope.entityType,
entityId: scope.entityId,
attributeScope: scope.attributeScope.value
},
fullscreen: true, fullscreen: true,
targetEvent: $event targetEvent: $event
}).then(function () { }).then(function () {
scope.getEntityAttributes(); scope.getEntityAttributes();
}); });
} }
} };
scope.deleteAttributes = function($event) { scope.deleteAttributes = function ($event) {
if (!scope.attributeScope.clientSide) { if (!scope.attributeScope.clientSide) {
$event.stopPropagation(); $event.stopPropagation();
var confirm = $mdDialog.confirm() var confirm = $mdDialog.confirm()
@ -244,33 +250,33 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
.cancel($translate.instant('action.no')) .cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes')); .ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () { $mdDialog.show(confirm).then(function () {
attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then( attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then(
function success() { function success() {
scope.selectedAttributes = []; scope.selectedAttributes = [];
scope.getEntityAttributes(); scope.getEntityAttributes();
} }
) )
}); });
} }
} };
scope.nextWidget = function() { scope.nextWidget = function () {
$mdUtil.nextTick(function () { $mdUtil.nextTick(function () {
if (scope.widgetsCarousel.index < scope.widgetsList.length - 1) { if (scope.widgetsCarousel.index < scope.widgetsList.length - 1) {
scope.widgetsCarousel.index++; scope.widgetsCarousel.index++;
} }
}); });
} };
scope.prevWidget = function() { scope.prevWidget = function () {
$mdUtil.nextTick(function () { $mdUtil.nextTick(function () {
if (scope.widgetsCarousel.index > 0) { if (scope.widgetsCarousel.index > 0) {
scope.widgetsCarousel.index--; scope.widgetsCarousel.index--;
} }
}); });
} };
scope.enterWidgetMode = function() { scope.enterWidgetMode = function () {
if (scope.widgetsIndexWatch) { if (scope.widgetsIndexWatch) {
scope.widgetsIndexWatch(); scope.widgetsIndexWatch();
@ -303,7 +309,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
entitiAliases[entityAlias.id] = entityAlias; entitiAliases[entityAlias.id] = entityAlias;
var stateController = { var stateController = {
getStateParams: function() { getStateParams: function () {
return {}; return {};
} }
}; };
@ -317,9 +323,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
type: types.datasourceType.entity, type: types.datasourceType.entity,
entityAliasId: entityAlias.id, entityAliasId: entityAlias.id,
dataKeys: [] dataKeys: []
} };
var i = 0; var i = 0;
for (var attr =0; attr < scope.selectedAttributes.length;attr++) { for (var attr = 0; attr < scope.selectedAttributes.length; attr++) {
var attribute = scope.selectedAttributes[attr]; var attribute = scope.selectedAttributes[attr];
var dataKey = { var dataKey = {
name: attribute.key, name: attribute.key,
@ -328,12 +334,12 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
color: utils.getMaterialColor(i), color: utils.getMaterialColor(i),
settings: {}, settings: {},
_hash: Math.random() _hash: Math.random()
} };
datasource.dataKeys.push(dataKey); datasource.dataKeys.push(dataKey);
i++; i++;
} }
scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function(newVal, prevVal) { scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function (newVal, prevVal) {
if (scope.mode === 'widget' && (newVal != prevVal)) { if (scope.mode === 'widget' && (newVal != prevVal)) {
var index = scope.widgetsCarousel.index; var index = scope.widgetsCarousel.index;
for (var i = 0; i < scope.widgetsList.length; i++) { for (var i = 0; i < scope.widgetsList.length; i++) {
@ -345,7 +351,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
} }
}); });
scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function(newVal, prevVal) { scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function (newVal, prevVal) {
if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) { if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) {
scope.widgetsList = []; scope.widgetsList = [];
scope.widgetsListCache = []; scope.widgetsListCache = [];
@ -358,7 +364,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then( widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
function success(widgetTypes) { function success(widgetTypes) {
widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']); widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type', '-createdTime']);
for (var i = 0; i < widgetTypes.length; i++) { for (var i = 0; i < widgetTypes.length; i++) {
var widgetType = widgetTypes[i]; var widgetType = widgetTypes[i];
@ -398,9 +404,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
} }
} }
}); });
} };
scope.exitWidgetMode = function() { scope.exitWidgetMode = function () {
if (scope.widgetsBundleWatch) { if (scope.widgetsBundleWatch) {
scope.widgetsBundleWatch(); scope.widgetsBundleWatch();
scope.widgetsBundleWatch = null; scope.widgetsBundleWatch = null;
@ -412,9 +418,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
scope.selectedWidgetsBundleAlias = null; scope.selectedWidgetsBundleAlias = null;
scope.mode = 'default'; scope.mode = 'default';
scope.getEntityAttributes(true); scope.getEntityAttributes(true);
} };
scope.addWidgetToDashboard = function($event) { scope.addWidgetToDashboard = function ($event) {
if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) { if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0]; var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
$event.stopPropagation(); $event.stopPropagation();
@ -423,21 +429,26 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: addWidgetToDashboardDialogTemplate, templateUrl: addWidgetToDashboardDialogTemplate,
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
locals: {entityId: scope.entityId, entityType: scope.entityType, entityName: scope.entityName, widget: angular.copy(widget)}, locals: {
entityId: scope.entityId,
entityType: scope.entityType,
entityName: scope.entityName,
widget: angular.copy(widget)
},
fullscreen: true, fullscreen: true,
targetEvent: $event targetEvent: $event
}).then(function () { }).then(function () {
}); });
} }
} };
scope.loading = function() { scope.loading = function () {
return $rootScope.loading; return $rootScope.loading;
} };
$compile(element.contents())(scope); $compile(element.contents())(scope);
} };
return { return {
restrict: "E", restrict: "E",

8
ui/src/app/entity/attribute/attribute-table.scss

@ -58,3 +58,11 @@ md-toolbar.md-table-toolbar.alternate {
} }
} }
} }
md-edit-dialog.tb-edit-dialog{
z-index: 78;
}
md-backdrop.md-edit-dialog-backdrop{
z-index: 77;
}

4
ui/src/app/entity/attribute/attribute-table.tpl.html

@ -77,9 +77,7 @@
</md-toolbar> </md-toolbar>
<md-toolbar class="md-table-toolbar alternate" ng-show="mode==='default' && selectedAttributes.length"> <md-toolbar class="md-table-toolbar alternate" ng-show="mode==='default' && selectedAttributes.length">
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
<span translate="{{attributeScope === types.latestTelemetry <span translate="{{(attributeScope === types.latestTelemetry) ? 'attribute.selected-telemetry' : 'attribute.selected-attributes'}}"
? 'attribute.selected-telemetry'
: 'attribute.selected-attributes'}}"
translate-values="{count: selectedAttributes.length}" translate-values="{count: selectedAttributes.length}"
translate-interpolation="messageformat"></span> translate-interpolation="messageformat"></span>
<span flex></span> <span flex></span>

58
ui/src/app/entity/attribute/edit-attribute-value.controller.js

@ -13,14 +13,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/* eslint-enable import/no-unresolved, import/default */
import AttributeDialogEditJsonController from "./attribute-dialog-edit-json.controller";
import attributeDialogEditJsonTemplate from "./attribute-dialog-edit-json.tpl.html";
/*@ngInject*/ /*@ngInject*/
export default function EditAttributeValueController($scope, $q, $element, types, attributeValue, save) { export default function EditAttributeValueController($scope, $mdDialog, $q, $element, $document, types, attributeValue, save) {
$scope.valueTypes = types.valueType; $scope.valueTypes = types.valueType;
$scope.model = {}; $scope.model = {
value: attributeValue
$scope.model.value = attributeValue; };
if ($scope.model.value === true || $scope.model.value === false) { if ($scope.model.value === true || $scope.model.value === false) {
$scope.valueType = types.valueType.boolean; $scope.valueType = types.valueType.boolean;
@ -30,26 +35,27 @@ export default function EditAttributeValueController($scope, $q, $element, types
} else { } else {
$scope.valueType = types.valueType.double; $scope.valueType = types.valueType.double;
} }
} else if (angular.isObject($scope.model.value)) {
$scope.valueType = types.valueType.json;
} else { } else {
$scope.valueType = types.valueType.string; $scope.valueType = types.valueType.string;
} }
$scope.submit = submit; $scope.submit = submit;
$scope.dismiss = dismiss; $scope.dismiss = dismiss;
$scope.editJSON = editJSON;
function dismiss() { function dismiss() {
$element.remove(); $element.remove();
} }
function update() { function update() {
if($scope.editDialog.$invalid) { if ($scope.editDialog.$invalid) {
return $q.reject(); return $q.reject();
} }
if (angular.isFunction(save)) {
if(angular.isFunction(save)) {
return $q.when(save($scope.model)); return $q.when(save($scope.model));
} }
return $q.resolve(); return $q.resolve();
} }
@ -59,13 +65,45 @@ export default function EditAttributeValueController($scope, $q, $element, types
}); });
} }
$scope.$watch('valueType', function(newVal, prevVal) {
if (newVal != prevVal) { $scope.$watch('valueType', function (newVal, prevVal) {
if (newVal !== prevVal) {
if ($scope.valueType === types.valueType.boolean) { if ($scope.valueType === types.valueType.boolean) {
$scope.model.value = false; $scope.model.value = false;
} else if ($scope.valueType === types.valueType.json) {
$scope.model.value = {};
} else { } else {
$scope.model.value = null; $scope.model.value = null;
} }
} }
}); });
function editJSON($event) {
$scope.hideDialog = true;
showJsonDialog($event, $scope.model.value, false).then((response) => {
$scope.hideDialog = false;
if (!angular.equals(response, $scope.model.value)) {
$scope.editDialog.$setDirty();
}
$scope.model.value = response;
})
}
function showJsonDialog($event, jsonValue, readOnly) {
if ($event) {
$event.stopPropagation();
}
return $mdDialog.show({
controller: AttributeDialogEditJsonController,
controllerAs: 'vm',
templateUrl: attributeDialogEditJsonTemplate,
locals: {
jsonValue: jsonValue,
readOnly: readOnly
},
targetEvent: $event,
fullscreen: true,
multiple: true
});
}
} }

30
ui/src/app/entity/attribute/edit-attribute-value.tpl.html

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<md-edit-dialog> <md-edit-dialog class="tb-edit-dialog">
<form name="editDialog" ng-submit="submit()"> <form name="editDialog" ng-submit="submit()" >
<div layout="column" class="md-content" style="width: 400px;"> <div layout="column" class="md-content" style="width: 400px;">
<fieldset> <fieldset>
<section layout="row"> <section layout="row">
@ -38,7 +38,8 @@
</md-input-container> </md-input-container>
<md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block"> <md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block">
<label translate>value.integer-value</label> <label translate>value.integer-value</label>
<input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="model.value"> <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/"
ng-model="model.value">
<div ng-messages="editDialog.value.$error"> <div ng-messages="editDialog.value.$error">
<div translate ng-message="required">attribute.value-required</div> <div translate ng-message="required">attribute.value-required</div>
<div translate ng-message="pattern">value.invalid-integer-value</div> <div translate ng-message="pattern">value.invalid-integer-value</div>
@ -52,16 +53,31 @@
</div> </div>
</md-input-container> </md-input-container>
<div layout="column" layout-align="center" flex="60" ng-if="valueType===valueTypes.boolean"> <div layout="column" layout-align="center" flex="60" ng-if="valueType===valueTypes.boolean">
<md-checkbox ng-model="model.value" style="margin-bottom: 0px;"> <md-checkbox ng-model="model.value" style="margin-bottom: 0;">
{{ (model.value ? 'value.true' : 'value.false') | translate }} {{ (model.value ? 'value.true' : 'value.false') | translate }}
</md-checkbox> </md-checkbox>
</div> </div>
<div layout="row" layout-align="center" ng-if="valueType===valueTypes.json" flex="60">
<md-input-container class="md-block">
<label translate>value.json-value</label>
<input required tb-json-to-string name="value" ng-model="model.value">
<div ng-messages="editDialog.value.$error">
<div translate ng-message="required">attribute.value-required</div>
</div>
</md-input-container>
<md-button class="md-icon-button" style="margin: 18px 0;"
ng-click="editJSON($event)" aria-label="{{ 'action.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.edit' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="open_in_new"></ng-md-icon>
</md-button>
</div>
</section> </section>
</fieldset> </fieldset>
</div> </div>
<div layout="row" layout-align="end" class="md-actions"> <div layout="row" layout-align="end" class="md-actions">
<md-button ng-click="dismiss()">{{ 'action.cancel' | <md-button ng-click="dismiss()">{{ 'action.cancel' | translate }}
translate }}
</md-button> </md-button>
<md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit" <md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit"
class="md-raised md-primary"> class="md-raised md-primary">
@ -69,4 +85,4 @@
</md-button> </md-button>
</div> </div>
</form> </form>
</md-edit-dialog> </md-edit-dialog>

2
ui/src/app/locale/locale.constant-el_GR.json

@ -2232,7 +2232,7 @@
"last": "Τελευταίος", "last": "Τελευταίος",
"time-period": "Χρονική Περίοδος" "time-period": "Χρονική Περίοδος"
}, },
"user": { "user": {
"user": "Χρήστης", "user": "Χρήστης",
"users": "Χρήστες", "users": "Χρήστες",
"management": "Διαχείριση Χρηστών", "management": "Διαχείριση Χρηστών",

8
ui/src/app/locale/locale.constant-en_US.json

@ -607,6 +607,7 @@
}, },
"details": { "details": {
"edit-mode": "Edit mode", "edit-mode": "Edit mode",
"edit-json": "Edit JSON",
"toggle-edit-mode": "Toggle edit mode" "toggle-edit-mode": "Toggle edit mode"
}, },
"device": { "device": {
@ -1270,7 +1271,8 @@
"js-func": { "js-func": {
"no-return-error": "Function must return value!", "no-return-error": "Function must return value!",
"return-type-mismatch": "Function must return value of '{{type}}' type!", "return-type-mismatch": "Function must return value of '{{type}}' type!",
"tidy": "Tidy" "tidy": "Tidy",
"mini": "Mini"
}, },
"key-val": { "key-val": {
"key": "Key", "key": "Key",
@ -1593,7 +1595,9 @@
"boolean-value": "Boolean value", "boolean-value": "Boolean value",
"false": "False", "false": "False",
"true": "True", "true": "True",
"long": "Long" "long": "Long",
"json": "JSON",
"json-value": "JSON value"
}, },
"widget": { "widget": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",

4
ui/src/app/locale/locale.constant-ru_RU.json

@ -607,6 +607,7 @@
}, },
"details": { "details": {
"edit-mode": "Режим редактирования", "edit-mode": "Режим редактирования",
"edit-json": "Редактировать JSON",
"toggle-edit-mode": "Режим редактирования" "toggle-edit-mode": "Режим редактирования"
}, },
"device": { "device": {
@ -1191,8 +1192,7 @@
}, },
"js-func": { "js-func": {
"no-return-error": "Функция должна возвращать значение!", "no-return-error": "Функция должна возвращать значение!",
"return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!", "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!"
"tidy": "Tidy"
}, },
"key-val": { "key-val": {
"key": "Ключ", "key": "Ключ",

4
ui/src/app/locale/locale.constant-uk_UA.json

@ -724,6 +724,7 @@
"details": { "details": {
"details": "Деталі", "details": "Деталі",
"edit-mode": "Режим редагування", "edit-mode": "Режим редагування",
"edit-json": "Редагувати JSON",
"toggle-edit-mode": "Перемкнути режим редагування" "toggle-edit-mode": "Перемкнути режим редагування"
}, },
"device": { "device": {
@ -1606,8 +1607,7 @@
}, },
"js-func": { "js-func": {
"no-return-error": "Функція повинна повертати значення!", "no-return-error": "Функція повинна повертати значення!",
"return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!", "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!"
"tidy": "Tidy"
}, },
"key-val": { "key-val": {
"key": "Ключ", "key": "Ключ",

Loading…
Cancel
Save