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,
"sizeY": 3.5,
"resources": [],
"templateHtml": "<tb-multiple-input-widget \n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-multiple-input-widget>",
"templateCss": "",
"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",
"templateHtml": "<tb-multiple-input-widget \n [ctx]=\"ctx\">\n</tb-multiple-input-widget>",
"templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\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}",
"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\":{}}"
@ -349,9 +349,9 @@
"sizeX": 7.5,
"sizeY": 4.5,
"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>",
"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}",
"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",
"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",
"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}",
"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\":{}}"
@ -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)
@ResponseBody
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.SystemDataLoaderService;
import org.thingsboard.server.service.install.TsDatabaseSchemaService;
import org.thingsboard.server.service.install.migrate.EntitiesMigrateService;
import org.thingsboard.server.service.install.update.DataUpdateService;
@Service
@ -68,99 +69,95 @@ public class ThingsboardInstallService {
@Autowired
private DataUpdateService dataUpdateService;
@Autowired(required = false)
private EntitiesMigrateService entitiesMigrateService;
public void performInstall() {
try {
if (isUpgrade) {
log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion);
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 ...");
databaseEntitiesUpgradeService.upgradeDatabase("1.2.3");
if ("2.5.0-cassandra".equals(upgradeFromVersion)) {
log.info("Migrating ThingsBoard entities data from cassandra to SQL database ...");
entitiesMigrateService.migrate();
log.info("Updating system data...");
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
log.info("Upgrading ThingsBoard from version 1.3.0 to 1.3.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("1.2.3");
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
log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("1.3.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":
log.info("Upgrading ThingsBoard from version 1.4.0 to 2.0.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("1.3.1");
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":
log.info("Upgrading ThingsBoard from version 2.0.0 to 2.1.1 ...");
dataUpdateService.updateData("1.4.0");
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":
log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.0.0");
databaseEntitiesUpgradeService.upgradeDatabase("2.1.1");
case "2.1.3":
log.info("Upgrading ThingsBoard from version 2.1.3 to 2.2.0 ...");
case "2.1.1":
log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ...");
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":
log.info("Upgrading ThingsBoard from version 2.3.0 to 2.3.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.1.3");
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":
log.info("Upgrading ThingsBoard from version 2.3.1 to 2.4.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.3.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":
log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.3.1");
case "2.4.1":
log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ...");
case "2.4.0":
log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ...");
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.1":
log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ...");
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":
log.info("Upgrading ThingsBoard from version 2.4.3 to 2.5 ...");
databaseEntitiesUpgradeService.upgradeDatabase("2.4.2");
if (databaseTsUpgradeService != null) {
databaseTsUpgradeService.upgradeDatabase("2.4.3");
}
databaseEntitiesUpgradeService.upgradeDatabase("2.4.3");
case "2.4.3":
log.info("Upgrading ThingsBoard from version 2.4.3 to 2.5 ...");
log.info("Updating system data...");
if (databaseTsUpgradeService != null) {
databaseTsUpgradeService.upgradeDatabase("2.4.3");
}
databaseEntitiesUpgradeService.upgradeDatabase("2.4.3");
systemDataLoaderService.deleteSystemWidgetBundle("charts");
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");
log.info("Updating system data...");
systemDataLoaderService.loadSystemWidgets();
break;
default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
systemDataLoaderService.updateSystemWidgets();
break;
default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
}
}
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.Connection;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Types;
@Slf4j
@ -93,6 +94,15 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
try {
CallableStatement callableStatement = conn.prepareCall("{" + query + "}");
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();
log.info(SUCCESSFULLY_EXECUTED_FUNCTION, query.replace(CALL_REGEX, ""));
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();
}
@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,
TenantId tenantId,
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.stereotype.Service;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlDao;
@Service
@SqlDao
@PsqlDao
@Profile("install")
public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
public class PsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
implements EntityDatabaseSchemaService {
public SqlEntityDatabaseSchemaService() {
public PsqlEntityDatabaseSchemaService() {
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 updateSystemWidgets() throws Exception;
void loadDemoData() 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_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;");
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) {
ps.setBoolean(5, tsKvEntity.getBooleanValue());
ps.setBoolean(9, tsKvEntity.getBooleanValue());
ps.setBoolean(10, tsKvEntity.getBooleanValue());
} else {
ps.setNull(5, 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_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 ";

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

@ -135,7 +135,7 @@ export class DeviceService {
public claimDevice(deviceName: string, claimRequest: ClaimRequest,
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) {

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

@ -168,7 +168,7 @@
class="tb-value-cell"
(click)="editAttribute($event, attribute)">
<div fxLayout="row">
<span fxFlex>{{attribute.value}}</span>
<span fxFlex>{{attribute.value | tbJson}}</span>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon>
</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',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
this.showToast('success', message, duration, verticalPosition, horizontalPosition, target);
this.ctx.showSuccessToast(message, duration, verticalPosition, horizontalPosition, target);
}
showErrorToast(message: string,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
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',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
this.store.dispatch(new ActionNotificationShow(
{
message,
type,
duration,
verticalPosition,
horizontalPosition,
target,
panelClass: this.ctx.widgetNamespace,
forceDismiss: true
}));
this.ctx.showToast(type, message, duration, verticalPosition, horizontalPosition, target);
}
}

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,
DateRangeNavigatorWidgetComponent
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
@NgModule({
declarations:
@ -41,7 +42,8 @@ import {
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent,
DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent
DateRangeNavigatorPanelComponent,
MultipleInputWidgetComponent
],
imports: [
CommonModule,
@ -55,7 +57,8 @@ import {
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent,
RpcWidgetsModule,
DateRangeNavigatorWidgetComponent
DateRangeNavigatorWidgetComponent,
MultipleInputWidgetComponent
],
providers: [
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.changeDetector = this.cd;
this.widgetContext.ngZone = this.ngZone;
this.widgetContext.store = this.store;
this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.isEdit = this.isEdit;
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 { forkJoin, Observable, of, ReplaySubject } from 'rxjs';
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 {
name: string;
@ -152,8 +160,8 @@ export class WidgetContext {
formatValue
};
$container: JQuery<any>;
$containerParent: JQuery<any>;
$container: JQuery<HTMLElement>;
$containerParent: JQuery<HTMLElement>;
width: number;
height: number;
$scope: IDynamicWidgetComponent;
@ -184,11 +192,44 @@ export class WidgetContext {
ngZone?: NgZone;
store?: Store<AppState>;
rxjs = {
forkJoin,
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) {
if (!this.destroyed) {
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>
<span fxFlex></span>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()">
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 type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen"
class="tb-mat-32"
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});
this.jsonEditor.setValue(res ? res : '', -1);
this.updateView();
}
minifyJSON() {
const res = JSON.stringify(this.contentBody);
this.jsonEditor.setValue(res ? res : '', -1);
this.updateView();
}
onFullscreen() {
if (this.jsonEditor) {
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}"
tb-fullscreen
[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"
ng-class="{'tb-required': required,
'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label>
<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"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
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 {
height: 100%;
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 {
this.modelValue = value;
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 {
Aggregation,
aggregationTranslations,
AggregationType,
AggregationType, DAY,
HistoryWindow,
HistoryWindowType,
IntervalWindow,
@ -205,9 +205,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
const timewindowFormValue = this.timewindowForm.getRawValue();
if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
return timewindowFormValue.history.timewindowMs;
} else {
} else if (timewindowFormValue.history.fixedTimewindow) {
return timewindowFormValue.history.fixedTimewindow.endTimeMs -
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 }}
</mat-checkbox>
</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>
</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 { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms';
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({
selector: 'tb-value-input',
@ -50,13 +56,35 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
private propagateChange = null;
constructor() {
constructor(
public dialog: MatDialog,
) {
}
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 {
this.propagateChange = fn;
}
@ -78,6 +106,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
} else {
this.valueType = ValueType.DOUBLE;
}
} else if (isObject(this.modelValue)) {
this.valueType = ValueType.JSON;
} else {
this.valueType = ValueType.STRING;
}
@ -94,6 +124,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
onValueTypeChanged() {
if (this.valueType === ValueType.BOOLEAN) {
this.modelValue = false;
} if (this.valueType === ValueType.JSON) {
this.modelValue = {};
} else {
this.modelValue = null;
}

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

@ -121,7 +121,8 @@ export enum ValueType {
STRING = 'STRING',
INTEGER = 'INTEGER',
DOUBLE = 'DOUBLE',
BOOLEAN = 'BOOLEAN'
BOOLEAN = 'BOOLEAN',
JSON = 'JSON'
}
export const valueTypesMap = new Map<ValueType, ValueTypeData>(
@ -153,6 +154,13 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>(
name: 'value.boolean',
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 { EntitySubTypeListComponent } from '@shared/components/entity/entity-subtype-list.component';
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 { MatChipDraggableDirective } from '@shared/components/mat-chip-draggable.directive';
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 { NavTreeComponent } from '@shared/components/nav-tree.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({
providers: [
@ -132,6 +135,7 @@ import { LedLightComponent } from '@shared/components/led-light.component';
EnumToArrayPipe,
HighlightPipe,
TruncatePipe,
TbJsonPipe,
{
provide: FlowInjectionToken,
useValue: Flow
@ -202,7 +206,10 @@ import { LedLightComponent } from '@shared/components/led-light.component';
EnumToArrayPipe,
HighlightPipe,
TruncatePipe,
KeyboardShortcutPipe
TbJsonPipe,
KeyboardShortcutPipe,
TbJsonToStringDirective,
JsonObjectEditDialogComponent
],
imports: [
CommonModule,
@ -357,8 +364,10 @@ import { LedLightComponent } from '@shared/components/led-light.component';
EnumToArrayPipe,
HighlightPipe,
TruncatePipe,
TbJsonPipe,
KeyboardShortcutPipe,
TranslateModule
TranslateModule,
JsonObjectEditDialogComponent
]
})
export class SharedModule { }

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

@ -626,6 +626,7 @@
"details": {
"details": "Details",
"edit-mode": "Edit mode",
"edit-json": "Edit JSON",
"toggle-edit-mode": "Toggle edit mode"
},
"device": {
@ -1298,7 +1299,8 @@
"js-func": {
"no-return-error": "Function must return value!",
"return-type-mismatch": "Function must return value of '{{type}}' type!",
"tidy": "Tidy"
"tidy": "Tidy",
"mini": "Mini"
},
"key-val": {
"key": "Key",
@ -1635,7 +1637,11 @@
"boolean-value": "Boolean value",
"false": "False",
"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-library": "Widgets Library",

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

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

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

@ -724,6 +724,7 @@
"details": {
"details": "Деталі",
"edit-mode": "Режим редагування",
"edit-json": "Редагувати JSON",
"toggle-edit-mode": "Перемкнути режим редагування"
},
"device": {
@ -1606,8 +1607,7 @@
},
"js-func": {
"no-return-error": "Функція повинна повертати значення!",
"return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!",
"tidy": "Tidy"
"return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!"
},
"key-val": {
"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 thingsboardJsonToString from '../components/tb-json-to-string.directive';
import TbFlot from '../widget/lib/flot-widget';
import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-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,
thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes,
thingsboardUtils, TripAnimationWidget])
thingsboardUtils, thingsboardJsonToString, TripAnimationWidget])
.factory('widgetService', WidgetService)
.name;

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

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

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

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

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

@ -27,7 +27,7 @@ tb-json-content {
min-height: 15px;
padding: 4px;
margin: 0 5px 0 0;
font-size: .8rem;
font-size: 12px;
line-height: 15px;
color: #7b7b7b;
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 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>
<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 }}
</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>
</div>
<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();
};
scope.beautifyJSON = function () {
scope.contentBody = angular.toJson(scope.object, 4);
};
scope.minifyJSON = function () {
scope.contentBody = angular.toJson(scope.object);
};
function updateEditorSize() {
if (scope.json_editor) {
scope.json_editor.resize();
@ -169,7 +177,7 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
});
$compile(element.contents())(scope);
}
};
return {
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 {
height: 100%;
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 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"
ng-class="{'tb-required': required,
'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label>
<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>
</div>
<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
* 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*/
export default function AddAttributeDialogController($scope, $mdDialog, types, attributeService, entityType, entityId, attributeScope) {
var vm = this;
let vm = this;
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) {
vm.attribute.value = false;
} else if (vm.valueType === types.valueType.json) {
vm.attribute.value = {};
} else {
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>
</div>
</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>
<md-dialog-content>
<div class="md-dialog-content">
@ -58,7 +59,8 @@
</md-input-container>
<md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block">
<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 translate ng-message="required">attribute.value-required</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>
</md-input-container>
<div layout="column" layout-align="center" flex="60" ng-if="vm.valueType===vm.valueTypes.boolean">
<md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0px;">
<div layout="column" layout-align="center" flex="60"
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 }}
</md-checkbox>
</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>
</fieldset>
</md-content>
@ -87,8 +109,8 @@
class="md-raised md-primary">
{{ 'action.add' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">
{{ 'action.cancel' | translate }}
</md-button>
</md-dialog-actions>
</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>
</div>
</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>
<md-dialog-content>
<div class="md-dialog-content">
@ -37,10 +38,10 @@
<section flex layout="column" style="width: 300px;">
<span translate style="padding-bottom: 10px;">dashboard.select-existing</span>
<tb-dashboard-autocomplete the-form="theForm"
ng-disabled="$root.loading || vm.addToDashboardType != 0"
tb-required="vm.addToDashboardType === 0"
ng-model="vm.dashboardId"
select-first-dashboard="false">
ng-disabled="$root.loading || vm.addToDashboardType != 0"
tb-required="vm.addToDashboardType === 0"
ng-model="vm.dashboardId"
select-first-dashboard="false">
</tb-dashboard-autocomplete>
</section>
</md-radio-button>
@ -49,7 +50,8 @@
<span translate>dashboard.create-new</span>
<md-input-container class="md-block">
<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 translate ng-message="required">dashboard.title-required</div>
</div>
@ -66,15 +68,15 @@
<md-checkbox
ng-model="vm.openDashboard"
aria-label="{{ 'dashboard.open-dashboard' | translate }}"
style="margin-bottom: 0px; padding-right: 20px;">
style="margin-bottom: 0; padding-right: 20px;">
{{ 'dashboard.open-dashboard' | translate }}
</md-checkbox>
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.add' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">
{{ 'action.cancel' | translate }}
</md-button>
</md-dialog-actions>
</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);
var getAttributeScopeByValue = function(attributeScopeValue) {
var getAttributeScopeByValue = function (attributeScopeValue) {
if (scope.types.latestTelemetry.value === attributeScopeValue) {
return scope.types.latestTelemetry;
}
@ -48,7 +48,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
return scope.attributeScopes[attrScope];
}
}
}
};
scope.types = types;
@ -87,14 +87,14 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
search: null
};
scope.$watch("entityId", function(newVal) {
scope.$watch("entityId", function (newVal) {
if (newVal) {
scope.resetFilter();
scope.getEntityAttributes(false, true);
}
});
scope.$watch("attributeScope", function(newVal, prevVal) {
scope.$watch("attributeScope", function (newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
scope.mode = 'default';
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.query.search = null;
scope.selectedAttributes = [];
scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
}
};
scope.enterFilterMode = function(event) {
scope.enterFilterMode = function (event) {
let $button = angular.element(event.currentTarget);
let $toolbarsContainer = $button.closest('.toolbarsContainer');
scope.query.search = '';
$timeout(()=>{
$timeout(() => {
$toolbarsContainer.find('.searchInput').focus();
})
}
};
scope.exitFilterMode = function() {
scope.exitFilterMode = function () {
scope.query.search = null;
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) {
scope.getEntityAttributes();
}
@ -142,15 +142,15 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
}
}
scope.onReorder = function() {
scope.onReorder = function () {
scope.getEntityAttributes(false, false);
}
};
scope.onPaginate = function() {
scope.onPaginate = function () {
scope.getEntityAttributes(false, false);
}
};
scope.getEntityAttributes = function(forceUpdate, reset) {
scope.getEntityAttributes = function (forceUpdate, reset) {
if (scope.attributesDeferred) {
scope.attributesDeferred.resolve();
}
@ -163,7 +163,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
}
scope.checkSubscription();
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);
}
);
@ -176,9 +176,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
});
deferred.resolve();
}
}
};
scope.checkSubscription = function() {
scope.checkSubscription = function () {
var newSubscriptionId = null;
if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') {
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);
}
scope.subscriptionId = newSubscriptionId;
}
};
scope.$on('$destroy', function() {
scope.$on('$destroy', function () {
if (scope.subscriptionId) {
attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
}
});
scope.editAttribute = function($event, attribute) {
scope.editAttribute = function ($event, attribute) {
if (!scope.attributeScope.clientSide) {
$event.stopPropagation();
$mdEditDialog.show({
controller: EditAttributeValueController,
templateUrl: editAttributeValueTemplate,
locals: {attributeValue: attribute.value,
save: function (model) {
var updatedAttribute = angular.copy(attribute);
updatedAttribute.value = model.value;
attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then(
function success() {
scope.getEntityAttributes();
}
);
}},
locals: {
attributeValue: attribute.value,
save: function (model) {
var updatedAttribute = angular.copy(attribute);
updatedAttribute.value = model.value;
attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then(
function success() {
scope.getEntityAttributes();
}
);
}
},
targetEvent: $event
});
}
}
};
scope.addAttribute = function($event) {
scope.addAttribute = function ($event) {
if (!scope.attributeScope.clientSide) {
$event.stopPropagation();
$mdDialog.show({
@ -224,16 +226,20 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
controllerAs: 'vm',
templateUrl: addAttributeDialogTemplate,
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,
targetEvent: $event
}).then(function () {
scope.getEntityAttributes();
});
}
}
};
scope.deleteAttributes = function($event) {
scope.deleteAttributes = function ($event) {
if (!scope.attributeScope.clientSide) {
$event.stopPropagation();
var confirm = $mdDialog.confirm()
@ -244,33 +250,33 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then(
function success() {
scope.selectedAttributes = [];
scope.getEntityAttributes();
}
)
attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then(
function success() {
scope.selectedAttributes = [];
scope.getEntityAttributes();
}
)
});
}
}
};
scope.nextWidget = function() {
scope.nextWidget = function () {
$mdUtil.nextTick(function () {
if (scope.widgetsCarousel.index < scope.widgetsList.length - 1) {
scope.widgetsCarousel.index++;
}
});
}
};
scope.prevWidget = function() {
scope.prevWidget = function () {
$mdUtil.nextTick(function () {
if (scope.widgetsCarousel.index > 0) {
scope.widgetsCarousel.index--;
}
});
}
};
scope.enterWidgetMode = function() {
scope.enterWidgetMode = function () {
if (scope.widgetsIndexWatch) {
scope.widgetsIndexWatch();
@ -303,7 +309,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
entitiAliases[entityAlias.id] = entityAlias;
var stateController = {
getStateParams: function() {
getStateParams: function () {
return {};
}
};
@ -317,9 +323,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
type: types.datasourceType.entity,
entityAliasId: entityAlias.id,
dataKeys: []
}
};
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 dataKey = {
name: attribute.key,
@ -328,12 +334,12 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
color: utils.getMaterialColor(i),
settings: {},
_hash: Math.random()
}
};
datasource.dataKeys.push(dataKey);
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)) {
var index = scope.widgetsCarousel.index;
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)) {
scope.widgetsList = [];
scope.widgetsListCache = [];
@ -358,7 +364,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
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++) {
var widgetType = widgetTypes[i];
@ -398,9 +404,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
}
}
});
}
};
scope.exitWidgetMode = function() {
scope.exitWidgetMode = function () {
if (scope.widgetsBundleWatch) {
scope.widgetsBundleWatch();
scope.widgetsBundleWatch = null;
@ -412,9 +418,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
scope.selectedWidgetsBundleAlias = null;
scope.mode = 'default';
scope.getEntityAttributes(true);
}
};
scope.addWidgetToDashboard = function($event) {
scope.addWidgetToDashboard = function ($event) {
if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
$event.stopPropagation();
@ -423,21 +429,26 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
controllerAs: 'vm',
templateUrl: addWidgetToDashboardDialogTemplate,
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,
targetEvent: $event
}).then(function () {
});
}
}
};
scope.loading = function() {
scope.loading = function () {
return $rootScope.loading;
}
};
$compile(element.contents())(scope);
}
};
return {
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 class="md-table-toolbar alternate" ng-show="mode==='default' && selectedAttributes.length">
<div class="md-toolbar-tools">
<span translate="{{attributeScope === types.latestTelemetry
? 'attribute.selected-telemetry'
: 'attribute.selected-attributes'}}"
<span translate="{{(attributeScope === types.latestTelemetry) ? 'attribute.selected-telemetry' : 'attribute.selected-attributes'}}"
translate-values="{count: selectedAttributes.length}"
translate-interpolation="messageformat"></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
* 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*/
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.model = {};
$scope.model.value = attributeValue;
$scope.model = {
value: attributeValue
};
if ($scope.model.value === true || $scope.model.value === false) {
$scope.valueType = types.valueType.boolean;
@ -30,26 +35,27 @@ export default function EditAttributeValueController($scope, $q, $element, types
} else {
$scope.valueType = types.valueType.double;
}
} else if (angular.isObject($scope.model.value)) {
$scope.valueType = types.valueType.json;
} else {
$scope.valueType = types.valueType.string;
}
$scope.submit = submit;
$scope.dismiss = dismiss;
$scope.editJSON = editJSON;
function dismiss() {
$element.remove();
}
function update() {
if($scope.editDialog.$invalid) {
if ($scope.editDialog.$invalid) {
return $q.reject();
}
if(angular.isFunction(save)) {
if (angular.isFunction(save)) {
return $q.when(save($scope.model));
}
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) {
$scope.model.value = false;
} else if ($scope.valueType === types.valueType.json) {
$scope.model.value = {};
} else {
$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.
-->
<md-edit-dialog>
<form name="editDialog" ng-submit="submit()">
<md-edit-dialog class="tb-edit-dialog">
<form name="editDialog" ng-submit="submit()" >
<div layout="column" class="md-content" style="width: 400px;">
<fieldset>
<section layout="row">
@ -38,7 +38,8 @@
</md-input-container>
<md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block">
<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 translate ng-message="required">attribute.value-required</div>
<div translate ng-message="pattern">value.invalid-integer-value</div>
@ -52,16 +53,31 @@
</div>
</md-input-container>
<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 }}
</md-checkbox>
</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>
</fieldset>
</div>
<div layout="row" layout-align="end" class="md-actions">
<md-button ng-click="dismiss()">{{ 'action.cancel' |
translate }}
<md-button ng-click="dismiss()">{{ 'action.cancel' | translate }}
</md-button>
<md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit"
class="md-raised md-primary">
@ -69,4 +85,4 @@
</md-button>
</div>
</form>
</md-edit-dialog>
</md-edit-dialog>

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

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

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

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

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

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

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

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

Loading…
Cancel
Save