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..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\":{}}"
@@ -349,9 +349,9 @@
"sizeX": 7.5,
"sizeY": 4.5,
"resources": [],
- "templateHtml": "
",
- "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",
+ "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/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/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);
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/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/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/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/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 extends Enum> enumClass;
+
+ public static CassandraToSqlColumn idColumn(String name) {
+ return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ID);
+ }
+
+ public static CassandraToSqlColumn stringColumn(String name) {
+ return new CassandraToSqlColumn(name, CassandraToSqlColumnType.STRING);
+ }
+
+ public static CassandraToSqlColumn stringColumn(String cassandraColumnName, String sqlColumnName) {
+ return new CassandraToSqlColumn(cassandraColumnName, sqlColumnName);
+ }
+
+ public static CassandraToSqlColumn bigintColumn(String name) {
+ return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BIGINT);
+ }
+
+ public static CassandraToSqlColumn doubleColumn(String name) {
+ return new CassandraToSqlColumn(name, CassandraToSqlColumnType.DOUBLE);
+ }
+
+ public static CassandraToSqlColumn booleanColumn(String name) {
+ return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BOOLEAN);
+ }
+
+ public static CassandraToSqlColumn enumToIntColumn(String name, Class extends Enum> enumClass) {
+ return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ENUM_TO_INT, enumClass);
+ }
+
+ public CassandraToSqlColumn(String columnName) {
+ this(columnName, columnName, CassandraToSqlColumnType.STRING, null);
+ }
+
+ public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type) {
+ this(columnName, columnName, type, null);
+ }
+
+ public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type, Class extends Enum> enumClass) {
+ this(columnName, columnName, type, enumClass);
+ }
+
+ public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName) {
+ this(cassandraColumnName, sqlColumnName, CassandraToSqlColumnType.STRING, null);
+ }
+
+ public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName, CassandraToSqlColumnType type,
+ Class extends Enum> enumClass) {
+ this.cassandraColumnName = cassandraColumnName;
+ this.sqlColumnName = sqlColumnName;
+ this.type = type;
+ this.enumClass = enumClass;
+ switch (this.type) {
+ case ID:
+ case STRING:
+ this.sqlType = Types.VARCHAR;
+ break;
+ case DOUBLE:
+ this.sqlType = Types.DOUBLE;
+ break;
+ case INTEGER:
+ case ENUM_TO_INT:
+ this.sqlType = Types.INTEGER;
+ break;
+ case FLOAT:
+ this.sqlType = Types.FLOAT;
+ break;
+ case BIGINT:
+ this.sqlType = Types.BIGINT;
+ break;
+ case BOOLEAN:
+ this.sqlType = Types.BOOLEAN;
+ break;
+ }
+ }
+
+ public void prepareColumnValue(Row row, PreparedStatement sqlInsertStatement, int index) throws SQLException {
+ String value = this.getColumnValue(row, index);
+ this.setColumnValue(sqlInsertStatement, index, value);
+ }
+
+ private String getColumnValue(Row row, int index) {
+ if (row.isNull(index)) {
+ return null;
+ } else {
+ switch (this.type) {
+ case ID:
+ return UUIDConverter.fromTimeUUID(row.getUUID(index));
+ case DOUBLE:
+ return Double.toString(row.getDouble(index));
+ case INTEGER:
+ return Integer.toString(row.getInt(index));
+ case FLOAT:
+ return Float.toString(row.getFloat(index));
+ case BIGINT:
+ return Long.toString(row.getLong(index));
+ case BOOLEAN:
+ return Boolean.toString(row.getBool(index));
+ case STRING:
+ case ENUM_TO_INT:
+ default:
+ return row.getString(index);
+ }
+ }
+ }
+
+ private void setColumnValue(PreparedStatement sqlInsertStatement, int index, String value) throws SQLException {
+ if (value == null) {
+ sqlInsertStatement.setNull(index, this.sqlType);
+ } else {
+ switch (this.type) {
+ case DOUBLE:
+ sqlInsertStatement.setDouble(index, Double.parseDouble(value));
+ break;
+ case INTEGER:
+ sqlInsertStatement.setInt(index, Integer.parseInt(value));
+ break;
+ case FLOAT:
+ sqlInsertStatement.setFloat(index, Float.parseFloat(value));
+ break;
+ case BIGINT:
+ sqlInsertStatement.setLong(index, Long.parseLong(value));
+ break;
+ case BOOLEAN:
+ sqlInsertStatement.setBoolean(index, Boolean.parseBoolean(value));
+ break;
+ case ENUM_TO_INT:
+ Enum enumVal = Enum.valueOf(this.enumClass, value);
+ int intValue = enumVal.ordinal();
+ sqlInsertStatement.setInt(index, intValue);
+ break;
+ case STRING:
+ case ID:
+ default:
+ sqlInsertStatement.setString(index, value);
+ break;
+ }
+ }
+ }
+
+}
+
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 {
- 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/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/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
index 0d597965ea..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 = 1000,
+ showToast(type: NotificationType, message: string, duration: number,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
- this.store.dispatch(new ActionNotificationShow(
- {
- message,
- type,
- duration,
- verticalPosition,
- horizontalPosition,
- target,
- panelClass: this.ctx.widgetNamespace,
- forceDismiss: true
- }));
+ this.ctx.showToast(type, message, duration, verticalPosition, horizontalPosition, target);
}
}
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 @@
+
+
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/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 @@
+
+
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 @@
+