From 75a417986b96cc6bac6fd53590eca1bbcdf43886 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 28 Feb 2020 14:11:52 +0200 Subject: [PATCH 01/12] Implement device claiming widget. --- .../data/json/system/widget_bundles/input_widgets.json | 8 ++++---- .../thingsboard/server/controller/DeviceController.java | 2 +- ui-ngx/src/app/core/http/device.service.ts | 2 +- .../home/components/widget/dynamic-widget.component.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index f228cc1b72..301f1ae3eb 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -349,9 +349,9 @@ "sizeX": 7.5, "sizeY": 4.5, "resources": [], - "templateHtml": "
\n
\n \n \n \n
\n
{{requiredErrorDevice}}
\n
\n
\n \n \n \n
\n
{{requiredErrorSecretKey}}
\n
\n
\n
\n
\n {{labelClaimButon}}\n
\n
", - "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}", - "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": "
\n
\n \n {{deviceLabel}}\n \n \n {{requiredErrorDevice}}\n \n \n \n {{secretKeyLabel}}\n \n \n {{requiredErrorSecretKey}}\n \n \n
\n
\n \n
\n
\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 @@ } } ] -} +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 3c9bc7b71e..ad006c87fe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/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 claimDevice(@PathVariable(DEVICE_NAME) String deviceName, diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 9bc2e74ecc..b432c81e41 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/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 { - return this.http.post(`api/customer/device/${deviceName}/claim`, claimRequest, defaultHttpOptionsFromConfig(config)); + return this.http.post(`/api/customer/device/${deviceName}/claim`, claimRequest, defaultHttpOptionsFromConfig(config)); } public unclaimDevice(deviceName: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 0d597965ea..6911695fe0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -85,7 +85,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid this.showToast('error', message, undefined, 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) { From 96147bff8f2a17b5feb41bab9943f99ee491836a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 28 Feb 2020 14:13:12 +0200 Subject: [PATCH 02/12] Fix PreAuthorize annotation for claimDevice REST method. --- .../org/thingsboard/server/controller/DeviceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 828f75bd47..d147d27b40 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -390,7 +390,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 claimDevice(@PathVariable(DEVICE_NAME) String deviceName, From e7d00f4b68e255192fcdf50f7304e85dbbb851ea Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 28 Feb 2020 19:49:14 +0200 Subject: [PATCH 03/12] Multiple input widget --- .../system/widget_bundles/input_widgets.json | 6 +- .../widget/dynamic-widget.component.ts | 16 +- .../lib/multiple-input-widget.component.html | 126 +++++ .../lib/multiple-input-widget.component.scss | 75 +++ .../lib/multiple-input-widget.component.ts | 483 ++++++++++++++++++ .../widget/widget-components.module.ts | 7 +- .../components/widget/widget.component.ts | 1 + .../home/models/widget-component.models.ts | 45 +- .../time/timewindow-panel.component.ts | 6 +- 9 files changed, 743 insertions(+), 22 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 301f1ae3eb..3285ba7903 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -317,9 +317,9 @@ "sizeX": 7.5, "sizeY": 3.5, "resources": [], - "templateHtml": "\n", - "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": "\n", + "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\":{}}" diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 6911695fe0..855f4314b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/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, 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); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html new file mode 100644 index 0000000000..10bc9881a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -0,0 +1,126 @@ + +
+
+
+ {{ getGroupTitle(source.datasource) }} + +
+
+
+ + {{key.label}} + + {{key.settings.icon}} + + {{key.settings.requiredErrorMessage}} + + +
+
+ + {{key.label}} + + {{key.settings.icon}} + + {{key.settings.requiredErrorMessage}} + + +
+
+ + {{key.label}} + +
+
+ + {{key.label}} + +
+
+
+ + {{key.label}} + + + + + {{key.settings.requiredErrorMessage}} + + +
+
+
+
+
+
+ + +
+
+
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }} +
+
+ {{ 'widgets.input-widgets.not-allowed-entity' | translate }} +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss new file mode 100644 index 0000000000..de1a672535 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts new file mode 100644 index 0000000000..ca7a2c5526 --- /dev/null +++ b/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; + @ViewChild('multipleInputForm', {static: true}) multipleInputForm: NgForm; + + @Input() + ctx: WidgetContext; + + private formResizeListener: any; + private settings: MultipleInputWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasources: Array; + private sources: Array = []; + + 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, + 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) { + 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; + if (dataToSave) { + data = [dataToSave]; + } else { + data = this.sources; + } + const tasks: Observable[] = []; + 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(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index e3cc12b853..9754019025 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/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 diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 18497e342b..5dd493a4f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index e9bec966a4..185074a8ad 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/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; - $containerParent: JQuery; + $container: JQuery; + $containerParent: JQuery; width: number; height: number; $scope: IDynamicWidgetComponent; @@ -184,11 +192,44 @@ export class WidgetContext { ngZone?: NgZone; + store?: Store; + 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) { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index ac0b22815e..dea27b8f4f 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/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; } } From 7de309e217087a866c1ed0d03bb43e88bad56338 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 27 Feb 2020 16:06:44 +0200 Subject: [PATCH 04/12] Created HsqlEntityDatabaseSchemaService, improvement TimescaleTsDatabaseUpgradeService and TimescaleInsertTsRepository --- .../HsqlEntityDatabaseSchemaService.java | 33 +++++++++++++++++++ ...a => PsqlEntityDatabaseSchemaService.java} | 6 ++-- .../TimescaleTsDatabaseUpgradeService.java | 2 +- .../TimescaleInsertTsRepository.java | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java rename application/src/main/java/org/thingsboard/server/service/install/{SqlEntityDatabaseSchemaService.java => PsqlEntityDatabaseSchemaService.java} (83%) diff --git a/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java new file mode 100644 index 0000000000..0b3232903b --- /dev/null +++ b/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"); + } +} + diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java similarity index 83% rename from application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java rename to application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java index f124b78842..11da8b306a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/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"); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index 0c57e2e9bd..a2a9611581 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/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!"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java index 4cd0a4ab59..738ae52a9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java +++ b/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); From 118c81da5f3b3d2703268e147fd05308a44eaca1 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 28 Feb 2020 22:23:33 +0200 Subject: [PATCH 05/12] remove min and max json query from AggregationRepository --- .../server/dao/sqlts/timescale/AggregationRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java index bb0b13f2a6..ed784b96ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java +++ b/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 "; From db65eac63806591b54bd93816d6126e69f53f1aa Mon Sep 17 00:00:00 2001 From: Dmytro Shvaika Date: Thu, 27 Feb 2020 14:30:14 +0200 Subject: [PATCH 06/12] added better logging to ts upgrade --- .../install/AbstractSqlTsDatabaseUpgradeService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java index 01bed834b8..fe56ac129c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java +++ b/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); From f522dbca2a326e72d5bf3232a108ce8a23d117b9 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 27 Feb 2020 16:06:44 +0200 Subject: [PATCH 07/12] Created HsqlEntityDatabaseSchemaService, improvement TimescaleTsDatabaseUpgradeService and TimescaleInsertTsRepository --- .../HsqlEntityDatabaseSchemaService.java | 33 +++++++++++++++++++ ...a => PsqlEntityDatabaseSchemaService.java} | 6 ++-- .../TimescaleTsDatabaseUpgradeService.java | 2 +- .../TimescaleInsertTsRepository.java | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java rename application/src/main/java/org/thingsboard/server/service/install/{SqlEntityDatabaseSchemaService.java => PsqlEntityDatabaseSchemaService.java} (83%) diff --git a/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java new file mode 100644 index 0000000000..0b3232903b --- /dev/null +++ b/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"); + } +} + diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java similarity index 83% rename from application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java rename to application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java index f124b78842..11da8b306a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/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"); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index 0c57e2e9bd..a2a9611581 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/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!"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java index 4cd0a4ab59..738ae52a9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/timescale/TimescaleInsertTsRepository.java +++ b/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); From bf755f1c46087f97b2ccf772db49b059d37ef96d Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 28 Feb 2020 22:23:33 +0200 Subject: [PATCH 08/12] remove min and max json query from AggregationRepository --- .../server/dao/sqlts/timescale/AggregationRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java index bb0b13f2a6..ed784b96ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java +++ b/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 "; From 7d898f58a13f4755e512840c9d31ce7aff468910 Mon Sep 17 00:00:00 2001 From: Dmytro Shvaika Date: Thu, 27 Feb 2020 14:30:14 +0200 Subject: [PATCH 09/12] added better logging to ts upgrade --- .../install/AbstractSqlTsDatabaseUpgradeService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java index 01bed834b8..fe56ac129c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java +++ b/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); From b490e4ef4db8ecd6abfa71fd0f343a118a51954c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 2 Mar 2020 17:43:10 +0200 Subject: [PATCH 10/12] Cassandra 2.5.0 to SQL migration --- .../install/ThingsboardInstallService.java | 121 ++++----- .../DefaultSystemDataLoaderService.java | 18 ++ .../install/SystemDataLoaderService.java | 2 + .../CassandraEntitiesToSqlMigrateService.java | 253 ++++++++++++++++++ .../install/migrate/CassandraToSqlColumn.java | 159 +++++++++++ .../migrate/CassandraToSqlColumnType.java | 12 + .../install/migrate/CassandraToSqlTable.java | 99 +++++++ .../migrate/EntitiesMigrateService.java | 7 + 8 files changed, 609 insertions(+), 62 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 842550d209..e0daf43bf7 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/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!"); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index fd907714fa..4f35509429 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/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, diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index e386e84132..76e65deaa4 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/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; diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java new file mode 100644 index 0000000000..e5d7844ba4 --- /dev/null +++ b/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 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")) + ); +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java new file mode 100644 index 0000000000..56d1d53833 --- /dev/null +++ b/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 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 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 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 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; + } + } + } + +} + diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java new file mode 100644 index 0000000000..c20484a898 --- /dev/null +++ b/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 +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java new file mode 100644 index 0000000000..fc2fcf78db --- /dev/null +++ b/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 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 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 Date: Mon, 2 Mar 2020 17:50:37 +0200 Subject: [PATCH 11/12] [2.5] Feature add support json[ui] (#2446) * Merge with master * Refactoring code * Add license header Co-authored-by: Andrew Shvayka --- ui/src/app/api/widget.service.js | 4 +- ui/src/app/common/types.constant.js | 5 + .../app/components/json-content.directive.js | 32 ++-- ui/src/app/components/json-content.scss | 2 +- ui/src/app/components/json-content.tpl.html | 8 +- .../components/json-object-edit.directive.js | 10 +- ui/src/app/components/json-object-edit.scss | 13 ++ .../app/components/json-object-edit.tpl.html | 8 +- .../components/tb-json-to-string.directive.js | 45 ++++++ .../add-attribute-dialog.controller.js | 38 ++++- .../attribute/add-attribute-dialog.tpl.html | 34 ++++- ...d-widget-to-dashboard-dialog.controller.js | 1 - .../add-widget-to-dashboard-dialog.tpl.html | 20 +-- .../attribute-dialog-edit-json.controller.js | 35 +++++ .../attribute/attribute-dialog-edit-json.scss | 24 +++ .../attribute-dialog-edit-json.tpl.html | 54 +++++++ .../attribute/attribute-table.directive.js | 143 ++++++++++-------- .../app/entity/attribute/attribute-table.scss | 8 + .../entity/attribute/attribute-table.tpl.html | 4 +- .../edit-attribute-value.controller.js | 58 +++++-- .../attribute/edit-attribute-value.tpl.html | 30 +++- ui/src/app/locale/locale.constant-el_GR.json | 2 +- ui/src/app/locale/locale.constant-en_US.json | 8 +- ui/src/app/locale/locale.constant-ru_RU.json | 4 +- ui/src/app/locale/locale.constant-uk_UA.json | 4 +- 25 files changed, 464 insertions(+), 130 deletions(-) create mode 100644 ui/src/app/components/tb-json-to-string.directive.js create mode 100644 ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js create mode 100644 ui/src/app/entity/attribute/attribute-dialog-edit-json.scss create mode 100644 ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 206d3c3d14..4b706e827d 100644 --- a/ui/src/app/api/widget.service.js +++ b/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; diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 57ac8825f5..86bf952582 100644 --- a/ui/src/app/common/types.constant.js +++ b/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: { diff --git a/ui/src/app/components/json-content.directive.js b/ui/src/app/components/json-content.directive.js index 0788216db9..c4281b15c2 100644 --- a/ui/src/app/components/json-content.directive.js +++ b/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:'=?' }, diff --git a/ui/src/app/components/json-content.scss b/ui/src/app/components/json-content.scss index 86937c3af4..d444f8051f 100644 --- a/ui/src/app/components/json-content.scss +++ b/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); diff --git a/ui/src/app/components/json-content.tpl.html b/ui/src/app/components/json-content.tpl.html index 5b847f1af6..2b7670942d 100644 --- a/ui/src/app/components/json-content.tpl.html +++ b/ui/src/app/components/json-content.tpl.html @@ -17,11 +17,15 @@ -->
- + - {{ + {{ 'js-func.tidy' | translate }} + {{ + 'js-func.mini' | translate }} +
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js index 61b54faab7..538b1cfc41 100644 --- a/ui/src/app/components/json-object-edit.directive.js +++ b/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", diff --git a/ui/src/app/components/json-object-edit.scss b/ui/src/app/components/json-object-edit.scss index d58d9f4b23..9c9cdc7e44 100644 --- a/ui/src/app/components/json-object-edit.scss +++ b/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; diff --git a/ui/src/app/components/json-object-edit.tpl.html b/ui/src/app/components/json-object-edit.tpl.html index aa6c867555..86f2a55650 100644 --- a/ui/src/app/components/json-object-edit.tpl.html +++ b/ui/src/app/components/json-object-edit.tpl.html @@ -16,12 +16,18 @@ -->
-
+
+ + {{'js-func.tidy' | translate }} + + + {{'js-func.mini' | translate }} +
diff --git a/ui/src/app/components/tb-json-to-string.directive.js b/ui/src/app/components/tb-json-to-string.directive.js new file mode 100644 index 0000000000..79bfadb540 --- /dev/null +++ b/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); + } + }; +} diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.controller.js b/ui/src/app/entity/attribute/add-attribute-dialog.controller.js index 6b277f08ef..e1c711ba66 100644 --- a/ui/src/app/entity/attribute/add-attribute-dialog.controller.js +++ b/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, + }); + } } diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html b/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html index f13e31eee9..6520d7a541 100644 --- a/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html +++ b/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html @@ -26,7 +26,8 @@
- +
@@ -58,7 +59,8 @@ - +
attribute.value-required
value.invalid-integer-value
@@ -71,11 +73,31 @@
attribute.value-required
-
- +
+ {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }}
+
+ + + +
+
attribute.value-required
+
+
+ + + {{ 'action.edit' | translate }} + + + +
@@ -87,8 +109,8 @@ class="md-raised md-primary"> {{ 'action.add' | translate }} - {{ 'action.cancel' | - translate }} + + {{ 'action.cancel' | translate }} diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js index 173132a4b3..076bee89d5 100644 --- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js +++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js @@ -151,5 +151,4 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, } ); } - } diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html index 10d7e4ce55..b6c1746b41 100644 --- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html +++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html @@ -26,7 +26,8 @@
- +
@@ -37,10 +38,10 @@
dashboard.select-existing + ng-disabled="$root.loading || vm.addToDashboardType != 0" + tb-required="vm.addToDashboardType === 0" + ng-model="vm.dashboardId" + select-first-dashboard="false">
@@ -49,7 +50,8 @@ dashboard.create-new - +
dashboard.title-required
@@ -66,15 +68,15 @@ + style="margin-bottom: 0; padding-right: 20px;"> {{ 'dashboard.open-dashboard' | translate }} {{ 'action.add' | translate }} - {{ 'action.cancel' | - translate }} + + {{ 'action.cancel' | translate }} diff --git a/ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js b/ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js new file mode 100644 index 0000000000..e53d168a68 --- /dev/null +++ b/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(); + }; +} diff --git a/ui/src/app/entity/attribute/attribute-dialog-edit-json.scss b/ui/src/app/entity/attribute/attribute-dialog-edit-json.scss new file mode 100644 index 0000000000..84219ac11a --- /dev/null +++ b/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; + } +} diff --git a/ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html b/ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html new file mode 100644 index 0000000000..1c801edc10 --- /dev/null +++ b/ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html @@ -0,0 +1,54 @@ + + +
+ +
+

{{ 'details.edit-json' | translate }}

+ + + + +
+
+ +
+ + +
+
+ + + {{'action.save'|translate}} + + + {{'action.cancel'|translate }} + + +
+
diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js index 3ba1f51744..59ea4fe91d 100644 --- a/ui/src/app/entity/attribute/attribute-table.directive.js +++ b/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", diff --git a/ui/src/app/entity/attribute/attribute-table.scss b/ui/src/app/entity/attribute/attribute-table.scss index dd48a5d4b5..5a0bde0c68 100644 --- a/ui/src/app/entity/attribute/attribute-table.scss +++ b/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; +} diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html index e6a8b12d3d..8844891e62 100644 --- a/ui/src/app/entity/attribute/attribute-table.tpl.html +++ b/ui/src/app/entity/attribute/attribute-table.tpl.html @@ -77,9 +77,7 @@
- diff --git a/ui/src/app/entity/attribute/edit-attribute-value.controller.js b/ui/src/app/entity/attribute/edit-attribute-value.controller.js index 208ad9a678..7cb550d4c1 100644 --- a/ui/src/app/entity/attribute/edit-attribute-value.controller.js +++ b/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 + }); + } } diff --git a/ui/src/app/entity/attribute/edit-attribute-value.tpl.html b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html index ca4f8181f1..35abe746aa 100644 --- a/ui/src/app/entity/attribute/edit-attribute-value.tpl.html +++ b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html @@ -15,8 +15,8 @@ limitations under the License. --> - -
+ +
@@ -38,7 +38,8 @@ - +
attribute.value-required
value.invalid-integer-value
@@ -52,16 +53,31 @@
- + {{ (model.value ? 'value.true' : 'value.false') | translate }}
+
+ + + +
+
attribute.value-required
+
+
+ + + {{ 'action.edit' | translate }} + + + +
- {{ 'action.cancel' | - translate }} + {{ 'action.cancel' | translate }} @@ -69,4 +85,4 @@
-
\ No newline at end of file +
diff --git a/ui/src/app/locale/locale.constant-el_GR.json b/ui/src/app/locale/locale.constant-el_GR.json index 1d81f7ea19..8d4951f28f 100644 --- a/ui/src/app/locale/locale.constant-el_GR.json +++ b/ui/src/app/locale/locale.constant-el_GR.json @@ -2232,7 +2232,7 @@ "last": "Τελευταίος", "time-period": "Χρονική Περίοδος" }, - "user": { + "user": { "user": "Χρήστης", "users": "Χρήστες", "management": "Διαχείριση Χρηστών", diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index d9e33e4488..f0dc673f84 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/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", diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json index d5284ebd1a..47c70da48a 100644 --- a/ui/src/app/locale/locale.constant-ru_RU.json +++ b/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": "Ключ", diff --git a/ui/src/app/locale/locale.constant-uk_UA.json b/ui/src/app/locale/locale.constant-uk_UA.json index 75124022dd..7b882423ed 100644 --- a/ui/src/app/locale/locale.constant-uk_UA.json +++ b/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": "Ключ", From 516ae3d8142c4c554cd20499bc6a2774300b48eb Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Mon, 2 Mar 2020 17:55:29 +0200 Subject: [PATCH 12/12] Create ui to support attribute type JSON (#2471) --- .../attribute/attribute-table.component.html | 2 +- .../json-object-edit-dialog.component.html | 57 +++++++++++ .../json-object-edit-dialog.component.ts | 63 ++++++++++++ .../directives/tb-json-to-string.directive.ts | 96 +++++++++++++++++++ .../components/json-content.component.html | 6 +- .../components/json-content.component.ts | 8 +- .../json-object-edit.component.html | 10 +- .../json-object-edit.component.scss | 19 ++++ .../components/json-object-edit.component.ts | 16 ++++ .../components/value-input.component.html | 16 ++++ .../components/value-input.component.ts | 34 ++++++- ui-ngx/src/app/shared/models/constants.ts | 10 +- ui-ngx/src/app/shared/pipe/tbJson.pipe.ts | 30 ++++++ ui-ngx/src/app/shared/shared.module.ts | 13 ++- .../assets/locale/locale.constant-en_US.json | 10 +- .../assets/locale/locale.constant-ru_RU.json | 4 +- .../assets/locale/locale.constant-uk_UA.json | 4 +- 17 files changed, 384 insertions(+), 14 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html create mode 100644 ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts create mode 100644 ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts create mode 100644 ui-ngx/src/app/shared/pipe/tbJson.pipe.ts diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 4e40fdbe90..774fc805da 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -168,7 +168,7 @@ class="tb-value-cell" (click)="editAttribute($event, attribute)">
- {{attribute.value}} + {{attribute.value | tbJson}} edit diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html new file mode 100644 index 0000000000..07098ea49c --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

{{ (this.data.title ? this.data.title : 'details.edit-json') | translate }}

+ + +
+ + +
+
+
+ + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts new file mode 100644 index 0000000000..5fc3882a35 --- /dev/null +++ b/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 + implements OnInit { + + jsonFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: JsonObjectEdittDialogData, + public dialogRef: MatDialogRef, + 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); + } +} diff --git a/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts b/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts new file mode 100644 index 0000000000..cf7cd0a0bd --- /dev/null +++ b/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 { + } +} diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 9daa94a137..d072c674f4 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -22,9 +22,13 @@ + +
+
+ + value.json-value + + + + {{ (requiredText ? requiredText : 'value.json-value-required') | translate }} + + + {{ 'value.json-value-invalid' | translate }} + + +
diff --git a/ui-ngx/src/app/shared/components/value-input.component.ts b/ui-ngx/src/app/shared/components/value-input.component.ts index d6c61a2438..0913aefe12 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.ts +++ b/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, { + 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; } diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 39235f5f45..4c9612489a 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/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( @@ -153,6 +154,13 @@ export const valueTypesMap = new Map( name: 'value.boolean', icon: 'mdi:checkbox-marked-outline' } + ], + [ + ValueType.JSON, + { + name: 'value.json', + icon: 'mdi:json' + } ] ] ); diff --git a/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts b/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts new file mode 100644 index 0000000000..c31cf161ab --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 99fb460f7d..1df4469fb2 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/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 { } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bd19625619..978e2cda87 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index 02bea6e453..6337b14854 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/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": "Ключ", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index d841880dd1..471031c9a3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/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": "Ключ",